Work on ODK manifest support.
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 6 Nov 2020 11:27:53 +0000 (12:27 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 6 Nov 2020 11:27:53 +0000 (12:27 +0100)
knowledge/org.argeo.support.odk/OSGI-INF/odkManifestServlet.xml [new file with mode: 0644]
knowledge/org.argeo.support.odk/bnd.bnd
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OdkUtils.java
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListName.java [new file with mode: 0644]
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListType.java [deleted file]
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxManifestName.java [new file with mode: 0644]
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/odk.cnd
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkFormListServlet.java
knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkManifestServlet.java [new file with mode: 0644]
org.argeo.entity.api/src/org/argeo/entity/entity.cnd
org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultLeadPane.java

diff --git a/knowledge/org.argeo.support.odk/OSGI-INF/odkManifestServlet.xml b/knowledge/org.argeo.support.odk/OSGI-INF/odkManifestServlet.xml
new file mode 100644 (file)
index 0000000..cfc56ef
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="ODK Manifest Servlet">
+   <implementation class="org.argeo.support.odk.servlet.OdkManifestServlet"/>
+   <service>
+      <provide interface="javax.servlet.Servlet"/>
+   </service>
+   <property name="osgi.http.whiteboard.servlet.pattern" type="String" value="/formManifest/*"/>
+   <property name="osgi.http.whiteboard.context.select" type="String" value="(osgi.http.whiteboard.context.name=odkServletContext)"/>
+   <reference bind="setRepository" cardinality="1..1" interface="javax.jcr.Repository" name="Repository" policy="static" target="(cn=odk)"/>
+</scr:component>
index b8a1a58d9f9deddd621c389a79a6598823bcf685..fa581ce68696f525a273ab9ce6123b5dd4987971 100644 (file)
@@ -9,7 +9,8 @@ Service-Component:\
 OSGI-INF/odkServletContext.xml,\
 OSGI-INF/odkFormListServlet.xml,\
 OSGI-INF/odkFormServlet.xml,\
-OSGI-INF/odkSubmissionServlet.xml
+OSGI-INF/odkSubmissionServlet.xml,\
+OSGI-INF/odkManifestServlet.xml
 
 Import-Package:\
 org.osgi.service.http.context,\
index 4c106a29b2a7dfd007ce2cb6cead390a34a09ce4..0e73295b6f3d6c3f1ab7744d3b9bf9290160781f 100644 (file)
@@ -3,9 +3,13 @@ package org.argeo.support.odk;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 
 import javax.jcr.ImportUUIDBehavior;
 import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.jcr.nodetype.NodeType;
@@ -13,6 +17,7 @@ import javax.jcr.nodetype.NodeType;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.argeo.entity.EntityType;
+import org.argeo.jcr.Jcr;
 import org.argeo.jcr.JcrUtils;
 import org.argeo.jcr.JcrxApi;
 import org.argeo.util.DigestUtils;
@@ -25,9 +30,11 @@ public class OdkUtils {
                if (!formBase.isNodeType(EntityType.formSet.get()))
                        throw new IllegalArgumentException(
                                        "Parent path " + formBase + " must be of type " + EntityType.formSet.get());
-               Node form = JcrUtils.getOrAdd(formBase, name, OrxListType.xform.get(), NodeType.MIX_SIMPLE_VERSIONABLE);
+               Node form = JcrUtils.getOrAdd(formBase, name, OrxListName.xform.get(), NodeType.MIX_VERSIONABLE);
 
                String previousCsum = JcrxApi.getChecksum(form, JcrxApi.MD5);
+               String previousFormId = Jcr.get(form, OrxListName.formID.get());
+               String previousFormVersion = Jcr.get(form, OrxListName.version.get());
 
                Session s = formBase.getSession();
 //             String res = "/odk/apafSession.odk.xml";
@@ -35,6 +42,65 @@ public class OdkUtils {
                s.importXML(form.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
 //             }
 
+               // manage instances
+               // NodeIterator instances =
+               // form.getNodes("h:html/h:head/xforms:model/xforms:instance");
+               NodeIterator instances = form.getNode("h:html/h:head/xforms:model").getNodes("xforms:instance");
+               Node primaryInstance = null;
+               while (instances.hasNext()) {
+                       Node instance = instances.nextNode();
+                       if (primaryInstance == null) {
+                               primaryInstance = instance;
+                       } else {// secondary instances
+                               String instanceId = instance.getProperty("id").getString();
+                               URI instanceUri = null;
+                               if (instance.hasProperty("src"))
+                                       try {
+                                               instanceUri = new URI(instance.getProperty("src").getString());
+                                       } catch (URISyntaxException e) {
+                                               throw new IllegalArgumentException("Instance " + instanceId + " has a badly formatted URI", e);
+                                       }
+                               if (instanceUri != null) {
+                                       if ("jr".equals(instanceUri.getScheme())) {
+                                               String type = instanceUri.getHost();
+                                               if ("file".equals(type)) {
+                                                       Node manifest = JcrUtils.getOrAdd(form, OrxManifestName.manifest.name(),
+                                                                       OrxManifestName.manifest.get());
+                                                       Node file = JcrUtils.getOrAdd(manifest, instanceId);
+                                                       String path = instanceUri.getPath();
+                                                       if (!path.endsWith(".xml"))
+                                                               throw new IllegalArgumentException("File uri " + instanceUri + " must end with .xml");
+                                                       path = path.substring(0, path.length() - ".xml".length());
+                                                       Node target = file.getSession().getNode(path);
+                                                       if (target.isNodeType(NodeType.MIX_REFERENCEABLE)) {
+                                                               file.setProperty(Property.JCR_ID, target);
+                                                               if (file.hasProperty(Property.JCR_PATH))
+                                                                       file.getProperty(Property.JCR_PATH).remove();
+                                                       } else {
+                                                               file.setProperty(Property.JCR_PATH, target.getPath());
+                                                               if (file.hasProperty(Property.JCR_ID))
+                                                                       file.getProperty(Property.JCR_ID).remove();
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               if (primaryInstance == null)
+                       throw new IllegalArgumentException("No primary instance found in " + form);
+               if (!primaryInstance.hasNodes())
+                       throw new IllegalArgumentException("No data found in primary instance of " + form);
+               NodeIterator primaryInstanceChildren = primaryInstance.getNodes();
+               Node data = primaryInstanceChildren.nextNode();
+               if (primaryInstanceChildren.hasNext())
+                       throw new IllegalArgumentException("More than one data found in primary instance of " + form);
+               String formId = data.getProperty("id").getString();
+               if (previousFormId != null && !formId.equals(previousFormId))
+                       log.warn("Form id of " + form + " changed from " + previousFormId + " to " + formId);
+               form.setProperty(OrxListName.formID.get(), formId);
+               String formVersion = data.getProperty("version").getString();
+
                if (previousCsum == null)// save before checksuming
                        s.save();
                String newCsum;
@@ -45,6 +111,7 @@ public class OdkUtils {
                if (previousCsum == null) {
                        JcrxApi.addChecksum(form, newCsum);
                        JcrUtils.updateLastModified(form);
+                       form.setProperty(OrxListName.version.get(), formVersion);
                        s.save();
                        s.getWorkspace().getVersionManager().checkpoint(form.getPath());
                        if (log.isDebugEnabled())
@@ -57,6 +124,12 @@ public class OdkUtils {
                                        log.debug("Unmodified form " + form);
                                return;
                        } else {
+                               if (formVersion.equals(previousFormVersion)) {
+                                       s.refresh(false);
+                                       throw new IllegalArgumentException("Form " + form + " has been changed but version " + formVersion
+                                                       + " has not been changed, discarding changes...");
+                               }
+                               form.setProperty(OrxListName.version.get(), formVersion);
                                JcrxApi.addChecksum(form, newCsum);
                                JcrUtils.updateLastModified(form);
                                s.save();
diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListName.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListName.java
new file mode 100644 (file)
index 0000000..0404c90
--- /dev/null
@@ -0,0 +1,29 @@
+package org.argeo.support.odk;
+
+import org.argeo.entity.JcrName;
+
+/** Types related to the http://openrosa.org/xforms/xformsList namespace. */
+public enum OrxListName implements JcrName {
+       xform,
+       // names
+       formID, version;
+
+       @Override
+       public String getPrefix() {
+               return prefix();
+       }
+
+       public static String prefix() {
+               return "orxList";
+       }
+
+       @Override
+       public String getNamespace() {
+               return namespace();
+       }
+
+       public static String namespace() {
+               return "http://openrosa.org/xforms/xformsList";
+       }
+
+}
diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListType.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListType.java
deleted file mode 100644 (file)
index c51484c..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.argeo.support.odk;
-
-import org.argeo.entity.JcrName;
-
-/** Types related to the http://openrosa.org/xforms/xformsList namespace. */
-public enum OrxListType implements JcrName {
-       xform;
-
-       @Override
-       public String getPrefix() {
-               return prefix();
-       }
-
-       public static String prefix() {
-               return "orxList";
-       }
-
-       @Override
-       public String getNamespace() {
-               return namespace();
-       }
-
-       public static String namespace() {
-               return "http://openrosa.org/xforms/xformsList";
-       }
-
-}
diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxManifestName.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxManifestName.java
new file mode 100644 (file)
index 0000000..c9fcc5d
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.support.odk;
+
+import org.argeo.entity.JcrName;
+
+/** Types related to the http://openrosa.org/xforms/xformsList namespace. */
+public enum OrxManifestName implements JcrName {
+       manifest, mediaFile;
+
+       @Override
+       public String getPrefix() {
+               return prefix();
+       }
+
+       public static String prefix() {
+               return "orxManifest";
+       }
+
+       @Override
+       public String getNamespace() {
+               return namespace();
+       }
+
+       public static String namespace() {
+               return "http://openrosa.org/xforms/xformsManifest";
+       }
+
+}
index e1cd74a3d24d0b5072b5d2347fd7b324b8aefa0f..d80096d5454126baf43f97860a886b8eba5cde13 100644 (file)
 // OpenRosa web API
 
 [orxList:xform] > mix:created, mix:lastModified, jcrx:csum, entity:form
+- orxList:formID (STRING)
+- orxList:version (STRING)
 + h:html (odk:html) = odk:html
++ manifest (orxManifest:manifest) = orxManifest:manifest
+
+[orxManifest:manifest]
++ * (orxManifest:mediaFile) = orxManifest:mediaFile
+
+[orxManifest:mediaFile] > nt:address, jcrx:csum
 
 [orx:submission] > mix:created, entity:formSubmission
 + xml_submission_file (nt:unstructured) = nt:unstructured
index f829c12f92eba407afd9238c6fcceaabb2454a5a..7c752c7ed60c4a51b4b4a336d3d22a1fc0a64994 100644 (file)
@@ -2,9 +2,6 @@ package org.argeo.support.odk.servlet;
 
 import java.io.IOException;
 import java.io.Writer;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -28,11 +25,10 @@ import org.argeo.api.NodeConstants;
 import org.argeo.cms.servlet.ServletAuthUtils;
 import org.argeo.entity.EntityType;
 import org.argeo.jcr.Jcr;
-import org.argeo.jcr.JcrUtils;
 import org.argeo.jcr.JcrxApi;
 import org.argeo.support.odk.OdkForm;
-import org.argeo.support.odk.OdkNames;
-import org.argeo.support.odk.OrxListType;
+import org.argeo.support.odk.OrxListName;
+import org.argeo.support.odk.OrxManifestName;
 
 /** Lists available forms. */
 public class OdkFormListServlet extends HttpServlet {
@@ -41,8 +37,8 @@ public class OdkFormListServlet extends HttpServlet {
 
        private Set<OdkForm> odkForms = Collections.synchronizedSet(new HashSet<>());
 
-       private DateTimeFormatter versionFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd-HHmm")
-                       .withZone(ZoneId.from(ZoneOffset.UTC));
+//     private DateTimeFormatter versionFormatter = DateTimeFormatter.ofPattern("YYYY-MM-dd-HHmm")
+//                     .withZone(ZoneId.from(ZoneOffset.UTC));
 
        private Repository repository;
 
@@ -72,11 +68,11 @@ public class OdkFormListServlet extends HttpServlet {
 //                             query = session.getWorkspace().getQueryManager()
 //                                             .createQuery("SELECT * FROM [nt:unstructured]", Query.JCR_SQL2);
                                        query = session.getWorkspace().getQueryManager()
-                                                       .createQuery("SELECT * FROM [" + OrxListType.xform.get() + "]", Query.JCR_SQL2);
+                                                       .createQuery("SELECT * FROM [" + OrxListName.xform.get() + "]", Query.JCR_SQL2);
                                } else {
                                        query = session.getWorkspace().getQueryManager()
                                                        .createQuery(
-                                                                       "SELECT node FROM [" + OrxListType.xform.get()
+                                                                       "SELECT node FROM [" + OrxListName.xform.get()
                                                                                        + "] AS node WHERE ISDESCENDANTNODE (node, '" + pathInfo + "')",
                                                                        Query.JCR_SQL2);
                                }
@@ -92,21 +88,23 @@ public class OdkFormListServlet extends HttpServlet {
                                while (nit.hasNext()) {
                                        StringBuilder sb = new StringBuilder();
                                        Node node = nit.nextNode();
-                                       if (node.isNodeType(OrxListType.xform.get())) {
+                                       if (node.isNodeType(OrxListName.xform.get())) {
                                                sb.append("<xform>");
-                                               sb.append("<formID>"
-                                                               + node.getNode(OdkNames.H_HTML + "/h:head/xforms:model/xforms:instance/xforms:data")
-                                                                               .getProperty("id").getString()
-                                                               + "</formID>");
+                                               sb.append("<formID>" + node.getProperty(OrxListName.formID.get()).getString() + "</formID>");
                                                sb.append("<name>" + Jcr.getTitle(node) + "</name>");
-                                               sb.append("<version>" + versionFormatter.format(JcrUtils.getModified(node)) + "</version>");
-//                                             sb.append("<version>" + versionFormatter.format(JcrUtils.getModified(node)) + "</version>");
+                                               sb.append("<version>" + node.getProperty(OrxListName.version.get()).getString() + "</version>");
                                                sb.append("<hash>md5:" + JcrxApi.getChecksum(node, JcrxApi.MD5) + "</hash>");
                                                if (node.hasProperty(Property.JCR_DESCRIPTION))
                                                        sb.append("<name>" + node.getProperty(Property.JCR_DESCRIPTION).getString() + "</name>");
                                                sb.append("<downloadUrl>" + protocol + "://" + serverName
                                                                + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort) + "/api/odk/form/"
                                                                + node.getPath() + "</downloadUrl>");
+                                               if (node.hasNode(OrxManifestName.manifest.name())) {
+                                                       sb.append("<manifestUrl>" + protocol + "://" + serverName
+                                                                       + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort)
+                                                                       + "/api/odk/formManifest" + node.getNode(OrxManifestName.manifest.name()).getPath()
+                                                                       + "</manifestUrl>");
+                                               }
                                                sb.append("</xform>");
                                        } else if (node.isNodeType(EntityType.formSet.get())) {
                                                sb.append("<xforms-group>");
diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkManifestServlet.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkManifestServlet.java
new file mode 100644 (file)
index 0000000..88b9c69
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.support.odk.servlet;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.cms.servlet.ServletAuthUtils;
+import org.argeo.jcr.Jcr;
+import org.argeo.jcr.JcrException;
+import org.argeo.support.odk.OrxManifestName;
+import org.argeo.util.DigestUtils;
+
+/** Describe additional files. */
+public class OdkManifestServlet extends HttpServlet {
+       private static final long serialVersionUID = 138030510865877478L;
+
+       private Repository repository;
+
+       @Override
+       protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               resp.setContentType("text/xml; charset=utf-8");
+               resp.setHeader("X-OpenRosa-Version", "1.0");
+               resp.setDateHeader("Date", System.currentTimeMillis());
+
+               String pathInfo = req.getPathInfo();
+               if (pathInfo.startsWith("//"))
+                       pathInfo = pathInfo.substring(1);
+
+               String serverName = req.getServerName();
+               int serverPort = req.getServerPort();
+               String protocol = serverPort == 443 || req.isSecure() ? "https" : "http";
+
+               Session session = ServletAuthUtils.doAs(() -> Jcr.login(repository, null), req);
+
+               try {
+                       Node node = session.getNode(pathInfo);
+                       if (node.isNodeType(OrxManifestName.manifest.get())) {
+                               Writer writer = resp.getWriter();
+                               writer.append("<?xml version='1.0' encoding='UTF-8' ?>");
+                               writer.append("<manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">");
+                               NodeIterator nit = node.getNodes();
+                               while (nit.hasNext()) {
+                                       Node file = nit.nextNode();
+                                       if (file.isNodeType(OrxManifestName.mediaFile.get())) {
+                                               writer.append("<mediaFile>");
+
+                                               if (file.isNodeType(NodeType.NT_ADDRESS)) {
+                                                       Node target = file.getProperty(Property.JCR_ID).getNode();
+                                                       writer.append("<filename>");
+                                                       writer.append(target.getPath().substring(1) + ".xml");
+                                                       writer.append("</filename>");
+                                                       try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+                                                               session.exportDocumentView(target.getPath(), out, true, false);
+                                                               String fileCsum = DigestUtils.digest(DigestUtils.MD5, out.toByteArray());
+//                                             JcrxApi.addChecksum(file, fileCsum);
+                                                               writer.append("<hash>");
+                                                               writer.append("md5sum:" + fileCsum);
+                                                               writer.append("</hash>");
+                                                       }
+                                                       writer.append("<downloadUrl>" + protocol + "://" + serverName
+                                                                       + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort)
+                                                                       + "/api/odk/formManifest" + file.getPath() + "</downloadUrl>");
+                                               }
+                                               writer.append("</mediaFile>");
+                                       }
+                               }
+
+                               writer.append("</manifest>");
+                       } else if (node.isNodeType(OrxManifestName.mediaFile.get())) {
+                               if (node.isNodeType(NodeType.NT_ADDRESS)) {
+                                       Node target = node.getProperty(Property.JCR_ID).getNode();
+                                       try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+                                               session.exportDocumentView(target.getPath(), out, true, false);
+                                               System.out.println(new String(out.toByteArray(), StandardCharsets.UTF_8));
+                                               resp.getOutputStream().write(out.toByteArray());
+                                       }
+                               } else {
+                                       throw new IllegalArgumentException("Unsupported node " + node);
+                               }
+                       } else {
+                               throw new IllegalArgumentException("Unsupported node " + node);
+                       }
+               } catch (RepositoryException e) {
+                       throw new JcrException(e);
+               } finally {
+                       Jcr.logout(session);
+               }
+
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+}
index 46b3f07d186322dd5f19500b8e504732149ae789..12d1ba69a2fa25b70bc347f7f3212dc30d1fe7f8 100644 (file)
@@ -4,7 +4,7 @@
 // see https://www.w3.org/2003/01/geo/
 //<geo = "http://www.w3.org/2003/01/geo/wgs84_pos#">
 
-<ldap = "http://www.argeo.org/ns/ldap">
+<ldif = "http://www.argeo.org/ns/ldif">
 <entity = 'http://www.argeo.org/ns/entity'>
 
 [entity:entity] > mix:created, mix:referenceable
@@ -60,5 +60,5 @@ mixin
 // A real person
 [entity:person] > entity:entity
 mixin
-- ldap:sn (String)
-- ldap:givenName (String)
+- ldif:sn (String)
+- ldif:givenName (String)
index ed7f9aaf54f75a6737202753787a74334acc9f94..4a641dcb807338cefb24cbe1e06d6bfded880966 100644 (file)
@@ -1,5 +1,6 @@
 package org.argeo.suite.ui;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
@@ -100,7 +101,7 @@ public class DefaultLeadPane implements CmsUiProvider {
                if (defaultLayers == null)
                        throw new IllegalArgumentException("Default layers must be set.");
                if (log.isDebugEnabled())
-                       log.debug("Default layers: " + defaultLayers);
+                       log.debug("Default layers: " + Arrays.asList(defaultLayers));
        }
 
        public void addLayer(SuiteLayer layer, Map<String, Object> properties) {