1 package org
.argeo
.cms
.integration
;
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
;
14 import java
.util
.TreeMap
;
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
;
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
;
39 import com
.fasterxml
.jackson
.core
.JsonGenerator
;
40 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
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);
47 protected final static String ACCEPT_HTTP_HEADER
= "Accept";
48 protected final static String CONTENT_DISPOSITION_HTTP_HEADER
= "Content-Disposition";
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";
54 private final static String PARAM_VERBOSE
= "verbose";
55 private final static String PARAM_DEPTH
= "depth";
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";
62 protected final static String _JCR
= "_jcr";
63 protected final static String JCR_PREFIX
= "jcr:";
64 protected final static String REP_PREFIX
= "rep:";
66 private Repository repository
;
67 private Integer maxDepth
= 8;
69 private ObjectMapper objectMapper
= new ObjectMapper();
72 protected void doGet(HttpServletRequest req
, HttpServletResponse resp
) throws ServletException
, IOException
{
73 if (log
.isTraceEnabled())
74 log
.trace("Data service: " + req
.getPathInfo());
76 String dataWorkspace
= getWorkspace(req
);
77 String jcrPath
= getJcrPath(req
);
79 boolean verbose
= req
.getParameter(PARAM_VERBOSE
) != null && !req
.getParameter(PARAM_VERBOSE
).equals("false");
81 if (req
.getParameter(PARAM_DEPTH
) != null) {
82 depth
= Integer
.parseInt(req
.getParameter(PARAM_DEPTH
));
84 throw new RuntimeException("Depth " + depth
+ " is higher than maximum " + maxDepth
);
87 Session session
= null;
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
);
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());
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);
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
);
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();
121 } catch (Exception e
) {
122 new CmsExceptionsChain(e
).writeAsJson(getObjectMapper(), resp
);
124 JcrUtils
.logoutQuietly(session
);
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
);
133 return Subject
.doAs(subject
, new PrivilegedExceptionAction
<Session
>() {
136 public Session
run() throws RepositoryException
{
137 return repository
.login(workspace
);
141 } catch (PrivilegedActionException e
) {
142 if (e
.getException() instanceof RepositoryException
)
143 throw (RepositoryException
) e
.getException();
145 throw new RuntimeException(e
.getException());
147 // return workspace != null ? repository.login(workspace) : repository.login();
150 protected String
getWorkspace(HttpServletRequest req
) {
151 String path
= req
.getPathInfo();
153 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
154 } catch (UnsupportedEncodingException e
) {
155 throw new IllegalArgumentException(e
);
157 String
[] pathTokens
= path
.split("/");
158 return pathTokens
[1];
161 protected String
getJcrPath(HttpServletRequest req
) {
162 String path
= req
.getPathInfo();
164 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
165 } catch (UnsupportedEncodingException e
) {
166 throw new IllegalArgumentException(e
);
168 String
[] pathTokens
= path
.split("/");
169 String domain
= pathTokens
[1];
170 String jcrPath
= path
.substring(domain
.length() + 1);
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)
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();
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
<>();
196 PropertyIterator pit
= node
.getProperties();
197 properties
: while (pit
.hasNext()) {
198 Property property
= pit
.nextProperty();
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
);
215 if (property
.getType() == PropertyType
.BINARY
) {
216 if (!(node
instanceof JackrabbitNode
)) {
217 continue properties
;// skip
221 writeProperty(propertyName
, property
, jsonGenerator
);
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
);
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
239 writeProperty(unqualifiedName
, property
, jsonGenerator
);
241 jsonGenerator
.writeEndObject();
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
);
251 jsonGenerator
.writeFieldName(fieldName
);
252 jsonGenerator
.writeStartArray();
253 Value
[] values
= property
.getValues();
254 for (Value value
: values
) {
255 writePropertyValue(property
.getType(), value
, jsonGenerator
);
257 jsonGenerator
.writeEndArray();
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
);
272 // TODO write Base64 ?
273 jsonGenerator
.writeNull();
276 jsonGenerator
.writeString(value
.getString());
279 protected void writeNodeChildren(Node node
, JsonGenerator jsonGenerator
, int depth
, boolean verbose
)
280 throws RepositoryException
, IOException
{
281 if (!node
.hasNodes())
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
294 jsonGenerator
.writeFieldName(child
.getName());
295 jsonGenerator
.writeStartObject();
296 writeNodeChildren(child
, jsonGenerator
, depth
- 1, verbose
);
297 writeNodeProperties(child
, jsonGenerator
, verbose
);
298 jsonGenerator
.writeEndObject();
302 public void setRepository(Repository repository
) {
303 this.repository
= repository
;
306 public void setMaxDepth(Integer maxDepth
) {
307 this.maxDepth
= maxDepth
;
310 protected Repository
getRepository() {
314 protected ObjectMapper
getObjectMapper() {