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
.util
.ArrayList
;
8 import java
.util
.LinkedHashMap
;
11 import java
.util
.TreeMap
;
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
;
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
;
35 import com
.fasterxml
.jackson
.core
.JsonGenerator
;
36 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
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);
43 protected final static String ACCEPT_HTTP_HEADER
= "Accept";
44 protected final static String CONTENT_DISPOSITION_HTTP_HEADER
= "Content-Disposition";
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";
50 private final static String PARAM_VERBOSE
= "verbose";
51 private final static String PARAM_DEPTH
= "depth";
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";
58 protected final static String _JCR
= "_jcr";
59 protected final static String JCR_PREFIX
= "jcr:";
60 protected final static String REP_PREFIX
= "rep:";
62 private Repository repository
;
63 private Integer maxDepth
= 8;
65 private ObjectMapper objectMapper
= new ObjectMapper();
68 protected void doGet(HttpServletRequest req
, HttpServletResponse resp
) throws ServletException
, IOException
{
69 if (log
.isTraceEnabled())
70 log
.trace("Data service: " + req
.getPathInfo());
72 String dataWorkspace
= getWorkspace(req
);
73 String jcrPath
= getJcrPath(req
);
75 boolean verbose
= req
.getParameter(PARAM_VERBOSE
) != null && !req
.getParameter(PARAM_VERBOSE
).equals("false");
77 if (req
.getParameter(PARAM_DEPTH
) != null) {
78 depth
= Integer
.parseInt(req
.getParameter(PARAM_DEPTH
));
80 throw new RuntimeException("Depth " + depth
+ " is higher than maximum " + maxDepth
);
83 Session session
= null;
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
);
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());
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);
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
);
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();
116 } catch (Exception e
) {
117 new CmsExceptionsChain(e
).writeAsJson(getObjectMapper(), resp
);
119 JcrUtils
.logoutQuietly(session
);
123 protected Session
openJcrSession(HttpServletRequest req
, HttpServletResponse resp
, Repository repository
,
124 String workspace
) throws RepositoryException
{
125 return workspace
!= null ? repository
.login(workspace
) : repository
.login();
128 protected String
getWorkspace(HttpServletRequest req
) {
129 String path
= req
.getPathInfo();
131 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
132 } catch (UnsupportedEncodingException e
) {
133 throw new IllegalArgumentException(e
);
135 String
[] pathTokens
= path
.split("/");
136 return pathTokens
[1];
139 protected String
getJcrPath(HttpServletRequest req
) {
140 String path
= req
.getPathInfo();
142 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
143 } catch (UnsupportedEncodingException e
) {
144 throw new IllegalArgumentException(e
);
146 String
[] pathTokens
= path
.split("/");
147 String domain
= pathTokens
[1];
148 String jcrPath
= path
.substring(domain
.length() + 1);
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)
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();
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
<>();
174 PropertyIterator pit
= node
.getProperties();
175 properties
: while (pit
.hasNext()) {
176 Property property
= pit
.nextProperty();
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
);
193 if (property
.getType() == PropertyType
.BINARY
) {
194 if (!(node
instanceof JackrabbitNode
)) {
195 continue properties
;// skip
199 writeProperty(propertyName
, property
, jsonGenerator
);
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
);
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
217 writeProperty(unqualifiedName
, property
, jsonGenerator
);
219 jsonGenerator
.writeEndObject();
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
);
229 jsonGenerator
.writeFieldName(fieldName
);
230 jsonGenerator
.writeStartArray();
231 Value
[] values
= property
.getValues();
232 for (Value value
: values
) {
233 writePropertyValue(property
.getType(), value
, jsonGenerator
);
235 jsonGenerator
.writeEndArray();
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
);
250 // TODO write Base64 ?
251 jsonGenerator
.writeNull();
254 jsonGenerator
.writeString(value
.getString());
257 protected void writeNodeChildren(Node node
, JsonGenerator jsonGenerator
, int depth
, boolean verbose
)
258 throws RepositoryException
, IOException
{
259 if (!node
.hasNodes())
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
272 jsonGenerator
.writeFieldName(child
.getName());
273 jsonGenerator
.writeStartObject();
274 writeNodeChildren(child
, jsonGenerator
, depth
- 1, verbose
);
275 writeNodeProperties(child
, jsonGenerator
, verbose
);
276 jsonGenerator
.writeEndObject();
280 public void setRepository(Repository repository
) {
281 this.repository
= repository
;
284 public void setMaxDepth(Integer maxDepth
) {
285 this.maxDepth
= maxDepth
;
288 protected Repository
getRepository() {
292 protected ObjectMapper
getObjectMapper() {