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