From 384e41caf78222c53e5cf365bb93310e5b28462b Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 6 Nov 2020 12:27:53 +0100 Subject: [PATCH] Work on ODK manifest support. --- .../OSGI-INF/odkManifestServlet.xml | 10 ++ knowledge/org.argeo.support.odk/bnd.bnd | 3 +- .../src/org/argeo/support/odk/OdkUtils.java | 75 +++++++++++- .../{OrxListType.java => OrxListName.java} | 6 +- .../argeo/support/odk/OrxManifestName.java | 27 +++++ .../src/org/argeo/support/odk/odk.cnd | 8 ++ .../odk/servlet/OdkFormListServlet.java | 32 +++--- .../odk/servlet/OdkManifestServlet.java | 108 ++++++++++++++++++ .../src/org/argeo/entity/entity.cnd | 6 +- .../org/argeo/suite/ui/DefaultLeadPane.java | 3 +- 10 files changed, 253 insertions(+), 25 deletions(-) create mode 100644 knowledge/org.argeo.support.odk/OSGI-INF/odkManifestServlet.xml rename knowledge/org.argeo.support.odk/src/org/argeo/support/odk/{OrxListType.java => OrxListName.java} (83%) create mode 100644 knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxManifestName.java create mode 100644 knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkManifestServlet.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 index 0000000..cfc56ef --- /dev/null +++ b/knowledge/org.argeo.support.odk/OSGI-INF/odkManifestServlet.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/knowledge/org.argeo.support.odk/bnd.bnd b/knowledge/org.argeo.support.odk/bnd.bnd index b8a1a58..fa581ce 100644 --- a/knowledge/org.argeo.support.odk/bnd.bnd +++ b/knowledge/org.argeo.support.odk/bnd.bnd @@ -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,\ diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OdkUtils.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OdkUtils.java index 4c106a2..0e73295 100644 --- a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OdkUtils.java +++ b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OdkUtils.java @@ -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/OrxListType.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListName.java similarity index 83% rename from knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListType.java rename to knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListName.java index c51484c..0404c90 100644 --- a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListType.java +++ b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxListName.java @@ -3,8 +3,10 @@ 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; +public enum OrxListName implements JcrName { + xform, + // names + formID, version; @Override public String getPrefix() { 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 index 0000000..c9fcc5d --- /dev/null +++ b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/OrxManifestName.java @@ -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"; + } + +} diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/odk.cnd b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/odk.cnd index e1cd74a..d80096d 100644 --- a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/odk.cnd +++ b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/odk.cnd @@ -39,7 +39,15 @@ // 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 diff --git a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkFormListServlet.java b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkFormListServlet.java index f829c12..7c752c7 100644 --- a/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkFormListServlet.java +++ b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkFormListServlet.java @@ -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 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(""); - sb.append("" - + node.getNode(OdkNames.H_HTML + "/h:head/xforms:model/xforms:instance/xforms:data") - .getProperty("id").getString() - + ""); + sb.append("" + node.getProperty(OrxListName.formID.get()).getString() + ""); sb.append("" + Jcr.getTitle(node) + ""); - sb.append("" + versionFormatter.format(JcrUtils.getModified(node)) + ""); -// sb.append("" + versionFormatter.format(JcrUtils.getModified(node)) + ""); + sb.append("" + node.getProperty(OrxListName.version.get()).getString() + ""); sb.append("md5:" + JcrxApi.getChecksum(node, JcrxApi.MD5) + ""); if (node.hasProperty(Property.JCR_DESCRIPTION)) sb.append("" + node.getProperty(Property.JCR_DESCRIPTION).getString() + ""); sb.append("" + protocol + "://" + serverName + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort) + "/api/odk/form/" + node.getPath() + ""); + if (node.hasNode(OrxManifestName.manifest.name())) { + sb.append("" + protocol + "://" + serverName + + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort) + + "/api/odk/formManifest" + node.getNode(OrxManifestName.manifest.name()).getPath() + + ""); + } sb.append(""); } else if (node.isNodeType(EntityType.formSet.get())) { sb.append(""); 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 index 0000000..88b9c69 --- /dev/null +++ b/knowledge/org.argeo.support.odk/src/org/argeo/support/odk/servlet/OdkManifestServlet.java @@ -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(""); + writer.append(""); + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) { + Node file = nit.nextNode(); + if (file.isNodeType(OrxManifestName.mediaFile.get())) { + writer.append(""); + + if (file.isNodeType(NodeType.NT_ADDRESS)) { + Node target = file.getProperty(Property.JCR_ID).getNode(); + writer.append(""); + writer.append(target.getPath().substring(1) + ".xml"); + writer.append(""); + 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(""); + writer.append("md5sum:" + fileCsum); + writer.append(""); + } + writer.append("" + protocol + "://" + serverName + + (serverPort == 80 || serverPort == 443 ? "" : ":" + serverPort) + + "/api/odk/formManifest" + file.getPath() + ""); + } + writer.append(""); + } + } + + writer.append(""); + } 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; + } + +} diff --git a/org.argeo.entity.api/src/org/argeo/entity/entity.cnd b/org.argeo.entity.api/src/org/argeo/entity/entity.cnd index 46b3f07..12d1ba6 100644 --- a/org.argeo.entity.api/src/org/argeo/entity/entity.cnd +++ b/org.argeo.entity.api/src/org/argeo/entity/entity.cnd @@ -4,7 +4,7 @@ // see https://www.w3.org/2003/01/geo/ // - + [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) diff --git a/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultLeadPane.java b/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultLeadPane.java index ed7f9aa..4a641dc 100644 --- a/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultLeadPane.java +++ b/org.argeo.suite.ui/src/org/argeo/suite/ui/DefaultLeadPane.java @@ -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 properties) { -- 2.30.2