]> git.argeo.org Git - lgpl/argeo-commons.git/blob - argeo/cms/integration/JcrReadServlet.java
Prepare next development cycle
[lgpl/argeo-commons.git] / argeo / cms / integration / JcrReadServlet.java
1 package org.argeo.cms.integration;
2
3 import java.io.IOException;
4 import java.io.UnsupportedEncodingException;
5 import java.net.URLDecoder;
6 import java.nio.charset.StandardCharsets;
7 import java.util.ArrayList;
8 import java.util.LinkedHashMap;
9 import java.util.List;
10 import java.util.Map;
11 import java.util.TreeMap;
12
13 import javax.jcr.Node;
14 import javax.jcr.NodeIterator;
15 import javax.jcr.Property;
16 import javax.jcr.PropertyIterator;
17 import javax.jcr.PropertyType;
18 import javax.jcr.Repository;
19 import javax.jcr.RepositoryException;
20 import javax.jcr.Session;
21 import javax.jcr.Value;
22 import javax.jcr.nodetype.NodeType;
23 import javax.servlet.ServletException;
24 import javax.servlet.http.HttpServlet;
25 import javax.servlet.http.HttpServletRequest;
26 import javax.servlet.http.HttpServletResponse;
27
28 import org.apache.commons.io.IOUtils;
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31 import org.apache.jackrabbit.api.JackrabbitNode;
32 import org.apache.jackrabbit.api.JackrabbitValue;
33 import org.argeo.jcr.JcrUtils;
34
35 import com.fasterxml.jackson.core.JsonGenerator;
36 import com.fasterxml.jackson.databind.ObjectMapper;
37
38 /** Access a JCR repository via web services. */
39 public class JcrReadServlet extends HttpServlet {
40 private static final long serialVersionUID = 6536175260540484539L;
41 private final static Log log = LogFactory.getLog(JcrReadServlet.class);
42
43 protected final static String ACCEPT_HTTP_HEADER = "Accept";
44 protected final static String CONTENT_DISPOSITION_HTTP_HEADER = "Content-Disposition";
45
46 protected final static String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
47 protected final static String XML_CONTENT_TYPE = "application/xml";
48 protected final static String JSON_CONTENT_TYPE = "application/json";
49
50 private final static String PARAM_VERBOSE = "verbose";
51 private final static String PARAM_DEPTH = "depth";
52
53 protected final static String JCR_NODES = "jcr:nodes";
54 // cf. javax.jcr.Property
55 protected final static String JCR_PATH = "path";
56 protected final static String JCR_NAME = "name";
57
58 protected final static String _JCR = "_jcr";
59 protected final static String JCR_PREFIX = "jcr:";
60 protected final static String REP_PREFIX = "rep:";
61
62 private Repository repository;
63 private Integer maxDepth = 8;
64
65 private ObjectMapper objectMapper = new ObjectMapper();
66
67 @Override
68 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
69 if (log.isTraceEnabled())
70 log.trace("Data service: " + req.getPathInfo());
71
72 String dataWorkspace = getWorkspace(req);
73 String jcrPath = getJcrPath(req);
74
75 boolean verbose = req.getParameter(PARAM_VERBOSE) != null && !req.getParameter(PARAM_VERBOSE).equals("false");
76 int depth = 1;
77 if (req.getParameter(PARAM_DEPTH) != null) {
78 depth = Integer.parseInt(req.getParameter(PARAM_DEPTH));
79 if (depth > maxDepth)
80 throw new RuntimeException("Depth " + depth + " is higher than maximum " + maxDepth);
81 }
82
83 Session session = null;
84 try {
85 // authentication
86 session = openJcrSession(req, resp, getRepository(), dataWorkspace);
87 if (!session.itemExists(jcrPath))
88 throw new RuntimeException("JCR node " + jcrPath + " does not exist");
89 Node node = session.getNode(jcrPath);
90
91 List<String> acceptHeader = readAcceptHeader(req);
92 if (!acceptHeader.isEmpty() && node.isNodeType(NodeType.NT_FILE)) {
93 resp.setContentType(OCTET_STREAM_CONTENT_TYPE);
94 resp.addHeader(CONTENT_DISPOSITION_HTTP_HEADER, "attachment; filename='" + node.getName() + "'");
95 IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream());
96 resp.flushBuffer();
97 } else {
98 if (!acceptHeader.isEmpty() && acceptHeader.get(0).equals(XML_CONTENT_TYPE)) {
99 // TODO Use req.startAsync(); ?
100 resp.setContentType(XML_CONTENT_TYPE);
101 session.exportSystemView(node.getPath(), resp.getOutputStream(), false, depth <= 1);
102 return;
103 }
104 if (!acceptHeader.isEmpty() && !acceptHeader.contains(JSON_CONTENT_TYPE)) {
105 log.warn("Content type " + acceptHeader + " in Accept header is not supported. Supported: "
106 + JSON_CONTENT_TYPE + " (default), " + XML_CONTENT_TYPE);
107 }
108 resp.setContentType(JSON_CONTENT_TYPE);
109 JsonGenerator jsonGenerator = getObjectMapper().getFactory().createGenerator(resp.getWriter());
110 jsonGenerator.writeStartObject();
111 writeNodeChildren(node, jsonGenerator, depth, verbose);
112 writeNodeProperties(node, jsonGenerator, verbose);
113 jsonGenerator.writeEndObject();
114 jsonGenerator.flush();
115 }
116 } catch (Exception e) {
117 new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp);
118 } finally {
119 JcrUtils.logoutQuietly(session);
120 }
121 }
122
123 protected Session openJcrSession(HttpServletRequest req, HttpServletResponse resp, Repository repository,
124 String workspace) throws RepositoryException {
125 return workspace != null ? repository.login(workspace) : repository.login();
126 }
127
128 protected String getWorkspace(HttpServletRequest req) {
129 String path = req.getPathInfo();
130 try {
131 path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
132 } catch (UnsupportedEncodingException e) {
133 throw new IllegalArgumentException(e);
134 }
135 String[] pathTokens = path.split("/");
136 return pathTokens[1];
137 }
138
139 protected String getJcrPath(HttpServletRequest req) {
140 String path = req.getPathInfo();
141 try {
142 path = URLDecoder.decode(path, StandardCharsets.UTF_8.name());
143 } catch (UnsupportedEncodingException e) {
144 throw new IllegalArgumentException(e);
145 }
146 String[] pathTokens = path.split("/");
147 String domain = pathTokens[1];
148 String jcrPath = path.substring(domain.length() + 1);
149 return jcrPath;
150 }
151
152 protected List<String> readAcceptHeader(HttpServletRequest req) {
153 List<String> lst = new ArrayList<>();
154 String acceptHeader = req.getHeader(ACCEPT_HTTP_HEADER);
155 if (acceptHeader == null)
156 return lst;
157 // Enumeration<String> acceptHeader = req.getHeaders(ACCEPT_HTTP_HEADER);
158 // while (acceptHeader.hasMoreElements()) {
159 String[] arr = acceptHeader.split("\\.");
160 for (int i = 0; i < arr.length; i++) {
161 String str = arr[i].trim();
162 if (!"".equals(str))
163 lst.add(str);
164 }
165 // }
166 return lst;
167 }
168
169 protected void writeNodeProperties(Node node, JsonGenerator jsonGenerator, boolean verbose)
170 throws RepositoryException, IOException {
171 String jcrPath = node.getPath();
172 Map<String, Map<String, Property>> namespaces = new TreeMap<>();
173
174 PropertyIterator pit = node.getProperties();
175 properties: while (pit.hasNext()) {
176 Property property = pit.nextProperty();
177
178 final String propertyName = property.getName();
179 int columnIndex = propertyName.indexOf(':');
180 if (columnIndex > 0) {
181 // mark prefix with a '_' before the name of the object, according to JSON
182 // conventions to indicate a special value
183 String prefix = "_" + propertyName.substring(0, columnIndex);
184 String unqualifiedName = propertyName.substring(columnIndex + 1);
185 if (!namespaces.containsKey(prefix))
186 namespaces.put(prefix, new LinkedHashMap<String, Property>());
187 Map<String, Property> map = namespaces.get(prefix);
188 assert !map.containsKey(unqualifiedName);
189 map.put(unqualifiedName, property);
190 continue properties;
191 }
192
193 if (property.getType() == PropertyType.BINARY) {
194 if (!(node instanceof JackrabbitNode)) {
195 continue properties;// skip
196 }
197 }
198
199 writeProperty(propertyName, property, jsonGenerator);
200 }
201
202 for (String prefix : namespaces.keySet()) {
203 Map<String, Property> map = namespaces.get(prefix);
204 jsonGenerator.writeFieldName(prefix);
205 jsonGenerator.writeStartObject();
206 if (_JCR.equals(prefix)) {
207 jsonGenerator.writeStringField(JCR_NAME, node.getName());
208 jsonGenerator.writeStringField(JCR_PATH, jcrPath);
209 }
210 properties: for (String unqualifiedName : map.keySet()) {
211 Property property = map.get(unqualifiedName);
212 if (property.getType() == PropertyType.BINARY) {
213 if (!(node instanceof JackrabbitNode)) {
214 continue properties;// skip
215 }
216 }
217 writeProperty(unqualifiedName, property, jsonGenerator);
218 }
219 jsonGenerator.writeEndObject();
220 }
221 }
222
223 protected void writeProperty(String fieldName, Property property, JsonGenerator jsonGenerator)
224 throws RepositoryException, IOException {
225 if (!property.isMultiple()) {
226 jsonGenerator.writeFieldName(fieldName);
227 writePropertyValue(property.getType(), property.getValue(), jsonGenerator);
228 } else {
229 jsonGenerator.writeFieldName(fieldName);
230 jsonGenerator.writeStartArray();
231 Value[] values = property.getValues();
232 for (Value value : values) {
233 writePropertyValue(property.getType(), value, jsonGenerator);
234 }
235 jsonGenerator.writeEndArray();
236 }
237 }
238
239 protected void writePropertyValue(int type, Value value, JsonGenerator jsonGenerator)
240 throws RepositoryException, IOException {
241 if (type == PropertyType.DOUBLE)
242 jsonGenerator.writeNumber(value.getDouble());
243 else if (type == PropertyType.LONG)
244 jsonGenerator.writeNumber(value.getLong());
245 else if (type == PropertyType.BINARY) {
246 if (value instanceof JackrabbitValue) {
247 String contentIdentity = ((JackrabbitValue) value).getContentIdentity();
248 jsonGenerator.writeString("SHA256:" + contentIdentity);
249 } else {
250 // TODO write Base64 ?
251 jsonGenerator.writeNull();
252 }
253 } else
254 jsonGenerator.writeString(value.getString());
255 }
256
257 protected void writeNodeChildren(Node node, JsonGenerator jsonGenerator, int depth, boolean verbose)
258 throws RepositoryException, IOException {
259 if (!node.hasNodes())
260 return;
261 if (depth <= 0)
262 return;
263 NodeIterator nit;
264
265 nit = node.getNodes();
266 children: while (nit.hasNext()) {
267 Node child = nit.nextNode();
268 if (!verbose && child.getName().startsWith(REP_PREFIX)) {
269 continue children;// skip Jackrabbit auth metadata
270 }
271
272 jsonGenerator.writeFieldName(child.getName());
273 jsonGenerator.writeStartObject();
274 writeNodeChildren(child, jsonGenerator, depth - 1, verbose);
275 writeNodeProperties(child, jsonGenerator, verbose);
276 jsonGenerator.writeEndObject();
277 }
278 }
279
280 public void setRepository(Repository repository) {
281 this.repository = repository;
282 }
283
284 public void setMaxDepth(Integer maxDepth) {
285 this.maxDepth = maxDepth;
286 }
287
288 protected Repository getRepository() {
289 return repository;
290 }
291
292 protected ObjectMapper getObjectMapper() {
293 return objectMapper;
294 }
295
296 }