1 package org
.argeo
.app
.servlet
.odk
;
3 import java
.io
.IOException
;
4 import java
.io
.OutputStream
;
6 import java
.nio
.charset
.Charset
;
7 import java
.security
.DigestOutputStream
;
8 import java
.security
.MessageDigest
;
9 import java
.security
.NoSuchAlgorithmException
;
10 import java
.util
.ArrayList
;
11 import java
.util
.List
;
13 import javax
.jcr
.ItemNotFoundException
;
14 import javax
.jcr
.Node
;
15 import javax
.jcr
.NodeIterator
;
16 import javax
.jcr
.Property
;
17 import javax
.jcr
.Repository
;
18 import javax
.jcr
.RepositoryException
;
19 import javax
.jcr
.Session
;
20 import javax
.jcr
.Value
;
21 import javax
.jcr
.nodetype
.NodeType
;
22 import javax
.jcr
.query
.Query
;
23 import javax
.jcr
.query
.QueryResult
;
24 import javax
.jcr
.query
.Row
;
25 import javax
.jcr
.query
.RowIterator
;
26 import javax
.servlet
.ServletException
;
27 import javax
.servlet
.http
.HttpServlet
;
28 import javax
.servlet
.http
.HttpServletRequest
;
29 import javax
.servlet
.http
.HttpServletResponse
;
31 import org
.apache
.commons
.io
.output
.NullOutputStream
;
32 import org
.argeo
.app
.api
.EntityMimeType
;
33 import org
.argeo
.app
.odk
.OrxManifestName
;
34 import org
.argeo
.cms
.auth
.RemoteAuthUtils
;
35 import org
.argeo
.cms
.servlet
.ServletHttpRequest
;
36 import org
.argeo
.cms
.servlet
.ServletUtils
;
37 import org
.argeo
.cms
.util
.CsvWriter
;
38 import org
.argeo
.cms
.util
.DigestUtils
;
39 import org
.argeo
.jcr
.Jcr
;
40 import org
.argeo
.jcr
.JcrException
;
42 /** Describe additional files. */
43 public class OdkManifestServlet
extends HttpServlet
{
44 private static final long serialVersionUID
= 138030510865877478L;
46 private Repository repository
;
49 protected void doGet(HttpServletRequest req
, HttpServletResponse resp
) throws ServletException
, IOException
{
50 resp
.setHeader("X-OpenRosa-Version", "1.0");
51 resp
.setDateHeader("Date", System
.currentTimeMillis());
53 String pathInfo
= req
.getPathInfo();
54 if (pathInfo
.startsWith("//"))
55 pathInfo
= pathInfo
.substring(1);
57 // we force HTTPS since ODK Collect will fail anyhow when sending http
58 // cf. https://forum.getodk.org/t/authentication-for-non-https-schems/32967/4
59 StringBuilder baseServer
= ServletUtils
.getRequestUrlBase(req
, true);
61 Session session
= RemoteAuthUtils
.doAs(() -> Jcr
.login(repository
, null), new ServletHttpRequest(req
));
64 Node node
= session
.getNode(pathInfo
);
65 if (node
.isNodeType(OrxManifestName
.manifest
.get())) {
66 resp
.setContentType(EntityMimeType
.XML
.toHttpContentType());
67 Writer writer
= resp
.getWriter();
68 writer
.append("<?xml version='1.0' encoding='UTF-8' ?>");
69 writer
.append("<manifest xmlns=\"http://openrosa.org/xforms/xformsManifest\">");
70 NodeIterator nit
= node
.getNodes();
71 children
: while (nit
.hasNext()) {
72 Node file
= nit
.nextNode();
73 if (file
.isNodeType(OrxManifestName
.mediaFile
.get())) {
74 EntityMimeType mimeType
= EntityMimeType
75 .find(file
.getProperty(Property
.JCR_MIMETYPE
).getString());
76 Charset charset
= Charset
.forName(file
.getProperty(Property
.JCR_ENCODING
).getString());
78 if (file
.isNodeType(NodeType
.NT_ADDRESS
)) {
81 target
= file
.getProperty(Property
.JCR_ID
).getNode();
82 } catch (ItemNotFoundException e
) {
83 // TODO remove old manifests
86 writer
.append("<mediaFile>");
87 writer
.append("<filename>");
88 // Work around bug in ODK Collect not supporting paths
89 // writer.append(target.getPath().substring(1) + ".xml");
90 writer
.append(target
.getIdentifier() + "." + mimeType
.getDefaultExtension());
91 writer
.append("</filename>");
93 MessageDigest messageDigest
= MessageDigest
.getInstance(DigestUtils
.MD5
);
94 // TODO cache a temp file ?
95 try (DigestOutputStream out
= new DigestOutputStream(NullOutputStream
.NULL_OUTPUT_STREAM
,
97 writeMediaFile(out
, target
, mimeType
, charset
);
98 writer
.append("<hash>");
99 writer
.append("md5sum:" + DigestUtils
.toHexString(out
.getMessageDigest().digest()));
100 writer
.append("</hash>");
102 writer
.append("<downloadUrl>" + baseServer
+ "/api/odk/formManifest" + file
.getPath()
105 writer
.append("</mediaFile>");
109 writer
.append("</manifest>");
110 } else if (node
.isNodeType(OrxManifestName
.mediaFile
.get())) {
111 EntityMimeType mimeType
= EntityMimeType
.find(node
.getProperty(Property
.JCR_MIMETYPE
).getString());
112 Charset charset
= Charset
.forName(node
.getProperty(Property
.JCR_ENCODING
).getString());
113 resp
.setContentType(mimeType
.toHttpContentType(charset
));
114 if (node
.isNodeType(NodeType
.NT_ADDRESS
)) {
115 Node target
= node
.getProperty(Property
.JCR_ID
).getNode();
117 writeMediaFile(resp
.getOutputStream(), target
, mimeType
, charset
);
119 throw new IllegalArgumentException("Unsupported node " + node
);
122 throw new IllegalArgumentException("Unsupported node " + node
);
124 } catch (RepositoryException e
) {
125 throw new JcrException(e
);
126 } catch (NoSuchAlgorithmException e
) {
127 throw new ServletException(e
);
134 protected void writeMediaFile(OutputStream out
, Node target
, EntityMimeType mimeType
, Charset charset
)
135 throws RepositoryException
, IOException
{
136 if (target
.isNodeType(NodeType
.NT_QUERY
)) {
137 Query query
= target
.getSession().getWorkspace().getQueryManager().getQuery(target
);
138 QueryResult queryResult
= query
.execute();
139 List
<String
> columnNames
= new ArrayList
<>();
140 for (String c
: queryResult
.getColumnNames()) {
143 // TODO make it more configurable
144 columnNames
.add("display");
146 if (EntityMimeType
.XML
.equals(mimeType
)) {
147 } else if (EntityMimeType
.CSV
.equals(mimeType
)) {
148 CsvWriter csvWriter
= new CsvWriter(out
, charset
);
149 csvWriter
.writeLine(columnNames
);
150 RowIterator rit
= queryResult
.getRows();
152 while (rit
.hasNext()) {
153 Row row
= rit
.nextRow();
154 Value
[] values
= row
.getValues();
155 List
<String
> lst
= new ArrayList
<>();
156 for (Value value
: values
) {
157 lst
.add(value
.getString());
160 lst
.add(row
.getValue("name").getString() + " (" + row
.getValue("label").getString() + ")");
161 csvWriter
.writeLine(lst
);
164 // corner case of an empty initial database
165 List
<String
> lst
= new ArrayList
<>();
166 for (int i
= 0; i
< columnNames
.size(); i
++)
168 csvWriter
.writeLine(lst
);
172 if (EntityMimeType
.XML
.equals(mimeType
)) {
173 target
.getSession().exportDocumentView(target
.getPath(), out
, true, false);
174 } else if (EntityMimeType
.CSV
.equals(mimeType
)) {
175 CsvWriter csvWriter
= new CsvWriter(out
, charset
);
176 csvWriter
.writeLine(new String
[] { "name", "label" });
177 NodeIterator children
= target
.getNodes();
178 while (children
.hasNext()) {
179 Node child
= children
.nextNode();
180 String label
= Jcr
.getTitle(child
);
181 csvWriter
.writeLine(new String
[] { child
.getIdentifier(), label
});
189 public void setRepository(Repository repository
) {
190 this.repository
= repository
;