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
.commons
.logging
.Log
;
34 import org
.apache
.commons
.logging
.LogFactory
;
35 import org
.apache
.jackrabbit
.api
.JackrabbitNode
;
36 import org
.apache
.jackrabbit
.api
.JackrabbitValue
;
37 import org
.argeo
.jcr
.JcrUtils
;
38 import org
.osgi
.service
.http
.context
.ServletContextHelper
;
40 import com
.fasterxml
.jackson
.core
.JsonGenerator
;
41 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
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 Log log
= LogFactory
.getLog(JcrReadServlet
.class);
48 protected final static String ACCEPT_HTTP_HEADER
= "Accept";
49 protected final static String CONTENT_DISPOSITION_HTTP_HEADER
= "Content-Disposition";
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";
55 private final static String PARAM_VERBOSE
= "verbose";
56 private final static String PARAM_DEPTH
= "depth";
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";
63 protected final static String _JCR
= "_jcr";
64 protected final static String JCR_PREFIX
= "jcr:";
65 protected final static String REP_PREFIX
= "rep:";
67 private Repository repository
;
68 private Integer maxDepth
= 8;
70 private ObjectMapper objectMapper
= new ObjectMapper();
73 protected void doGet(HttpServletRequest req
, HttpServletResponse resp
) throws ServletException
, IOException
{
74 if (log
.isTraceEnabled())
75 log
.trace("Data service: " + req
.getPathInfo());
77 String dataWorkspace
= getWorkspace(req
);
78 String jcrPath
= getJcrPath(req
);
80 boolean verbose
= req
.getParameter(PARAM_VERBOSE
) != null && !req
.getParameter(PARAM_VERBOSE
).equals("false");
82 if (req
.getParameter(PARAM_DEPTH
) != null) {
83 depth
= Integer
.parseInt(req
.getParameter(PARAM_DEPTH
));
85 throw new RuntimeException("Depth " + depth
+ " is higher than maximum " + maxDepth
);
88 Session session
= null;
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
);
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());
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);
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
);
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();
122 } catch (Exception e
) {
123 new CmsExceptionsChain(e
).writeAsJson(getObjectMapper(), resp
);
125 JcrUtils
.logoutQuietly(session
);
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
);
134 return Subject
.doAs(subject
, new PrivilegedExceptionAction
<Session
>() {
137 public Session
run() throws RepositoryException
{
138 return repository
.login(workspace
);
142 } catch (PrivilegedActionException e
) {
143 if (e
.getException() instanceof RepositoryException
)
144 throw (RepositoryException
) e
.getException();
146 throw new RuntimeException(e
.getException());
148 // return workspace != null ? repository.login(workspace) : repository.login();
151 protected String
getWorkspace(HttpServletRequest req
) {
152 String path
= req
.getPathInfo();
154 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
155 } catch (UnsupportedEncodingException e
) {
156 throw new IllegalArgumentException(e
);
158 String
[] pathTokens
= path
.split("/");
159 return pathTokens
[1];
162 protected String
getJcrPath(HttpServletRequest req
) {
163 String path
= req
.getPathInfo();
165 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
166 } catch (UnsupportedEncodingException e
) {
167 throw new IllegalArgumentException(e
);
169 String
[] pathTokens
= path
.split("/");
170 String domain
= pathTokens
[1];
171 String jcrPath
= path
.substring(domain
.length() + 1);
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)
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();
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
<>();
197 PropertyIterator pit
= node
.getProperties();
198 properties
: while (pit
.hasNext()) {
199 Property property
= pit
.nextProperty();
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
);
216 if (property
.getType() == PropertyType
.BINARY
) {
217 if (!(node
instanceof JackrabbitNode
)) {
218 continue properties
;// skip
222 writeProperty(propertyName
, property
, jsonGenerator
);
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
);
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
240 writeProperty(unqualifiedName
, property
, jsonGenerator
);
242 jsonGenerator
.writeEndObject();
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
);
252 jsonGenerator
.writeFieldName(fieldName
);
253 jsonGenerator
.writeStartArray();
254 Value
[] values
= property
.getValues();
255 for (Value value
: values
) {
256 writePropertyValue(property
.getType(), value
, jsonGenerator
);
258 jsonGenerator
.writeEndArray();
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
);
273 // TODO write Base64 ?
274 jsonGenerator
.writeNull();
277 jsonGenerator
.writeString(value
.getString());
280 protected void writeNodeChildren(Node node
, JsonGenerator jsonGenerator
, int depth
, boolean verbose
)
281 throws RepositoryException
, IOException
{
282 if (!node
.hasNodes())
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
295 jsonGenerator
.writeFieldName(child
.getName());
296 jsonGenerator
.writeStartObject();
297 writeNodeChildren(child
, jsonGenerator
, depth
- 1, verbose
);
298 writeNodeProperties(child
, jsonGenerator
, verbose
);
299 jsonGenerator
.writeEndObject();
303 public void setRepository(Repository repository
) {
304 this.repository
= repository
;
307 public void setMaxDepth(Integer maxDepth
) {
308 this.maxDepth
= maxDepth
;
311 protected Repository
getRepository() {
315 protected ObjectMapper
getObjectMapper() {