diff --git a/src/org/opendatakit/validate/FormValidator.java b/src/org/opendatakit/validate/FormValidator.java index 1421253..ab8a23d 100644 --- a/src/org/opendatakit/validate/FormValidator.java +++ b/src/org/opendatakit/validate/FormValidator.java @@ -84,31 +84,31 @@ public class FormValidator implements ActionListener { * Classes needed to serialize objects. Need to put anything from JR in here. */ public final static String[] SERIALIABLE_CLASSES = { - "org.javarosa.core.services.locale.ResourceFileDataSource", // JavaRosaCoreModule - "org.javarosa.core.services.locale.TableLocaleSource", // JavaRosaCoreModule - "org.javarosa.core.model.FormDef", - "org.javarosa.core.model.SubmissionProfile", // CoreModelModule - "org.javarosa.core.model.QuestionDef", // CoreModelModule - "org.javarosa.core.model.GroupDef", // CoreModelModule - "org.javarosa.core.model.instance.FormInstance", // CoreModelModule - "org.javarosa.core.model.data.BooleanData", // CoreModelModule - "org.javarosa.core.model.data.DateData", // CoreModelModule - "org.javarosa.core.model.data.DateTimeData", // CoreModelModule - "org.javarosa.core.model.data.DecimalData", // CoreModelModule - "org.javarosa.core.model.data.GeoPointData", // CoreModelModule - "org.javarosa.core.model.data.GeoShapeData", // CoreModelModule - "org.javarosa.core.model.data.GeoTraceData", // CoreModelModule - "org.javarosa.core.model.data.IntegerData", // CoreModelModule - "org.javarosa.core.model.data.LongData", // CoreModelModule - "org.javarosa.core.model.data.MultiPointerAnswerData", // CoreModelModule - "org.javarosa.core.model.data.PointerAnswerData", // CoreModelModule - "org.javarosa.core.model.data.SelectMultiData", // CoreModelModule - "org.javarosa.core.model.data.SelectOneData", // CoreModelModule - "org.javarosa.core.model.data.StringData", // CoreModelModule - "org.javarosa.core.model.data.TimeData", // CoreModelModule - "org.javarosa.core.model.data.UncastData", // CoreModelModule - "org.javarosa.core.model.data.helper.BasicDataPointer", // CoreModelModule - "org.javarosa.core.model.actions.SetValueAction" //CoreModelModule + "org.javarosa.core.services.locale.ResourceFileDataSource", // JavaRosaCoreModule + "org.javarosa.core.services.locale.TableLocaleSource", // JavaRosaCoreModule + "org.javarosa.core.model.FormDef", + "org.javarosa.core.model.SubmissionProfile", // CoreModelModule + "org.javarosa.core.model.QuestionDef", // CoreModelModule + "org.javarosa.core.model.GroupDef", // CoreModelModule + "org.javarosa.core.model.instance.FormInstance", // CoreModelModule + "org.javarosa.core.model.data.BooleanData", // CoreModelModule + "org.javarosa.core.model.data.DateData", // CoreModelModule + "org.javarosa.core.model.data.DateTimeData", // CoreModelModule + "org.javarosa.core.model.data.DecimalData", // CoreModelModule + "org.javarosa.core.model.data.GeoPointData", // CoreModelModule + "org.javarosa.core.model.data.GeoShapeData", // CoreModelModule + "org.javarosa.core.model.data.GeoTraceData", // CoreModelModule + "org.javarosa.core.model.data.IntegerData", // CoreModelModule + "org.javarosa.core.model.data.LongData", // CoreModelModule + "org.javarosa.core.model.data.MultiPointerAnswerData", // CoreModelModule + "org.javarosa.core.model.data.PointerAnswerData", // CoreModelModule + "org.javarosa.core.model.data.SelectMultiData", // CoreModelModule + "org.javarosa.core.model.data.SelectOneData", // CoreModelModule + "org.javarosa.core.model.data.StringData", // CoreModelModule + "org.javarosa.core.model.data.TimeData", // CoreModelModule + "org.javarosa.core.model.data.UncastData", // CoreModelModule + "org.javarosa.core.model.data.helper.BasicDataPointer", // CoreModelModule + "org.javarosa.core.model.actions.SetValueAction" //CoreModelModule }; private JFrame validatorFrame; @@ -139,19 +139,15 @@ public static void main(String[] args) { } new FormValidator().validateAndExitWithErrorCode( - paths, failFast); + paths, failFast); } - } - catch (UnsupportedLookAndFeelException e) { + } catch (UnsupportedLookAndFeelException e) { e.printStackTrace(); - } - catch (ClassNotFoundException e) { + } catch (ClassNotFoundException e) { e.printStackTrace(); - } - catch (InstantiationException e) { + } catch (InstantiationException e) { e.printStackTrace(); - } - catch (IllegalAccessException e) { + } catch (IllegalAccessException e) { e.printStackTrace(); } @@ -162,7 +158,8 @@ private void setError(boolean outcome) { inError = outcome; } - public FormValidator() {} + public FormValidator() { + } private FormValidator show() { validatorFrame = new JFrame(BuildConfig.NAME + " " + BuildConfig.VERSION); @@ -204,7 +201,7 @@ public JTextAreaOutputStream(JTextArea textArea) { @Override public void write(int b) { - textArea.append(new String(new byte[] { + textArea.append(new String(new byte[]{ (byte) (b % 256) }, 0, 1)); } @@ -291,21 +288,21 @@ boolean stepThroughEntireForm(FormEntryModel model) throws InvalidReferenceExcep // step through every value in the form FormIndex idx = FormIndex.createBeginningOfFormIndex(); int event; - for (;;) { + for (; ; ) { idx = model.incrementIndex(idx); event = model.getEvent(idx); - if ( event == FormEntryController.EVENT_END_OF_FORM ) break; + if (event == FormEntryController.EVENT_END_OF_FORM) break; if (event == FormEntryController.EVENT_PROMPT_NEW_REPEAT) { String elementPath = idx.getReference().toString().replaceAll("\\[\\d+\\]", ""); - if ( !loops.contains(elementPath) ) { + if (!loops.contains(elementPath)) { loops.add(elementPath); model.getForm().createNewRepeat(idx); idx = model.getFormIndex(); } } else if (event == FormEntryController.EVENT_GROUP) { GroupDef gd = (GroupDef) model.getForm().getChild(idx); - if ( gd.getChildren() == null || gd.getChildren().size() == 0 ) { + if (gd.getChildren() == null || gd.getChildren().size() == 0) { outcome = true; setError(true); String elementPath = idx.getReference().toString().replaceAll("\\[\\d+\\]", ""); @@ -315,25 +312,25 @@ boolean stepThroughEntireForm(FormEntryModel model) throws InvalidReferenceExcep continue; } else { FormEntryPrompt prompt = model.getQuestionPrompt(idx); - if ( prompt.getControlType() == Constants.CONTROL_SELECT_MULTI || - prompt.getControlType() == Constants.CONTROL_SELECT_ONE ) { + if (prompt.getControlType() == Constants.CONTROL_SELECT_MULTI || + prompt.getControlType() == Constants.CONTROL_SELECT_ONE) { String elementPath = idx.getReference().toString().replaceAll("\\[\\d+\\]", ""); List items; items = prompt.getSelectChoices(); // check for null values... - for ( int i = 0 ; i < items.size() ; ++i ) { + for (int i = 0; i < items.size(); ++i) { SelectChoice s = items.get(i); String text = prompt.getSelectChoiceText(s); String image = prompt.getSpecialFormSelectChoiceText(s, - FormEntryCaption.TEXT_FORM_IMAGE); - if ((text == null || text.trim().length() == 0 ) && - (image == null || image.trim().length() == 0)) { - errors.error("Selection choice label text and image uri are both missing for: " + elementPath + " choice: " + (i+1) + ".\n"); + FormEntryCaption.TEXT_FORM_IMAGE); + if ((text == null || text.trim().length() == 0) && + (image == null || image.trim().length() == 0)) { + errors.error("Selection choice label text and image uri are both missing for: " + elementPath + " choice: " + (i + 1) + ".\n"); } - if ( s.getValue() == null || s.getValue().trim().length() == 0) { + if (s.getValue() == null || s.getValue().trim().length() == 0) { outcome = true; setError(true); - errors.error("Selection value is missing for: " + elementPath + " choice: " + (i+1) + ". The XML is invalid.\n"); + errors.error("Selection value is missing for: " + elementPath + " choice: " + (i + 1) + ". The XML is invalid.\n"); } } } @@ -353,12 +350,12 @@ public void validateAndExitWithErrorCode(List paths, boolean failFast) { } if (inError) { - if (failFast) { + if (failFast) { break; - } else { - failed.add(path); - setError(false); - } + } else { + failed.add(path); + setError(false); + } } } @@ -377,7 +374,7 @@ public void validateAndExitWithErrorCode(List paths, boolean failFast) { } } - public void validate(String path) { + public void validate(String path) { File src = new File(path); if (!src.exists()) { setError(true); @@ -423,102 +420,135 @@ public void validate(InputStream xmlSource) { } } - public void validate(byte[] xformData) { - // validate well formed xml - // errors.info("Checking form..."); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - try { - factory.newDocumentBuilder().parse(new ByteArrayInputStream(xformData)); - } catch (Exception e) { - setError(true); - errors.error("\n\n\n>> XML is invalid.",e); - return; - } + public void validate(FormDef fd) { + // need a list of classes that formdef uses + // unfortunately, the JR registerModule() functions do more than this. + // register just the classes that would have been registered by: + // new JavaRosaCoreModule().registerModule(); + // new CoreModelModule().registerModule(); + // replace with direct call to PrototypeManager + PrototypeManager.registerPrototypes(SERIALIABLE_CLASSES); + // initialize XForms module + new XFormsModule().registerModule(); - // need a list of classes that formdef uses - // unfortunately, the JR registerModule() functions do more than this. - // register just the classes that would have been registered by: - // new JavaRosaCoreModule().registerModule(); - // new CoreModelModule().registerModule(); - // replace with direct call to PrototypeManager - PrototypeManager.registerPrototypes(SERIALIABLE_CLASSES); - // initialize XForms module - new XFormsModule().registerModule(); + // needed to override rms property manager + org.javarosa.core.services.PropertyManager + .setPropertyManager(new StubPropertyManager()); - // needed to override rms property manager - org.javarosa.core.services.PropertyManager - .setPropertyManager(new StubPropertyManager()); + // For forms with external secondary instances + final ReferenceManager referenceManager = ReferenceManager.instance(); + referenceManager.addReferenceFactory(new StubReferenceFactory()); - // For forms with external secondary instances - final ReferenceManager referenceManager = ReferenceManager.instance(); - referenceManager.addReferenceFactory(new StubReferenceFactory()); + PrototypeManager.registerPrototype("org.opendatakit.validate.StubSetGeopointAction"); + XFormParser.registerActionHandler(StubSetGeopointActionHandler.ELEMENT_NAME, new StubSetGeopointActionHandler()); - PrototypeManager.registerPrototype("org.opendatakit.validate.StubSetGeopointAction"); - XFormParser.registerActionHandler(StubSetGeopointActionHandler.ELEMENT_NAME, new StubSetGeopointActionHandler()); - // validate if the xform can be parsed. - try { - FormDef fd = XFormUtils.getFormFromInputStream(new ByteArrayInputStream(xformData)); - if (fd == null) { - setError(true); - errors.error("\n\n\n>> Something broke the parser. Try again."); - return; - } + // make sure properties get loaded + fd.getPreloader().addPreloadHandler(new FakePreloadHandler("property")); - // make sure properties get loaded - fd.getPreloader().addPreloadHandler(new FakePreloadHandler("property")); + // update evaluation context for function handlers + fd.getEvaluationContext().addFunctionHandler(new IFunctionHandler() { - // update evaluation context for function handlers - fd.getEvaluationContext().addFunctionHandler(new IFunctionHandler() { + public String getName() { + return "pulldata"; + } - public String getName() { - return "pulldata"; - } + public List getPrototypes() { + return new ArrayList(); + } - public List getPrototypes() { - return new ArrayList(); - } + public boolean rawArgs() { + return true; + } - public boolean rawArgs() { - return true; - } + public boolean realTime() { + return false; + } - public boolean realTime() { - return false; - } + public Object eval(Object[] args, EvaluationContext ec) { + // no actual implementation here -- just a stub to facilitate validation + return args[0]; + } + }); - public Object eval(Object[] args, EvaluationContext ec) { - // no actual implementation here -- just a stub to facilitate validation - return args[0]; - }}); + fd.getEvaluationContext().addFunctionHandler(new IFunctionHandler() { - // check for runtime errors - fd.initialize(true, new InstanceInitializationFactory()); + public String getName() { + return "intersects"; + } - errors.info("\n\n>> Xform parsing completed!\n"); + public List getPrototypes() { + return new ArrayList(); + } - // create FormEntryController from formdef - FormEntryModel fem = new FormEntryModel(fd); + public boolean rawArgs() { + return true; + } - // and try to step through the form... - if ( stepThroughEntireForm(fem) ) { - setError(true); - errors.error("\n\n>> Xform is invalid!"); - } else { - errors.info("\n\n>> Xform is valid!"); - } + public boolean realTime() { + return false; + } - } catch (XFormParseException e) { - setError(true); - errors.error("\n\n>> XForm is invalid.",e); + public Object eval(Object[] args, EvaluationContext ec) { + // stub for validation + return args[0]; + } + }); - } catch (Exception e) { - setError(true); - errors.error("\n\n>> Something broke the parser.",e); + // check for runtime errors + try { + fd.initialize(true, new InstanceInitializationFactory()); + + errors.info("\n\n>> Xform parsing completed!\n"); + + // create FormEntryController from formdef + FormEntryModel fem = new FormEntryModel(fd); + // and try to step through the form... + if (stepThroughEntireForm(fem)) { + setError(true); + errors.error("\n\n>> Xform is invalid!"); + } else { + errors.info("\n\n>> Xform is valid!"); } + } catch (InvalidReferenceException e) { + setError(true); + errors.error("\n\n>> " + e.getMessage() + ": " + e.getInvalidReference()); + } catch (Exception e) { + setError(true); + errors.error("\n\n>> Xform is invalid!", e); + } + } + + public void validate(byte[] xformData) { + // validate well formed xml + // errors.info("Checking form..."); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + try { + factory.newDocumentBuilder().parse(new ByteArrayInputStream(xformData)); + } catch (Exception e) { + setError(true); + errors.error("\n\n\n>> XML is invalid.", e); + return; + } + // validate if the xform can be parsed. + try { + FormDef fd = XFormUtils.getFormFromInputStream(new ByteArrayInputStream(xformData)); + if (fd == null) { + setError(true); + errors.error("\n\n\n>> Something broke the parser. Try again."); + return; + } + validate(fd); + } catch (XFormParseException e) { + setError(true); + errors.error("\n\n>> XForm is invalid.", e); + } catch (Exception e) { + setError(true); + errors.error("\n\n>> Something broke the parser.", e); + } } private byte[] copyToByteArray(InputStream input) throws IOException { @@ -533,41 +563,41 @@ private byte[] copyToByteArray(InputStream input) throws IOException { return baos.toByteArray(); } - public FormValidator setErrorListener(ErrorListener listener){ - if(listener == null){ + public FormValidator setErrorListener(ErrorListener listener) { + if (listener == null) { throw new NullPointerException("Cannot set a null error listener"); } this.errors = listener; return this; } - private class FakePreloadHandler implements IPreloadHandler { +private class FakePreloadHandler implements IPreloadHandler { - String preloadHandled; + String preloadHandled; - public FakePreloadHandler(String preloadHandled) { - this.preloadHandled = preloadHandled; - } - + public FakePreloadHandler(String preloadHandled) { + this.preloadHandled = preloadHandled; + } - public boolean handlePostProcess(TreeElement arg0, String arg1) { - // TODO Auto-generated method stub - return false; - } + public boolean handlePostProcess(TreeElement arg0, String arg1) { + // TODO Auto-generated method stub + return false; + } - public IAnswerData handlePreload(String arg0) { - // TODO Auto-generated method stub - return null; - } + public IAnswerData handlePreload(String arg0) { + // TODO Auto-generated method stub + return null; + } - public String preloadHandled() { - // TODO Auto-generated method stub - return preloadHandled; - } + public String preloadHandled() { + // TODO Auto-generated method stub + return preloadHandled; } } + +} diff --git a/src/test/java/org/opendatakit/validate/CollectFunctionHandlersTest.java b/src/test/java/org/opendatakit/validate/CollectFunctionHandlersTest.java new file mode 100644 index 0000000..3fbc27d --- /dev/null +++ b/src/test/java/org/opendatakit/validate/CollectFunctionHandlersTest.java @@ -0,0 +1,103 @@ +package org.opendatakit.validate; +import org.javarosa.core.model.FormDef; +import org.javarosa.test.Scenario; + +import org.javarosa.xform.parse.XFormParser; +import org.junit.Test; + +import java.io.IOException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.isEmptyString; +import static org.javarosa.test.BindBuilderXFormsElement.bind; +import static org.javarosa.test.XFormsElement.body; +import static org.javarosa.test.XFormsElement.head; +import static org.javarosa.test.XFormsElement.html; +import static org.javarosa.test.XFormsElement.input; +import static org.javarosa.test.XFormsElement.mainInstance; +import static org.javarosa.test.XFormsElement.model; +import static org.javarosa.test.XFormsElement.t; +import static org.javarosa.test.XFormsElement.title; + +public class CollectFunctionHandlersTest { + + @Test + public void acceptsPulldata() throws IOException, XFormParser.ParseException { + FormDef fd = Scenario.createFormDef("Pulldata", html( + head( + title("Form with pulldata"), + model( + mainInstance(t("data id=\"pulldata\"", + t("q1"), + t("calc") + )), + // Below is an invalid pulldata call. The handler doesn't currently validate args but could. + bind("/data/calc").type("string").calculate("pulldata(/data/q1)") + ) + ), + body( + input("/data/q1") + ) + )); + + final FormValidator validator = new FormValidator(); + Output output = Output.runAndGet(() -> validator.validate(fd)); + + assertThat(output.getErr(), isEmptyString()); + assertThat(output.getStd(), containsString("Xform parsing completed")); + assertThat(output.getStd(), containsString("Xform is valid")); + } + + @Test + public void rejectsPuldata() throws IOException, XFormParser.ParseException { + FormDef fd = Scenario.createFormDef("Puldata with typo", html( + head( + title("Form with function name typo"), + model( + mainInstance(t("data id=\"puldata\"", + t("q1"), + t("calc") + )), + bind("/data/calc").type("string").calculate("puldata(/data/q1)") + ) + ), + body( + input("/data/q1") + ) + )); + + final FormValidator validator = new FormValidator(); + Output output = Output.runAndGet(() -> validator.validate(fd)); + + assertThat(output.getErr(), containsString("cannot handle function 'puldata'")); + assertThat(output.getErr(), containsString("Xform is invalid")); + assertThat(output.getStd(), isEmptyString()); + } + + @Test + public void acceptsIntersects() throws IOException, XFormParser.ParseException { + FormDef fd = Scenario.createFormDef("Intersects", html( + head( + title("Form with intersects"), + model( + mainInstance(t("data id=\"intersects\"", + t("q1"), + t("calc") + )), + bind("/data/calc").type("string").calculate("intersects(/data/q1)") + ) + ), + body( + input("/data/q1") + ) + )); + + final FormValidator validator = new FormValidator(); + Output output = Output.runAndGet(() -> validator.validate(fd)); + + assertThat(output.getErr(), isEmptyString()); + assertThat(output.getStd(), containsString("Xform parsing completed")); + assertThat(output.getStd(), containsString("Xform is valid")); + } +} diff --git a/src/test/java/org/opendatakit/validate/Output.java b/src/test/java/org/opendatakit/validate/Output.java new file mode 100644 index 0000000..7145ea9 --- /dev/null +++ b/src/test/java/org/opendatakit/validate/Output.java @@ -0,0 +1,48 @@ +package org.opendatakit.validate; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +class Output { + private final String std; + private final String err; + + Output(String std, String err) { + this.std = std; + this.err = err; + } + + static Output runAndGet(Runnable runnable) { + PrintStream outBackup = System.out; + ByteArrayOutputStream stdBaos = new ByteArrayOutputStream(); + PrintStream stdPs = new PrintStream(stdBaos); + System.setOut(stdPs); + + PrintStream errBackup = System.err; + ByteArrayOutputStream errBaos = new ByteArrayOutputStream(); + PrintStream errPs = new PrintStream(errBaos); + System.setErr(errPs); + + runnable.run(); + + stdPs.flush(); + String std = stdBaos.toString(); + System.setOut(outBackup); + System.out.print(std); + + errPs.flush(); + String err = errBaos.toString(); + System.setErr(errBackup); + System.err.print(err); + + return new Output(std, err); + } + + public String getStd() { + return std; + } + + public String getErr() { + return err; + } +} \ No newline at end of file diff --git a/src/test/java/org/opendatakit/validate/ValidateExternalSecondaryInstancesTest.java b/src/test/java/org/opendatakit/validate/ValidateExternalSecondaryInstancesTest.java index c19b09e..bff9230 100644 --- a/src/test/java/org/opendatakit/validate/ValidateExternalSecondaryInstancesTest.java +++ b/src/test/java/org/opendatakit/validate/ValidateExternalSecondaryInstancesTest.java @@ -4,8 +4,6 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.isEmptyString; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; @@ -18,8 +16,8 @@ public void supportsXlsformsDefaultValueAndLabelRefs_inItemsets_usingXMLExternal final FormValidator validator = new FormValidator(); Output output = Output.runAndGet(() -> validator.validate(path.toString())); - assertThat(output.err, isEmptyString()); - assertThat(output.std, containsString("Xform is valid")); + assertThat(output.getErr(), isEmptyString()); + assertThat(output.getStd(), containsString("Xform is valid")); } @Test @@ -28,8 +26,8 @@ public void supportsXlsformsDefaultValueAndLabelRefs_inItemsets_usingCsvSecondar final FormValidator validator = new FormValidator(); Output output = Output.runAndGet(() -> validator.validate(path.toString())); - assertThat(output.err, isEmptyString()); - assertThat(output.std, containsString("Xform is valid")); + assertThat(output.getErr(), isEmptyString()); + assertThat(output.getStd(), containsString("Xform is valid")); } @Test @@ -38,48 +36,11 @@ public void supportsCustomValueAndLabelRefs_inItemsets_usingExternalSecondaryIns final FormValidator validator = new FormValidator(); Output output = Output.runAndGet(() -> validator.validate(path.toString())); - assertThat(output.err, isEmptyString()); - assertThat(output.std, containsString("Xform is valid")); + assertThat(output.getErr(), isEmptyString()); + assertThat(output.getStd(), containsString("Xform is valid")); } private Path getPathOf(String filename) throws URISyntaxException { return Paths.get(ValidateExternalSecondaryInstancesTest.class.getResource(filename.startsWith("/") ? filename : "/" + filename).toURI()); } - - static class Output { - private final String std; - private final String err; - - Output(String std, String err) { - this.std = std; - this.err = err; - } - - static Output runAndGet(Runnable runnable) { - PrintStream outBackup = System.out; - ByteArrayOutputStream stdBaos = new ByteArrayOutputStream(); - PrintStream stdPs = new PrintStream(stdBaos); - System.setOut(stdPs); - - PrintStream errBackup = System.err; - ByteArrayOutputStream errBaos = new ByteArrayOutputStream(); - PrintStream errPs = new PrintStream(errBaos); - System.setErr(errPs); - - runnable.run(); - - stdPs.flush(); - String std = stdBaos.toString(); - System.setOut(outBackup); - System.out.print(std); - - errPs.flush(); - String err = errBaos.toString(); - System.setErr(errBackup); - System.err.print(err); - - return new Output(std, err); - } - } - }