diff --git a/src/main/java/org/javarosa/xform/parse/XFormParser.java b/src/main/java/org/javarosa/xform/parse/XFormParser.java index 32c0f29cf..24c611d24 100644 --- a/src/main/java/org/javarosa/xform/parse/XFormParser.java +++ b/src/main/java/org/javarosa/xform/parse/XFormParser.java @@ -59,6 +59,7 @@ import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.javarosa.xpath.XPathConditional; import org.javarosa.xpath.XPathParseTool; +import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.expr.XPathFuncExpr; import org.javarosa.xpath.expr.XPathNumericLiteral; import org.javarosa.xpath.expr.XPathPathExpr; @@ -87,6 +88,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableList; @@ -180,6 +183,9 @@ public class XFormParser implements IXFormParserFunctions { private final List formDefProcessors = new ArrayList<>(); private final List modelAttributeProcessors = new ArrayList<>(); private final List questionProcessors = new ArrayList<>(); + private final List xpathProcessors = new ArrayList<>(); + + public static final List tempXPathProcessors = new ArrayList<>(); /** * The string IDs of all instances that are referenced in a instance() function call in the primary instance @@ -197,6 +203,8 @@ public static IAnswerResolver getAnswerResolver() { return answerResolver; } + private static final Lock parseLock = new ReentrantLock(); + public static void setAnswerResolver(IAnswerResolver answerResolver) { XFormParser.answerResolver = answerResolver; } @@ -387,36 +395,47 @@ public FormDef parse() throws ParseException { * no data will be loaded and the instance will be blank. */ public FormDef parse(String formXmlSrc, String lastSavedSrc) throws ParseException { - if (_f == null) { - logger.info("Parsing form..."); - - if (_xmldoc == null) { - try { - _xmldoc = getXMLDocument(_reader, stringCache); - } catch (IOException e) { - throw new ParseException("IO Exception during parse! " + e.getMessage()); - } + try { + if (!parseLock.tryLock()) { + throw new IllegalStateException("Another XForm is being parsed!"); } - parseDoc(formXmlSrc, buildNamespacesMap(_xmldoc.getRootElement()), lastSavedSrc); + tempXPathProcessors.addAll(xpathProcessors); + + if (_f == null) { + logger.info("Parsing form..."); + + if (_xmldoc == null) { + try { + _xmldoc = getXMLDocument(_reader, stringCache); + } catch (IOException e) { + throw new ParseException("IO Exception during parse! " + e.getMessage()); + } + } + + parseDoc(formXmlSrc, buildNamespacesMap(_xmldoc.getRootElement()), lastSavedSrc); - //load in a custom xml instance, if applicable - if (_instReader != null) { - try { - loadXmlInstance(_f, _instReader); - } catch (IOException e) { - throw new ParseException("IO Exception during parse! " + e.getMessage()); + //load in a custom xml instance, if applicable + if (_instReader != null) { + try { + loadXmlInstance(_f, _instReader); + } catch (IOException e) { + throw new ParseException("IO Exception during parse! " + e.getMessage()); + } + } else if (_instDoc != null) { + loadXmlInstance(_f, _instDoc); } - } else if (_instDoc != null) { - loadXmlInstance(_f, _instDoc); } - } - for (FormDefProcessor formDefProcessor : formDefProcessors) { - formDefProcessor.processFormDef(_f); - } + for (FormDefProcessor formDefProcessor : formDefProcessors) { + formDefProcessor.processFormDef(_f); + } - return _f; + return _f; + } finally { + tempXPathProcessors.clear(); + parseLock.unlock(); + } } public void addProcessor(Processor processor) { @@ -435,6 +454,10 @@ public void addProcessor(Processor processor) { if (processor instanceof QuestionProcessor) { questionProcessors.add((QuestionProcessor) processor); } + + if (processor instanceof XPathProcessor) { + xpathProcessors.add((XPathProcessor) processor); + } } public void addBindAttributeProcessor(BindAttributeProcessor bindAttributeProcessor) { @@ -2438,6 +2461,10 @@ public interface Processor { } + public interface XPathProcessor extends Processor { + void processXPath(@NotNull XPathExpression xPathExpression); + } + public interface FormDefProcessor extends Processor { void processFormDef(FormDef formDef) throws ParseException; } diff --git a/src/main/java/org/javarosa/xpath/XPathParseTool.java b/src/main/java/org/javarosa/xpath/XPathParseTool.java index b3336303e..201c57823 100644 --- a/src/main/java/org/javarosa/xpath/XPathParseTool.java +++ b/src/main/java/org/javarosa/xpath/XPathParseTool.java @@ -16,6 +16,7 @@ package org.javarosa.xpath; +import org.javarosa.xform.parse.XFormParser; import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.Lexer; import org.javarosa.xpath.parser.Parser; @@ -38,6 +39,11 @@ public class XPathParseTool { }; public static XPathExpression parseXPath (String xpath) throws XPathSyntaxException { - return Parser.parse(Lexer.lex(xpath)); + XPathExpression expression = Parser.parse(Lexer.lex(xpath)); + for (XFormParser.XPathProcessor processor : XFormParser.tempXPathProcessors) { + processor.processXPath(expression); + } + + return expression; } } diff --git a/src/test/java/org/javarosa/xform/parse/XPathProcessorTest.java b/src/test/java/org/javarosa/xform/parse/XPathProcessorTest.java new file mode 100644 index 000000000..301dbea90 --- /dev/null +++ b/src/test/java/org/javarosa/xform/parse/XPathProcessorTest.java @@ -0,0 +1,102 @@ +package org.javarosa.xform.parse; + +import org.javarosa.core.util.XFormsElement; +import org.javarosa.xpath.expr.XPathExpression; +import org.javarosa.xpath.expr.XPathPathExpr; +import org.javarosa.xpath.expr.XPathQName; +import org.javarosa.xpath.expr.XPathStep; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.javarosa.core.util.BindBuilderXFormsElement.bind; +import static org.javarosa.core.util.XFormsElement.body; +import static org.javarosa.core.util.XFormsElement.head; +import static org.javarosa.core.util.XFormsElement.input; +import static org.javarosa.core.util.XFormsElement.mainInstance; +import static org.javarosa.core.util.XFormsElement.model; +import static org.javarosa.core.util.XFormsElement.t; + +public class XPathProcessorTest { + + @Test + public void processesXPathExpressions() throws Exception { + XFormsElement form = XFormsElement.html( + head( + model( + mainInstance( + t("data id=\"form\"", + t("question") + ) + ), + bind("/data/question").type("string") + ) + ), + body( + input("/data/question") + ) + ); + + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + RecordingXPathProcessor processor = new RecordingXPathProcessor(); + parser.addProcessor(processor); + parser.parse(null); + + assertThat(processor.processedExpressions, contains( + new XPathPathExpr(XPathPathExpr.INIT_CONTEXT_ROOT, new XPathStep[]{ + new XPathStep(XPathStep.AXIS_CHILD, new XPathQName("data")), + new XPathStep(XPathStep.AXIS_CHILD, new XPathQName("question")) + }), + new XPathPathExpr(XPathPathExpr.INIT_CONTEXT_ROOT, new XPathStep[]{ + new XPathStep(XPathStep.AXIS_CHILD, new XPathQName("data")), + new XPathStep(XPathStep.AXIS_CHILD, new XPathQName("question")) + }) + )); + } + + @Test + public void processorsAreNotRetainedBetweenParses() throws Exception { + XFormsElement form = XFormsElement.html( + head( + model( + mainInstance( + t("data id=\"form\"", + t("question") + ) + ), + bind("/data/question").type("string") + ) + ), + body( + input("/data/question") + ) + ); + + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + RecordingXPathProcessor processor = new RecordingXPathProcessor(); + parser.addProcessor(processor); + parser.parse(null); + assertThat(processor.processedExpressions.size(), equalTo(2)); + + XFormParser secondParser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + secondParser.parse(null); + assertThat(processor.processedExpressions.size(), equalTo(2)); + } + + private static class RecordingXPathProcessor implements XFormParser.XPathProcessor { + + public final List processedExpressions = new ArrayList<>(); + + @Override + public void processXPath(@NotNull XPathExpression xPathExpression) { + processedExpressions.add(xPathExpression); + } + } +}