From 6382157532ac3676bafd2c1ab7b0ab2d137f66a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 11:31:50 -0800 Subject: [PATCH 01/11] Move triggering nested actions out of FormDef --- src/main/java/org/javarosa/core/model/FormDef.java | 5 +---- .../javarosa/core/model/actions/ActionController.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/FormDef.java b/src/main/java/org/javarosa/core/model/FormDef.java index 55d5c9880..be9e39a54 100644 --- a/src/main/java/org/javarosa/core/model/FormDef.java +++ b/src/main/java/org/javarosa/core/model/FormDef.java @@ -1298,10 +1298,7 @@ public void initialize(boolean newInstance, InstanceInitializationFactory factor } if (newInstance) { - actionController.triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, this); - for (IFormElement element : elementsWithActionTriggeredByToplevelEvent) { - element.getActionController().triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, this, ((TreeReference) element.getBind().getReference()).getParentRef(), null); - } + actionController.triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, elementsWithActionTriggeredByToplevelEvent, this); // xforms-ready is marked as deprecated as of JavaRosa 2.14.0 but is still dispatched for compatibility with // old form definitions diff --git a/src/main/java/org/javarosa/core/model/actions/ActionController.java b/src/main/java/org/javarosa/core/model/actions/ActionController.java index 455262508..af1f4cd51 100644 --- a/src/main/java/org/javarosa/core/model/actions/ActionController.java +++ b/src/main/java/org/javarosa/core/model/actions/ActionController.java @@ -9,7 +9,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Set; import org.javarosa.core.model.FormDef; +import org.javarosa.core.model.IFormElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; @@ -78,6 +80,14 @@ public void triggerActionsFromEvent(String event, FormDef model) { triggerActionsFromEvent(event, model, null, null); } + public void triggerActionsFromEvent(String event, Set nestedElements, FormDef model) { + triggerActionsFromEvent(event, model, null, null); + + for (IFormElement element : nestedElements) { + element.getActionController().triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, model, ((TreeReference) element.getBind().getReference()).getParentRef(), null); + } + } + public void triggerActionsFromEvent(String event, FormDef model, TreeReference contextForAction, ActionResultProcessor resultProcessor) { for (Action action : getListenersForEvent(event)) { From 7baacdc23711afda32610d864e459e5a916445f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 11:33:23 -0800 Subject: [PATCH 02/11] Trigger actions in body for all top-level events We keep a single set of elements that have actions triggered by any top-level event because the elements' action controllers will take care of only triggering relevant actions and we don't expect enough different actions triggered by different events in a single form definition that this would be a performance problem. This is still much better than having to go through every single body element looking for nested actions. --- .../java/org/javarosa/core/model/FormDef.java | 4 ++-- .../javarosa/core/model/actions/Action.java | 24 +++++++++++++------ .../core/model/actions/ActionController.java | 4 ---- .../org/javarosa/xform/parse/XFormParser.java | 2 +- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/FormDef.java b/src/main/java/org/javarosa/core/model/FormDef.java index be9e39a54..ae06ef622 100644 --- a/src/main/java/org/javarosa/core/model/FormDef.java +++ b/src/main/java/org/javarosa/core/model/FormDef.java @@ -1133,7 +1133,7 @@ public void preloadInstance(TreeElement node) { } public boolean postProcessInstance() { - actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_REVALIDATE, this); + actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_REVALIDATE, elementsWithActionTriggeredByToplevelEvent, this); return postProcessInstance(mainInstance.getRoot()); } @@ -1302,7 +1302,7 @@ public void initialize(boolean newInstance, InstanceInitializationFactory factor // xforms-ready is marked as deprecated as of JavaRosa 2.14.0 but is still dispatched for compatibility with // old form definitions - actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_READY, this); + actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_READY, elementsWithActionTriggeredByToplevelEvent, this); } Collection qts = initializeTriggerables(TreeReference.rootRef()); diff --git a/src/main/java/org/javarosa/core/model/actions/Action.java b/src/main/java/org/javarosa/core/model/actions/Action.java index 3baadd412..54284f9de 100644 --- a/src/main/java/org/javarosa/core/model/actions/Action.java +++ b/src/main/java/org/javarosa/core/model/actions/Action.java @@ -3,6 +3,9 @@ */ package org.javarosa.core.model.actions; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.util.externalizable.DeserializationException; @@ -10,10 +13,6 @@ import org.javarosa.core.util.externalizable.Externalizable; import org.javarosa.core.util.externalizable.PrototypeFactory; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - /** * @author ctsims * @@ -44,9 +43,11 @@ public abstract class Action implements Externalizable { public static final String EVENT_QUESTION_VALUE_CHANGED = "xforms-value-changed"; - private static final String[] allEvents = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_XFORMS_READY, + private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_XFORMS_READY, EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE}; + private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_XFORMS_READY, EVENT_XFORMS_REVALIDATE}; + private String name; public Action() { @@ -81,8 +82,17 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, name); } - public static boolean isValidEvent(String actionEventAttribute) { - for (String event : allEvents) { + public static boolean isValidEvent(String actionEventAttribute) { + for (String event : ALL_EVENTS) { + if (event.equals(actionEventAttribute)) { + return true; + } + } + return false; + } + + public static boolean isTopLevelEvent(String actionEventAttribute) { + for (String event : TOP_LEVEL_EVENTS) { if (event.equals(actionEventAttribute)) { return true; } diff --git a/src/main/java/org/javarosa/core/model/actions/ActionController.java b/src/main/java/org/javarosa/core/model/actions/ActionController.java index af1f4cd51..ae340b627 100644 --- a/src/main/java/org/javarosa/core/model/actions/ActionController.java +++ b/src/main/java/org/javarosa/core/model/actions/ActionController.java @@ -76,10 +76,6 @@ public void registerEventListener(List eventList, Action action) { } } - public void triggerActionsFromEvent(String event, FormDef model) { - triggerActionsFromEvent(event, model, null, null); - } - public void triggerActionsFromEvent(String event, Set nestedElements, FormDef model) { triggerActionsFromEvent(event, model, null, null); diff --git a/src/main/java/org/javarosa/xform/parse/XFormParser.java b/src/main/java/org/javarosa/xform/parse/XFormParser.java index f016c495f..cc541a4d0 100644 --- a/src/main/java/org/javarosa/xform/parse/XFormParser.java +++ b/src/main/java/org/javarosa/xform/parse/XFormParser.java @@ -730,7 +730,7 @@ private void parseAction(Element e, Object parent, IElementHandler specificHandl "Must be either a child of a control element, or a child of the "); } - if (!parent.equals(_f) && event.equals(Action.EVENT_ODK_INSTANCE_FIRST_LOAD)) { + if (!parent.equals(_f) && Action.isTopLevelEvent(event)) { _f.registerElementWithActionTriggeredByToplevelEvent((IFormElement) parent); } } From 2fcd02522dfa8f66507b1440168d8cfb5a276400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 12:09:15 -0800 Subject: [PATCH 03/11] Add odk-instance-load event --- .../java/org/javarosa/core/model/FormDef.java | 2 + .../javarosa/core/model/actions/Action.java | 10 ++- .../model/actions/InstanceLoadEventsTest.java | 88 +++++++++++++++++++ .../java/org/javarosa/core/test/Scenario.java | 1 + 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java diff --git a/src/main/java/org/javarosa/core/model/FormDef.java b/src/main/java/org/javarosa/core/model/FormDef.java index ae06ef622..524eb2087 100644 --- a/src/main/java/org/javarosa/core/model/FormDef.java +++ b/src/main/java/org/javarosa/core/model/FormDef.java @@ -1305,6 +1305,8 @@ public void initialize(boolean newInstance, InstanceInitializationFactory factor actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_READY, elementsWithActionTriggeredByToplevelEvent, this); } + actionController.triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_LOAD, elementsWithActionTriggeredByToplevelEvent, this); + Collection qts = initializeTriggerables(TreeReference.rootRef()); dagImpl.publishSummary("Form initialized", null, qts); } diff --git a/src/main/java/org/javarosa/core/model/actions/Action.java b/src/main/java/org/javarosa/core/model/actions/Action.java index 54284f9de..f9a9fef6e 100644 --- a/src/main/java/org/javarosa/core/model/actions/Action.java +++ b/src/main/java/org/javarosa/core/model/actions/Action.java @@ -25,6 +25,11 @@ public abstract class Action implements Externalizable { */ public static final String EVENT_ODK_INSTANCE_FIRST_LOAD = "odk-instance-first-load"; + /** + * Dispatched any time a form instance is loaded. + */ + public static final String EVENT_ODK_INSTANCE_LOAD = "odk-instance-load"; + /** * @deprecated because as W3C XForms defines it, it should be dispatched any time the XForms engine is ready. In * JavaRosa, it was dispatched only on first load of a form instance. Use @@ -43,10 +48,11 @@ public abstract class Action implements Externalizable { public static final String EVENT_QUESTION_VALUE_CHANGED = "xforms-value-changed"; - private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_XFORMS_READY, + private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE}; - private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_XFORMS_READY, EVENT_XFORMS_REVALIDATE}; + private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, + EVENT_XFORMS_REVALIDATE}; private String name; diff --git a/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java b/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java new file mode 100644 index 000000000..3aa95e13a --- /dev/null +++ b/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java @@ -0,0 +1,88 @@ +package org.javarosa.core.model.actions; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.javarosa.core.test.AnswerDataMatchers.intAnswer; +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.html; +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.setvalue; +import static org.javarosa.core.util.XFormsElement.t; +import static org.javarosa.core.util.XFormsElement.title; + +import org.javarosa.core.test.Scenario; +import org.junit.Test; + +public class InstanceLoadEventsTest { + @Test + public void instanceLoadEvent_firesOnFirstLoad() throws Exception { + Scenario scenario = Scenario.init("Instance load form", html( + head( + title("Instance load form"), + model( + mainInstance( + t("data id=\"instance-load-form\"", + t("q1") + )), + bind("/data/q1").type("int"), + setvalue("odk-instance-load", "/data/q1", "4*4"))), + body( + input("/data/q1") + ) + )); + + assertThat(scenario.answerOf("/data/q1"), is(intAnswer(16))); + } + + @Test + public void instanceLoadEvent_firesOnSecondLoad() throws Exception { + Scenario scenario = Scenario.init("Instance load form", html( + head( + title("Instance load form"), + model( + mainInstance( + t("data id=\"instance-load-form\"", + t("q1") + )), + bind("/data/q1").type("int"), + setvalue("odk-instance-load", "/data/q1", "4*4"))), + body( + input("/data/q1") + ) + )); + + assertThat(scenario.answerOf("/data/q1"), is(intAnswer(16))); + scenario.answer("/data/q1", 555); + + Scenario restored = scenario.serializeAndDeserializeForm(); + assertThat(restored.answerOf("/data/q1"), is(intAnswer(16))); + } + + @Test + public void instanceFirstLoadEvent_doesNotfireOnSecondLoad() throws Exception { + Scenario scenario = Scenario.init("Instance load form", html( + head( + title("Instance load form"), + model( + mainInstance( + t("data id=\"instance-load-form\"", + t("q1") + )), + bind("/data/q1").type("int"), + setvalue("odk-instance-first-load", "/data/q1", "4*4"))), + body( + input("/data/q1") + ) + )); + + assertThat(scenario.answerOf("/data/q1"), is(intAnswer(16))); + scenario.answer("/data/q1", 555); + + Scenario restored = scenario.serializeAndDeserializeForm(); + assertThat(restored.answerOf("/data/q1"), is(intAnswer(555))); + } +} diff --git a/src/test/java/org/javarosa/core/test/Scenario.java b/src/test/java/org/javarosa/core/test/Scenario.java index 362f13baf..d8c254faf 100644 --- a/src/test/java/org/javarosa/core/test/Scenario.java +++ b/src/test/java/org/javarosa/core/test/Scenario.java @@ -273,6 +273,7 @@ public Scenario serializeAndDeserializeForm() throws IOException, Deserializatio ); delete(tempFile); + deserializedFormDef.initialize(false, new InstanceInitializationFactory()); return Scenario.from(deserializedFormDef); } From e916be80f264810b4c82ad0a56e1caca2eea7c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 16:01:55 -0800 Subject: [PATCH 04/11] Move static behavior out of Action --- .../java/org/javarosa/core/model/FormDef.java | 20 +++---- .../javarosa/core/model/actions/Action.java | 56 +------------------ .../core/model/actions/ActionController.java | 2 +- .../javarosa/core/model/actions/Actions.java | 51 +++++++++++++++++ .../org/javarosa/xform/parse/XFormParser.java | 9 +-- .../javarosa/xform/parse/XFormParserTest.java | 23 ++++---- 6 files changed, 79 insertions(+), 82 deletions(-) create mode 100644 src/main/java/org/javarosa/core/model/actions/Actions.java diff --git a/src/main/java/org/javarosa/core/model/FormDef.java b/src/main/java/org/javarosa/core/model/FormDef.java index 524eb2087..098e5b3f2 100644 --- a/src/main/java/org/javarosa/core/model/FormDef.java +++ b/src/main/java/org/javarosa/core/model/FormDef.java @@ -31,8 +31,8 @@ import java.util.Set; import org.javarosa.core.log.WrappedException; import org.javarosa.core.model.TriggerableDag.EventNotifierAccessor; -import org.javarosa.core.model.actions.Action; import org.javarosa.core.model.actions.ActionController; +import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.condition.Constraint; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.condition.IConditionExpr; @@ -397,7 +397,7 @@ public void setValue(IAnswerData data, TreeReference ref, TreeElement node, QuestionDef currentQuestion = findQuestionByRef(ref, this); if (valueChanged && currentQuestion != null) { - currentQuestion.getActionController().triggerActionsFromEvent(Action.EVENT_QUESTION_VALUE_CHANGED, this, + currentQuestion.getActionController().triggerActionsFromEvent(Actions.EVENT_QUESTION_VALUE_CHANGED, this, ref.getParentRef(), null); } @@ -515,17 +515,17 @@ public void createNewRepeat(FormIndex index) throws InvalidReferenceException { // Fire events before form re-computation (calculates, relevance, etc). First trigger actions defined in the // model and then trigger actions defined in the body - actionController.triggerActionsFromEvent(Action.EVENT_JR_INSERT, this, repeatContextRef, this); - actionController.triggerActionsFromEvent(Action.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this); + actionController.triggerActionsFromEvent(Actions.EVENT_JR_INSERT, this, repeatContextRef, this); + actionController.triggerActionsFromEvent(Actions.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this); // Trigger actions nested in the new repeat - getChild(index).getActionController().triggerActionsFromEvent(Action.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this); + getChild(index).getActionController().triggerActionsFromEvent(Actions.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this); dagImpl.createRepeatInstance(getMainInstance(), getEvaluationContext(), repeatContextRef, newNode); } @Override public void processResultOfAction(TreeReference refSetByAction, String event) { - if (Action.EVENT_JR_INSERT.equals(event)) { + if (Actions.EVENT_JR_INSERT.equals(event)) { // CommCare has an implementation if needed } } @@ -1133,7 +1133,7 @@ public void preloadInstance(TreeElement node) { } public boolean postProcessInstance() { - actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_REVALIDATE, elementsWithActionTriggeredByToplevelEvent, this); + actionController.triggerActionsFromEvent(Actions.EVENT_XFORMS_REVALIDATE, elementsWithActionTriggeredByToplevelEvent, this); return postProcessInstance(mainInstance.getRoot()); } @@ -1298,14 +1298,14 @@ public void initialize(boolean newInstance, InstanceInitializationFactory factor } if (newInstance) { - actionController.triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, elementsWithActionTriggeredByToplevelEvent, this); + actionController.triggerActionsFromEvent(Actions.EVENT_ODK_INSTANCE_FIRST_LOAD, elementsWithActionTriggeredByToplevelEvent, this); // xforms-ready is marked as deprecated as of JavaRosa 2.14.0 but is still dispatched for compatibility with // old form definitions - actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_READY, elementsWithActionTriggeredByToplevelEvent, this); + actionController.triggerActionsFromEvent(Actions.EVENT_XFORMS_READY, elementsWithActionTriggeredByToplevelEvent, this); } - actionController.triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_LOAD, elementsWithActionTriggeredByToplevelEvent, this); + actionController.triggerActionsFromEvent(Actions.EVENT_ODK_INSTANCE_LOAD, elementsWithActionTriggeredByToplevelEvent, this); Collection qts = initializeTriggerables(TreeReference.rootRef()); dagImpl.publishSummary("Form initialized", null, qts); diff --git a/src/main/java/org/javarosa/core/model/actions/Action.java b/src/main/java/org/javarosa/core/model/actions/Action.java index f9a9fef6e..59ff40cc5 100644 --- a/src/main/java/org/javarosa/core/model/actions/Action.java +++ b/src/main/java/org/javarosa/core/model/actions/Action.java @@ -18,46 +18,10 @@ * */ public abstract class Action implements Externalizable { - // Events that can trigger an action - - /** - * Dispatched the first time a form instance is loaded. - */ - public static final String EVENT_ODK_INSTANCE_FIRST_LOAD = "odk-instance-first-load"; - - /** - * Dispatched any time a form instance is loaded. - */ - public static final String EVENT_ODK_INSTANCE_LOAD = "odk-instance-load"; - - /** - * @deprecated because as W3C XForms defines it, it should be dispatched any time the XForms engine is ready. In - * JavaRosa, it was dispatched only on first load of a form instance. Use - * {@link #EVENT_ODK_INSTANCE_FIRST_LOAD} instead. - */ - @Deprecated - public static final String EVENT_XFORMS_READY = "xforms-ready"; - public static final String EVENT_XFORMS_REVALIDATE = "xforms-revalidate"; - - public static final String EVENT_ODK_NEW_REPEAT = "odk-new-repeat"; - - /** - * @deprecated because it was never documented. Use {@link #EVENT_ODK_NEW_REPEAT} instead. - */ - public static final String EVENT_JR_INSERT = "jr-insert"; - - public static final String EVENT_QUESTION_VALUE_CHANGED = "xforms-value-changed"; - - private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, - EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE}; - - private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, - EVENT_XFORMS_REVALIDATE}; - private String name; public Action() { - + // for serialization } public Action(String name) { @@ -87,22 +51,4 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, name); } - - public static boolean isValidEvent(String actionEventAttribute) { - for (String event : ALL_EVENTS) { - if (event.equals(actionEventAttribute)) { - return true; - } - } - return false; - } - - public static boolean isTopLevelEvent(String actionEventAttribute) { - for (String event : TOP_LEVEL_EVENTS) { - if (event.equals(actionEventAttribute)) { - return true; - } - } - return false; - } } diff --git a/src/main/java/org/javarosa/core/model/actions/ActionController.java b/src/main/java/org/javarosa/core/model/actions/ActionController.java index ae340b627..5a9f14f31 100644 --- a/src/main/java/org/javarosa/core/model/actions/ActionController.java +++ b/src/main/java/org/javarosa/core/model/actions/ActionController.java @@ -80,7 +80,7 @@ public void triggerActionsFromEvent(String event, Set nestedElemen triggerActionsFromEvent(event, model, null, null); for (IFormElement element : nestedElements) { - element.getActionController().triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, model, ((TreeReference) element.getBind().getReference()).getParentRef(), null); + element.getActionController().triggerActionsFromEvent(Actions.EVENT_ODK_INSTANCE_FIRST_LOAD, model, ((TreeReference) element.getBind().getReference()).getParentRef(), null); } } diff --git a/src/main/java/org/javarosa/core/model/actions/Actions.java b/src/main/java/org/javarosa/core/model/actions/Actions.java new file mode 100644 index 000000000..b88efe641 --- /dev/null +++ b/src/main/java/org/javarosa/core/model/actions/Actions.java @@ -0,0 +1,51 @@ +package org.javarosa.core.model.actions; + +import java.util.Arrays; + +public class Actions { + private Actions() { } + + //region Event names + /** + * Dispatched the first time a form instance is loaded. + */ + public static final String EVENT_ODK_INSTANCE_FIRST_LOAD = "odk-instance-first-load"; + + /** + * Dispatched any time a form instance is loaded. + */ + public static final String EVENT_ODK_INSTANCE_LOAD = "odk-instance-load"; + + /** + * @deprecated because as W3C XForms defines it, it should be dispatched any time the XForms engine is ready. In + * JavaRosa, it was dispatched only on first load of a form instance. Use + * {@link #EVENT_ODK_INSTANCE_FIRST_LOAD} instead. + */ + @Deprecated + public static final String EVENT_XFORMS_READY = "xforms-ready"; + public static final String EVENT_XFORMS_REVALIDATE = "xforms-revalidate"; + + public static final String EVENT_ODK_NEW_REPEAT = "odk-new-repeat"; + + /** + * @deprecated because it was never documented. Use {@link #EVENT_ODK_NEW_REPEAT} instead. + */ + public static final String EVENT_JR_INSERT = "jr-insert"; + + public static final String EVENT_QUESTION_VALUE_CHANGED = "xforms-value-changed"; + //endregion + + private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, + EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE}; + + private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, + EVENT_XFORMS_REVALIDATE}; + + public static boolean isValidEvent(String actionEventAttribute) { + return Arrays.asList(ALL_EVENTS).contains(actionEventAttribute); + } + + public static boolean isTopLevelEvent(String actionEventAttribute) { + return Arrays.asList(TOP_LEVEL_EVENTS).contains(actionEventAttribute); + } +} diff --git a/src/main/java/org/javarosa/xform/parse/XFormParser.java b/src/main/java/org/javarosa/xform/parse/XFormParser.java index cc541a4d0..93695bfe9 100644 --- a/src/main/java/org/javarosa/xform/parse/XFormParser.java +++ b/src/main/java/org/javarosa/xform/parse/XFormParser.java @@ -70,6 +70,7 @@ import org.javarosa.core.model.SubmissionProfile; import org.javarosa.core.model.actions.Action; import org.javarosa.core.model.actions.ActionController; +import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.actions.SetValueAction; import org.javarosa.core.model.actions.setgeopoint.SetGeopointActionHandler; import org.javarosa.core.model.actions.setgeopoint.StubSetGeopointActionHandler; @@ -704,8 +705,8 @@ private void parseModel(Element e) { parseSubmission(child); } else { // For now, anything that isn't a submission is an action - if (actionHandlers.containsKey(name) && child.getAttributeValue(null, EVENT_ATTR).equals(Action.EVENT_ODK_NEW_REPEAT)) { - throw new XFormParseException("Actions triggered by " + Action.EVENT_ODK_NEW_REPEAT + " must be nested in the repeat form control.", child); + if (actionHandlers.containsKey(name) && child.getAttributeValue(null, EVENT_ATTR).equals(Actions.EVENT_ODK_NEW_REPEAT)) { + throw new XFormParseException("Actions triggered by " + Actions.EVENT_ODK_NEW_REPEAT + " must be nested in the repeat form control.", child); } else { actionHandlers.get(name).handle(this, child, _f); } @@ -730,7 +731,7 @@ private void parseAction(Element e, Object parent, IElementHandler specificHandl "Must be either a child of a control element, or a child of the "); } - if (!parent.equals(_f) && Action.isTopLevelEvent(event)) { + if (!parent.equals(_f) && Actions.isTopLevelEvent(event)) { _f.registerElementWithActionTriggeredByToplevelEvent((IFormElement) parent); } } @@ -744,7 +745,7 @@ public static List getValidEventNames(String eventsString) { List validEvents = new ArrayList<>(); List invalidEventList = new ArrayList<>(); for (String event : eventsString.split(" ")) - if (Action.isValidEvent(event)) + if (Actions.isValidEvent(event)) validEvents.add(event); else invalidEventList.add(event); diff --git a/src/test/java/org/javarosa/xform/parse/XFormParserTest.java b/src/test/java/org/javarosa/xform/parse/XFormParserTest.java index 90b5e046f..41af924a9 100644 --- a/src/test/java/org/javarosa/xform/parse/XFormParserTest.java +++ b/src/test/java/org/javarosa/xform/parse/XFormParserTest.java @@ -9,19 +9,18 @@ import static org.javarosa.core.model.Constants.CONTROL_RANGE; import static org.javarosa.core.model.Constants.CONTROL_RANK; import static org.javarosa.core.test.AnswerDataMatchers.intAnswer; -import static org.javarosa.test.utils.ResourcePathHelper.r; -import static org.javarosa.xform.parse.FormParserHelper.deserializeAndCleanUpSerializedForm; -import static org.javarosa.xform.parse.FormParserHelper.getSerializedFormPath; -import static org.javarosa.xform.parse.FormParserHelper.parse; -import static org.javarosa.core.util.XFormsElement.t; +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.html; import static org.javarosa.core.util.XFormsElement.input; import static org.javarosa.core.util.XFormsElement.mainInstance; -import static org.javarosa.core.util.XFormsElement.body; import static org.javarosa.core.util.XFormsElement.model; -import static org.javarosa.core.util.XFormsElement.head; -import static org.javarosa.core.util.BindBuilderXFormsElement.bind; - +import static org.javarosa.core.util.XFormsElement.t; +import static org.javarosa.test.utils.ResourcePathHelper.r; +import static org.javarosa.xform.parse.FormParserHelper.deserializeAndCleanUpSerializedForm; +import static org.javarosa.xform.parse.FormParserHelper.getSerializedFormPath; +import static org.javarosa.xform.parse.FormParserHelper.parse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -38,7 +37,7 @@ import org.javarosa.core.model.QuestionDef; import org.javarosa.core.model.RangeQuestion; import org.javarosa.core.model.SubmissionProfile; -import org.javarosa.core.model.actions.Action; +import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.data.StringData; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.DataInstance; @@ -385,13 +384,13 @@ public void parseFormWithSetValueAction() throws IOException { // Given & When FormDef formDef = parse(r("form-with-setvalue-action.xml")); - // dispatch 'odk-instance-first-load' event (Action.EVENT_ODK_INSTANCE_FIRST_LOAD) + // dispatch 'odk-instance-first-load' event (Actions.EVENT_ODK_INSTANCE_FIRST_LOAD) formDef.initialize(true, new InstanceInitializationFactory()); // Then assertEquals(formDef.getTitle(), "SetValue action"); assertNoParseErrors(formDef); - assertEquals(1, formDef.getActionController().getListenersForEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD).size()); + assertEquals(1, formDef.getActionController().getListenersForEvent(Actions.EVENT_ODK_INSTANCE_FIRST_LOAD).size()); TreeElement textNode = formDef.getMainInstance().getRoot().getChildrenWithName("text").get(0); From 36d1cc07b67f175d8762bf4517923a6eb768c04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 16:26:33 -0800 Subject: [PATCH 05/11] Handle odk:recordaudio action on form parse --- .../recordaudio/RecordAudioAction.java | 25 ++++++++++ .../recordaudio/RecordAudioActionHandler.java | 50 +++++++++++++++++++ .../org/javarosa/xform/parse/XFormParser.java | 4 ++ .../model/actions/RecordAudioActionTest.java | 38 ++++++++++++++ .../org/javarosa/core/util/XFormsElement.java | 3 +- 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java create mode 100644 src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java create mode 100644 src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java new file mode 100644 index 000000000..1a8d3c624 --- /dev/null +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java @@ -0,0 +1,25 @@ +package org.javarosa.core.model.actions.recordaudio; + +import org.javarosa.core.model.FormDef; +import org.javarosa.core.model.actions.Action; +import org.javarosa.core.model.instance.TreeReference; + +public class RecordAudioAction extends Action { + private TreeReference targetReference; + + public RecordAudioAction(TreeReference targetReference) { + this.targetReference = targetReference; + } + + public RecordAudioAction() { + // empty body for serialization + } + + @Override + public TreeReference processAction(FormDef model, TreeReference contextRef) { + TreeReference contextualizedTargetReference = contextRef == null ? this.targetReference + : this.targetReference.contextualize(contextRef); + + return contextualizedTargetReference; + } +} diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java new file mode 100644 index 000000000..286e090de --- /dev/null +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java @@ -0,0 +1,50 @@ +package org.javarosa.core.model.actions.recordaudio; + +import static org.javarosa.xform.parse.XFormParser.EVENT_ATTR; +import static org.javarosa.xform.parse.XFormParser.getValidEventNames; + +import java.util.List; +import org.javarosa.core.model.FormDef; +import org.javarosa.core.model.IDataReference; +import org.javarosa.core.model.IFormElement; +import org.javarosa.core.model.actions.Actions; +import org.javarosa.core.model.instance.FormInstance; +import org.javarosa.core.model.instance.TreeReference; +import org.javarosa.model.xform.XPathReference; +import org.javarosa.xform.parse.IElementHandler; +import org.javarosa.xform.parse.XFormParseException; +import org.javarosa.xform.parse.XFormParser; +import org.kxml2.kdom.Element; + +public class RecordAudioActionHandler implements IElementHandler { + public static final String ELEMENT_NAME = "recordaudio"; + + @Override + public void handle(XFormParser p, Element e, Object parent) { + if (!e.getNamespace().equals(XFormParser.NAMESPACE_ODK)) { + throw new XFormParseException("recordaudio action must be in http://www.opendatakit.org/xforms namespace"); + } + + String ref = e.getAttributeValue(null, "ref"); + + if (ref == null) { + throw new XFormParseException("odk:recordaudio action must specify a ref"); + } + + IDataReference dataRef = FormDef.getAbsRef(new XPathReference(ref), TreeReference.rootRef()); + TreeReference target = FormInstance.unpackReference(dataRef); + p.registerActionTarget(target); + + List validEventNames = getValidEventNames(e.getAttributeValue(null, EVENT_ATTR)); + for (String eventName : validEventNames) { + if (!Actions.isTopLevelEvent(eventName)) { + throw new XFormParseException("odk:recordaudio action may only be triggered by top-level events (e.g. odk-instance-load)"); + } + } + + RecordAudioAction action = new RecordAudioAction(target); + + // XFormParser.parseAction already ensures parent is an IFormElement so we can safely cast + ((IFormElement) parent).getActionController().registerEventListener(validEventNames, action); + } +} diff --git a/src/main/java/org/javarosa/xform/parse/XFormParser.java b/src/main/java/org/javarosa/xform/parse/XFormParser.java index 93695bfe9..8e27d6ac4 100644 --- a/src/main/java/org/javarosa/xform/parse/XFormParser.java +++ b/src/main/java/org/javarosa/xform/parse/XFormParser.java @@ -72,6 +72,7 @@ import org.javarosa.core.model.actions.ActionController; import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.actions.SetValueAction; +import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; import org.javarosa.core.model.actions.setgeopoint.SetGeopointActionHandler; import org.javarosa.core.model.actions.setgeopoint.StubSetGeopointActionHandler; import org.javarosa.core.model.instance.AbstractTreeElement; @@ -320,6 +321,9 @@ private static void setUpActionHandlers() { // Register a stub odk:setgeopoint action handler. Clients that want to actually collect location need to // register their own subclass handler which will replace this one. registerActionHandler(SetGeopointActionHandler.ELEMENT_NAME, new StubSetGeopointActionHandler()); + + // Clients that want to record audio must register an action listener with Actions.registerActionListener + registerActionHandler(RecordAudioActionHandler.ELEMENT_NAME, new RecordAudioActionHandler()); } private void initState() { diff --git a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java new file mode 100644 index 000000000..c9254cd77 --- /dev/null +++ b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java @@ -0,0 +1,38 @@ +package org.javarosa.core.model.actions; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.javarosa.core.util.XFormsElement.body; +import static org.javarosa.core.util.XFormsElement.head; +import static org.javarosa.core.util.XFormsElement.html; +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; +import static org.javarosa.core.util.XFormsElement.title; + +import java.io.IOException; +import org.javarosa.core.test.Scenario; +import org.junit.Test; + +public class RecordAudioActionTest { + @Test + public void recordAudioAction_isProcessedOnFormParse() throws IOException { + Scenario scenario = Scenario.init("Record audio form", html( + head( + title("Record audio form"), + model( + mainInstance( + t("data id=\"record-audio-form\"", + t("recording"), + t("q1") + )), + t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\""))), + body( + input("/data/q1") + ) + )); + + assertThat(scenario.getFormDef().hasAction("recordaudio"), is(true)); + } +} diff --git a/src/test/java/org/javarosa/core/util/XFormsElement.java b/src/test/java/org/javarosa/core/util/XFormsElement.java index bfa140682..5e9208b9b 100644 --- a/src/test/java/org/javarosa/core/util/XFormsElement.java +++ b/src/test/java/org/javarosa/core/util/XFormsElement.java @@ -66,7 +66,8 @@ static XFormsElement html(XFormsElement... children) { return t("h:html " + "xmlns=\"http://www.w3.org/2002/xforms\" " + "xmlns:h=\"http://www.w3.org/1999/xhtml\" " + - "xmlns:jr=\"http://openrosa.org/javarosa\"", + "xmlns:jr=\"http://openrosa.org/javarosa\" " + + "xmlns:odk=\"http://www.opendatakit.org/xforms\"", children ); } From 233cb1c9bb5f4774e54ab3caeeae5c05fe232859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 17:10:46 -0800 Subject: [PATCH 06/11] Add support for client action listeners --- .../javarosa/core/model/actions/Actions.java | 28 +++++++++++++++++++ .../recordaudio/RecordAudioAction.java | 6 ++++ .../recordaudio/XFormsActionListener.java | 7 +++++ .../CapturingXFormsActionListener.java | 23 +++++++++++++++ .../model/actions/RecordAudioActionTest.java | 28 +++++++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java create mode 100644 src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java diff --git a/src/main/java/org/javarosa/core/model/actions/Actions.java b/src/main/java/org/javarosa/core/model/actions/Actions.java index b88efe641..381bfb2b1 100644 --- a/src/main/java/org/javarosa/core/model/actions/Actions.java +++ b/src/main/java/org/javarosa/core/model/actions/Actions.java @@ -1,6 +1,10 @@ package org.javarosa.core.model.actions; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; +import org.javarosa.core.model.actions.recordaudio.XFormsActionListener; public class Actions { private Actions() { } @@ -41,6 +45,12 @@ private Actions() { } private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, EVENT_XFORMS_REVALIDATE}; + /** + * Global registry of client classes that want to get updates about triggered actions. Addresses the need for some + * actions to be handled entirely client-side. + */ + private static Map actionListeners = new HashMap<>(); + public static boolean isValidEvent(String actionEventAttribute) { return Arrays.asList(ALL_EVENTS).contains(actionEventAttribute); } @@ -48,4 +58,22 @@ public static boolean isValidEvent(String actionEventAttribute) { public static boolean isTopLevelEvent(String actionEventAttribute) { return Arrays.asList(TOP_LEVEL_EVENTS).contains(actionEventAttribute); } + + + public static void registerActionListener(String actionName, XFormsActionListener listener) { + if (!actionName.equals(RecordAudioActionHandler.ELEMENT_NAME)){ + throw new IllegalArgumentException("Currently, only the recordaudio action notifies listeners"); + } + + actionListeners.put(actionName, listener); + } + + public static void unregisterActionListener(String actionName) { + actionListeners.remove(actionName); + } + + public static XFormsActionListener getActionListener(String actionName) { + return actionListeners.get(actionName); + } + } diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java index 1a8d3c624..a1368cdc4 100644 --- a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java @@ -2,12 +2,14 @@ import org.javarosa.core.model.FormDef; import org.javarosa.core.model.actions.Action; +import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.instance.TreeReference; public class RecordAudioAction extends Action { private TreeReference targetReference; public RecordAudioAction(TreeReference targetReference) { + super(RecordAudioActionHandler.ELEMENT_NAME); this.targetReference = targetReference; } @@ -20,6 +22,10 @@ public TreeReference processAction(FormDef model, TreeReference contextRef) { TreeReference contextualizedTargetReference = contextRef == null ? this.targetReference : this.targetReference.contextualize(contextRef); + if (Actions.getActionListener(getName()) != null) { + Actions.getActionListener(getName()).actionTriggered(getName(), contextualizedTargetReference); + } + return contextualizedTargetReference; } } diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java new file mode 100644 index 000000000..d302e787b --- /dev/null +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java @@ -0,0 +1,7 @@ +package org.javarosa.core.model.actions.recordaudio; + +import org.javarosa.core.model.instance.TreeReference; + +public interface XFormsActionListener { + void actionTriggered(String actionName, TreeReference absoluteTargetRef); +} \ No newline at end of file diff --git a/src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java b/src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java new file mode 100644 index 000000000..ff6b80427 --- /dev/null +++ b/src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java @@ -0,0 +1,23 @@ +package org.javarosa.core.model.actions; + +import org.javarosa.core.model.actions.recordaudio.XFormsActionListener; +import org.javarosa.core.model.instance.TreeReference; + +public class CapturingXFormsActionListener implements XFormsActionListener { + public String actionName; + public TreeReference absoluteTargetRef; + + @Override + public void actionTriggered(String actionName, TreeReference absoluteTargetRef) { + this.actionName = actionName; + this.absoluteTargetRef = absoluteTargetRef; + } + + public String getActionName() { + return actionName; + } + + public TreeReference getAbsoluteTargetRef() { + return absoluteTargetRef; + } +} diff --git a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java index c9254cd77..fb7169817 100644 --- a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java +++ b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.javarosa.core.test.Scenario.getRef; import static org.javarosa.core.util.XFormsElement.body; import static org.javarosa.core.util.XFormsElement.head; import static org.javarosa.core.util.XFormsElement.html; @@ -12,6 +13,7 @@ import static org.javarosa.core.util.XFormsElement.title; import java.io.IOException; +import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; import org.javarosa.core.test.Scenario; import org.junit.Test; @@ -35,4 +37,30 @@ public void recordAudioAction_isProcessedOnFormParse() throws IOException { assertThat(scenario.getFormDef().hasAction("recordaudio"), is(true)); } + + @Test + public void recordAudioAction_callsListenerActionTriggeredWhenTriggered() throws IOException { + CapturingXFormsActionListener listener = new CapturingXFormsActionListener(); + Actions.registerActionListener(RecordAudioActionHandler.ELEMENT_NAME, listener); + + Scenario.init("Record audio form", html( + head( + title("Record audio form"), + model( + mainInstance( + t("data id=\"record-audio-form\"", + t("recording"), + t("q1") + )), + t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\""))), + body( + input("/data/q1") + ) + )); + + assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); + assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/recording"))); + + Actions.unregisterActionListener(RecordAudioActionHandler.ELEMENT_NAME); + } } From 73fab5487a29dac40b496e09884064b96ae860b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 17:29:53 -0800 Subject: [PATCH 07/11] Support serialization --- .../javarosa/core/model/CoreModelModule.java | 3 ++- .../recordaudio/RecordAudioAction.java | 23 ++++++++++++++++ .../model/actions/RecordAudioActionTest.java | 27 ++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/CoreModelModule.java b/src/main/java/org/javarosa/core/model/CoreModelModule.java index 02829d033..bbc9526b0 100644 --- a/src/main/java/org/javarosa/core/model/CoreModelModule.java +++ b/src/main/java/org/javarosa/core/model/CoreModelModule.java @@ -51,7 +51,8 @@ public class CoreModelModule implements IModule { "org.javarosa.core.model.data.UncastData", "org.javarosa.core.model.data.helper.BasicDataPointer", "org.javarosa.core.model.actions.SetValueAction", - "org.javarosa.core.model.actions.setgeopoint.StubSetGeopointAction" + "org.javarosa.core.model.actions.setgeopoint.StubSetGeopointAction", + "org.javarosa.core.model.actions.recordaudio.RecordAudioAction" }; @Override diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java index a1368cdc4..0947274ca 100644 --- a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java @@ -1,9 +1,16 @@ package org.javarosa.core.model.actions.recordaudio; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.actions.Action; import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.instance.TreeReference; +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.ExtWrapNullable; +import org.javarosa.core.util.externalizable.PrototypeFactory; public class RecordAudioAction extends Action { private TreeReference targetReference; @@ -28,4 +35,20 @@ public TreeReference processAction(FormDef model, TreeReference contextRef) { return contextualizedTargetReference; } + + //region serialization + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { + super.readExternal(in, pf); + + targetReference = (TreeReference) ExtUtil.read(in, new ExtWrapNullable(TreeReference.class), pf); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + super.writeExternal(out); + + ExtUtil.write(out, new ExtWrapNullable(targetReference)); + } + //endregion } diff --git a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java index fb7169817..9b20820a9 100644 --- a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java +++ b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java @@ -15,6 +15,7 @@ import java.io.IOException; import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; import org.javarosa.core.test.Scenario; +import org.javarosa.core.util.externalizable.DeserializationException; import org.junit.Test; public class RecordAudioActionTest { @@ -60,7 +61,31 @@ public void recordAudioAction_callsListenerActionTriggeredWhenTriggered() throws assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/recording"))); + } - Actions.unregisterActionListener(RecordAudioActionHandler.ELEMENT_NAME); + @Test + public void serializationAndDeserialization_maintainsFields() throws IOException, DeserializationException { + Scenario scenario = Scenario.init("Record audio form", html( + head( + title("Record audio form"), + model( + mainInstance( + t("data id=\"record-audio-form\"", + t("recording"), + t("q1") + )), + t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\""))), + body( + input("/data/q1") + ) + )); + + CapturingXFormsActionListener listener = new CapturingXFormsActionListener(); + Actions.registerActionListener(RecordAudioActionHandler.ELEMENT_NAME, listener); + + scenario.serializeAndDeserializeForm(); + + assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); + assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/recording"))); } } From 5fcb88a50e69dc4caad9c6a2ce0c1bc061de4d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 18:21:34 -0800 Subject: [PATCH 08/11] Fire correct event for nested elements --- .../core/model/actions/ActionController.java | 2 +- .../model/actions/InstanceLoadEventsTest.java | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/core/model/actions/ActionController.java b/src/main/java/org/javarosa/core/model/actions/ActionController.java index 5a9f14f31..7ecf0ffc3 100644 --- a/src/main/java/org/javarosa/core/model/actions/ActionController.java +++ b/src/main/java/org/javarosa/core/model/actions/ActionController.java @@ -80,7 +80,7 @@ public void triggerActionsFromEvent(String event, Set nestedElemen triggerActionsFromEvent(event, model, null, null); for (IFormElement element : nestedElements) { - element.getActionController().triggerActionsFromEvent(Actions.EVENT_ODK_INSTANCE_FIRST_LOAD, model, ((TreeReference) element.getBind().getReference()).getParentRef(), null); + element.getActionController().triggerActionsFromEvent(event, model, ((TreeReference) element.getBind().getReference()).getParentRef(), null); } } diff --git a/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java b/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java index 3aa95e13a..591905e00 100644 --- a/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java +++ b/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java @@ -1,8 +1,10 @@ package org.javarosa.core.model.actions; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.javarosa.core.test.AnswerDataMatchers.intAnswer; +import static org.javarosa.core.test.AnswerDataMatchers.stringAnswer; import static org.javarosa.core.util.BindBuilderXFormsElement.bind; import static org.javarosa.core.util.XFormsElement.body; import static org.javarosa.core.util.XFormsElement.head; @@ -10,10 +12,13 @@ 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.repeat; import static org.javarosa.core.util.XFormsElement.setvalue; import static org.javarosa.core.util.XFormsElement.t; import static org.javarosa.core.util.XFormsElement.title; +import java.io.IOException; +import org.hamcrest.CoreMatchers; import org.javarosa.core.test.Scenario; import org.junit.Test; @@ -85,4 +90,29 @@ public void instanceFirstLoadEvent_doesNotfireOnSecondLoad() throws Exception { Scenario restored = scenario.serializeAndDeserializeForm(); assertThat(restored.answerOf("/data/q1"), is(intAnswer(555))); } + + @Test + public void instanceLoadEvent_triggersNestedActions() throws IOException { + Scenario scenario = Scenario.init("Nested instance load", html( + head( + title("Nested instance load"), + model( + mainInstance( + t("data id=\"nested-instance-load\"", + t("repeat", + t("q1")) + )), + bind("/data/repeat/q1").type("string"))), + body( + repeat("/data/repeat", + setvalue("odk-instance-load", "/data/repeat/q1", "4*4"), + input("/data/repeat/q1")) + ) + )); + + assertThat(scenario.answerOf("/data/repeat[0]/q1"), CoreMatchers.is(stringAnswer("16"))); + + scenario.createNewRepeat("/data/repeat"); + assertThat(scenario.answerOf("/data/repeat[1]/q1"), CoreMatchers.is(nullValue())); + } } From fc3250f270ae4af03609392b9f7212303c390054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Thu, 4 Feb 2021 20:30:14 -0800 Subject: [PATCH 09/11] Fire on-load events for all pre-existing repeats --- .../core/model/actions/ActionController.java | 11 +++++++- .../model/actions/InstanceLoadEventsTest.java | 28 +++++++++++++++++++ .../model/actions/RecordAudioActionTest.java | 27 ++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/core/model/actions/ActionController.java b/src/main/java/org/javarosa/core/model/actions/ActionController.java index 7ecf0ffc3..a971dbf91 100644 --- a/src/main/java/org/javarosa/core/model/actions/ActionController.java +++ b/src/main/java/org/javarosa/core/model/actions/ActionController.java @@ -11,7 +11,9 @@ import java.util.List; import java.util.Set; import org.javarosa.core.model.FormDef; +import org.javarosa.core.model.GroupDef; import org.javarosa.core.model.IFormElement; +import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; @@ -80,7 +82,14 @@ public void triggerActionsFromEvent(String event, Set nestedElemen triggerActionsFromEvent(event, model, null, null); for (IFormElement element : nestedElements) { - element.getActionController().triggerActionsFromEvent(event, model, ((TreeReference) element.getBind().getReference()).getParentRef(), null); + TreeReference elementReference = (TreeReference) element.getBind().getReference(); + TreeReference unqualifiedContext = element instanceof GroupDef ? elementReference : elementReference.getParentRef(); + EvaluationContext context = new EvaluationContext(model.getEvaluationContext(), unqualifiedContext); + List allContextRefs = context.expandReference(unqualifiedContext); + + for (TreeReference contextRef : allContextRefs) { + element.getActionController().triggerActionsFromEvent(event, model, contextRef, null); + } } } diff --git a/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java b/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java index 591905e00..7200b2396 100644 --- a/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java +++ b/src/test/java/org/javarosa/core/model/actions/InstanceLoadEventsTest.java @@ -115,4 +115,32 @@ public void instanceLoadEvent_triggersNestedActions() throws IOException { scenario.createNewRepeat("/data/repeat"); assertThat(scenario.answerOf("/data/repeat[1]/q1"), CoreMatchers.is(nullValue())); } + + @Test + public void instanceLoadEvent_triggeredForAllPreExistingRepeatInstances() throws IOException { + Scenario scenario = Scenario.init("Nested instance load", html( + head( + title("Nested instance load"), + model( + mainInstance( + t("data id=\"nested-instance-load\"", + t("repeat", + t("q1")), + t("repeat", + t("q1")) + )), + bind("/data/repeat/q1").type("string"))), + body( + repeat("/data/repeat", + setvalue("odk-instance-load", "/data/repeat/q1", "4*4"), + input("/data/repeat/q1")) + ) + )); + + assertThat(scenario.answerOf("/data/repeat[0]/q1"), CoreMatchers.is(stringAnswer("16"))); + assertThat(scenario.answerOf("/data/repeat[1]/q1"), CoreMatchers.is(stringAnswer("16"))); + + scenario.createNewRepeat("/data/repeat"); + assertThat(scenario.answerOf("/data/repeat[2]/q1"), CoreMatchers.is(nullValue())); + } } diff --git a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java index 9b20820a9..bb3e932d7 100644 --- a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java +++ b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java @@ -9,6 +9,7 @@ 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.repeat; import static org.javarosa.core.util.XFormsElement.t; import static org.javarosa.core.util.XFormsElement.title; @@ -63,6 +64,32 @@ public void recordAudioAction_callsListenerActionTriggeredWhenTriggered() throws assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/recording"))); } + @Test + public void targetReferenceInRepeat_isContextualized() throws IOException { + CapturingXFormsActionListener listener = new CapturingXFormsActionListener(); + Actions.registerActionListener(RecordAudioActionHandler.ELEMENT_NAME, listener); + + Scenario.init("Record audio form", html( + head( + title("Record audio form"), + model( + mainInstance( + t("data id=\"record-audio-form\"", + t("repeat", + t("recording"), + t("q1")) + )))), + body( + repeat("/data/repeat", + t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/repeat/recording\""), + input("/data/repeat/q1")) + ) + )); + + assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); + assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/repeat[0]/recording"))); + } + @Test public void serializationAndDeserialization_maintainsFields() throws IOException, DeserializationException { Scenario scenario = Scenario.init("Record audio form", html( From ac2df80b4e431fe2ae259e5a0b3cce94a379eef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Fri, 5 Feb 2021 09:27:44 -0800 Subject: [PATCH 10/11] Make listener implementation specific to RecordAudioAction --- .../javarosa/core/model/actions/Actions.java | 28 ------------- .../recordaudio/RecordAudioAction.java | 11 ++++-- .../recordaudio/RecordAudioActionHandler.java | 3 +- .../RecordAudioActionListener.java | 7 ++++ .../recordaudio/RecordAudioActions.java | 22 +++++++++++ .../recordaudio/XFormsActionListener.java | 7 ---- .../CapturingRecordAudioActionListener.java | 39 +++++++++++++++++++ .../CapturingXFormsActionListener.java | 23 ----------- .../model/actions/RecordAudioActionTest.java | 39 +++++++++++++------ 9 files changed, 104 insertions(+), 75 deletions(-) create mode 100644 src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionListener.java create mode 100644 src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActions.java delete mode 100644 src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java create mode 100644 src/test/java/org/javarosa/core/model/actions/CapturingRecordAudioActionListener.java delete mode 100644 src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java diff --git a/src/main/java/org/javarosa/core/model/actions/Actions.java b/src/main/java/org/javarosa/core/model/actions/Actions.java index 381bfb2b1..b88efe641 100644 --- a/src/main/java/org/javarosa/core/model/actions/Actions.java +++ b/src/main/java/org/javarosa/core/model/actions/Actions.java @@ -1,10 +1,6 @@ package org.javarosa.core.model.actions; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; -import org.javarosa.core.model.actions.recordaudio.XFormsActionListener; public class Actions { private Actions() { } @@ -45,12 +41,6 @@ private Actions() { } private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, EVENT_XFORMS_REVALIDATE}; - /** - * Global registry of client classes that want to get updates about triggered actions. Addresses the need for some - * actions to be handled entirely client-side. - */ - private static Map actionListeners = new HashMap<>(); - public static boolean isValidEvent(String actionEventAttribute) { return Arrays.asList(ALL_EVENTS).contains(actionEventAttribute); } @@ -58,22 +48,4 @@ public static boolean isValidEvent(String actionEventAttribute) { public static boolean isTopLevelEvent(String actionEventAttribute) { return Arrays.asList(TOP_LEVEL_EVENTS).contains(actionEventAttribute); } - - - public static void registerActionListener(String actionName, XFormsActionListener listener) { - if (!actionName.equals(RecordAudioActionHandler.ELEMENT_NAME)){ - throw new IllegalArgumentException("Currently, only the recordaudio action notifies listeners"); - } - - actionListeners.put(actionName, listener); - } - - public static void unregisterActionListener(String actionName) { - actionListeners.remove(actionName); - } - - public static XFormsActionListener getActionListener(String actionName) { - return actionListeners.get(actionName); - } - } diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java index 0947274ca..efc1755b1 100644 --- a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioAction.java @@ -5,7 +5,6 @@ import java.io.IOException; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.actions.Action; -import org.javarosa.core.model.actions.Actions; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; @@ -14,10 +13,12 @@ public class RecordAudioAction extends Action { private TreeReference targetReference; + private String quality; - public RecordAudioAction(TreeReference targetReference) { + public RecordAudioAction(TreeReference targetReference, String quality) { super(RecordAudioActionHandler.ELEMENT_NAME); this.targetReference = targetReference; + this.quality = quality; } public RecordAudioAction() { @@ -29,8 +30,8 @@ public TreeReference processAction(FormDef model, TreeReference contextRef) { TreeReference contextualizedTargetReference = contextRef == null ? this.targetReference : this.targetReference.contextualize(contextRef); - if (Actions.getActionListener(getName()) != null) { - Actions.getActionListener(getName()).actionTriggered(getName(), contextualizedTargetReference); + if (RecordAudioActions.getRecordAudioListener() != null) { + RecordAudioActions.getRecordAudioListener().recordAudioTriggered(contextualizedTargetReference, quality); } return contextualizedTargetReference; @@ -42,6 +43,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep super.readExternal(in, pf); targetReference = (TreeReference) ExtUtil.read(in, new ExtWrapNullable(TreeReference.class), pf); + quality = (String) ExtUtil.read(in, new ExtWrapNullable(String.class), pf); } @Override @@ -49,6 +51,7 @@ public void writeExternal(DataOutputStream out) throws IOException { super.writeExternal(out); ExtUtil.write(out, new ExtWrapNullable(targetReference)); + ExtUtil.write(out, new ExtWrapNullable(quality)); } //endregion } diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java index 286e090de..26a8834af 100644 --- a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java @@ -25,6 +25,7 @@ public void handle(XFormParser p, Element e, Object parent) { throw new XFormParseException("recordaudio action must be in http://www.opendatakit.org/xforms namespace"); } + String quality = e.getAttributeValue("http://www.opendatakit.org/xforms", "quality"); String ref = e.getAttributeValue(null, "ref"); if (ref == null) { @@ -42,7 +43,7 @@ public void handle(XFormParser p, Element e, Object parent) { } } - RecordAudioAction action = new RecordAudioAction(target); + RecordAudioAction action = new RecordAudioAction(target, quality); // XFormParser.parseAction already ensures parent is an IFormElement so we can safely cast ((IFormElement) parent).getActionController().registerEventListener(validEventNames, action); diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionListener.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionListener.java new file mode 100644 index 000000000..b867edf83 --- /dev/null +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionListener.java @@ -0,0 +1,7 @@ +package org.javarosa.core.model.actions.recordaudio; + +import org.javarosa.core.model.instance.TreeReference; + +public interface RecordAudioActionListener { + void recordAudioTriggered(TreeReference absoluteTargetRef, String quality); +} \ No newline at end of file diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActions.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActions.java new file mode 100644 index 000000000..1e9e73727 --- /dev/null +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActions.java @@ -0,0 +1,22 @@ +package org.javarosa.core.model.actions.recordaudio; + +public class RecordAudioActions { + private RecordAudioActions() { + + } + + /** + * Global reference to a client class that want to get updates about triggered record audio actions. Recording audio + * needs to be handled entirely client-side and there's no convenient object to hook into to get information + * about triggered actions. + */ + private static RecordAudioActionListener recordAudioListener; + + public static void setRecordAudioListener(RecordAudioActionListener listener) { + recordAudioListener = listener; + } + + public static RecordAudioActionListener getRecordAudioListener() { + return recordAudioListener; + } +} diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java deleted file mode 100644 index d302e787b..000000000 --- a/src/main/java/org/javarosa/core/model/actions/recordaudio/XFormsActionListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.javarosa.core.model.actions.recordaudio; - -import org.javarosa.core.model.instance.TreeReference; - -public interface XFormsActionListener { - void actionTriggered(String actionName, TreeReference absoluteTargetRef); -} \ No newline at end of file diff --git a/src/test/java/org/javarosa/core/model/actions/CapturingRecordAudioActionListener.java b/src/test/java/org/javarosa/core/model/actions/CapturingRecordAudioActionListener.java new file mode 100644 index 000000000..103af280d --- /dev/null +++ b/src/test/java/org/javarosa/core/model/actions/CapturingRecordAudioActionListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 ODK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.javarosa.core.model.actions; + +import org.javarosa.core.model.actions.recordaudio.RecordAudioActionListener; +import org.javarosa.core.model.instance.TreeReference; + +public class CapturingRecordAudioActionListener implements RecordAudioActionListener { + private TreeReference absoluteTargetRef; + private String quality; + + @Override + public void recordAudioTriggered(TreeReference absoluteTargetRef, String quality) { + this.absoluteTargetRef = absoluteTargetRef; + this.quality = quality; + } + + public TreeReference getAbsoluteTargetRef() { + return absoluteTargetRef; + } + + public String getQuality() { + return quality; + } +} diff --git a/src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java b/src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java deleted file mode 100644 index ff6b80427..000000000 --- a/src/test/java/org/javarosa/core/model/actions/CapturingXFormsActionListener.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.javarosa.core.model.actions; - -import org.javarosa.core.model.actions.recordaudio.XFormsActionListener; -import org.javarosa.core.model.instance.TreeReference; - -public class CapturingXFormsActionListener implements XFormsActionListener { - public String actionName; - public TreeReference absoluteTargetRef; - - @Override - public void actionTriggered(String actionName, TreeReference absoluteTargetRef) { - this.actionName = actionName; - this.absoluteTargetRef = absoluteTargetRef; - } - - public String getActionName() { - return actionName; - } - - public TreeReference getAbsoluteTargetRef() { - return absoluteTargetRef; - } -} diff --git a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java index bb3e932d7..221feee2e 100644 --- a/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java +++ b/src/test/java/org/javarosa/core/model/actions/RecordAudioActionTest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2021 ODK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.javarosa.core.model.actions; import static org.hamcrest.MatcherAssert.assertThat; @@ -14,7 +30,7 @@ import static org.javarosa.core.util.XFormsElement.title; import java.io.IOException; -import org.javarosa.core.model.actions.recordaudio.RecordAudioActionHandler; +import org.javarosa.core.model.actions.recordaudio.RecordAudioActions; import org.javarosa.core.test.Scenario; import org.javarosa.core.util.externalizable.DeserializationException; import org.junit.Test; @@ -42,8 +58,8 @@ public void recordAudioAction_isProcessedOnFormParse() throws IOException { @Test public void recordAudioAction_callsListenerActionTriggeredWhenTriggered() throws IOException { - CapturingXFormsActionListener listener = new CapturingXFormsActionListener(); - Actions.registerActionListener(RecordAudioActionHandler.ELEMENT_NAME, listener); + CapturingRecordAudioActionListener listener = new CapturingRecordAudioActionListener(); + RecordAudioActions.setRecordAudioListener(listener); Scenario.init("Record audio form", html( head( @@ -54,20 +70,20 @@ public void recordAudioAction_callsListenerActionTriggeredWhenTriggered() throws t("recording"), t("q1") )), - t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\""))), + t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\" odk:quality=\"foo\""))), body( input("/data/q1") ) )); - assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/recording"))); + assertThat(listener.getQuality(), is("foo")); } @Test public void targetReferenceInRepeat_isContextualized() throws IOException { - CapturingXFormsActionListener listener = new CapturingXFormsActionListener(); - Actions.registerActionListener(RecordAudioActionHandler.ELEMENT_NAME, listener); + CapturingRecordAudioActionListener listener = new CapturingRecordAudioActionListener(); + RecordAudioActions.setRecordAudioListener(listener); Scenario.init("Record audio form", html( head( @@ -86,7 +102,6 @@ public void targetReferenceInRepeat_isContextualized() throws IOException { ) )); - assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/repeat[0]/recording"))); } @@ -101,18 +116,18 @@ public void serializationAndDeserialization_maintainsFields() throws IOException t("recording"), t("q1") )), - t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\""))), + t("odk:recordaudio event=\"odk-instance-load\" ref=\"/data/recording\" odk:quality=\"foo\""))), body( input("/data/q1") ) )); - CapturingXFormsActionListener listener = new CapturingXFormsActionListener(); - Actions.registerActionListener(RecordAudioActionHandler.ELEMENT_NAME, listener); + CapturingRecordAudioActionListener listener = new CapturingRecordAudioActionListener(); + RecordAudioActions.setRecordAudioListener(listener); scenario.serializeAndDeserializeForm(); - assertThat(listener.getActionName(), is(RecordAudioActionHandler.ELEMENT_NAME)); assertThat(listener.getAbsoluteTargetRef(), is(getRef("/data/recording"))); + assertThat(listener.getQuality(), is("foo")); } } From 7799be9fe83e26ba09f7e0083c7df730df94c7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Fri, 5 Feb 2021 09:38:43 -0800 Subject: [PATCH 11/11] Only allow odk:recordaudio for instance load events --- src/main/java/org/javarosa/core/model/actions/Actions.java | 6 ++++++ .../model/actions/recordaudio/RecordAudioActionHandler.java | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/actions/Actions.java b/src/main/java/org/javarosa/core/model/actions/Actions.java index b88efe641..a11daee8e 100644 --- a/src/main/java/org/javarosa/core/model/actions/Actions.java +++ b/src/main/java/org/javarosa/core/model/actions/Actions.java @@ -38,6 +38,8 @@ private Actions() { } private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE}; + private static final String[] INSTANCE_LOAD_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY}; + private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY, EVENT_XFORMS_REVALIDATE}; @@ -48,4 +50,8 @@ public static boolean isValidEvent(String actionEventAttribute) { public static boolean isTopLevelEvent(String actionEventAttribute) { return Arrays.asList(TOP_LEVEL_EVENTS).contains(actionEventAttribute); } + + public static boolean isInstanceLoadEvent(String actionEventAttribute) { + return Arrays.asList(INSTANCE_LOAD_EVENTS).contains(actionEventAttribute); + } } diff --git a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java index 26a8834af..337ffe093 100644 --- a/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java +++ b/src/main/java/org/javarosa/core/model/actions/recordaudio/RecordAudioActionHandler.java @@ -38,8 +38,8 @@ public void handle(XFormParser p, Element e, Object parent) { List validEventNames = getValidEventNames(e.getAttributeValue(null, EVENT_ATTR)); for (String eventName : validEventNames) { - if (!Actions.isTopLevelEvent(eventName)) { - throw new XFormParseException("odk:recordaudio action may only be triggered by top-level events (e.g. odk-instance-load)"); + if (!Actions.isInstanceLoadEvent(eventName)) { + throw new XFormParseException("odk:recordaudio action may only be triggered by instance load events (e.g. odk-instance-load)"); } }