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