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 if (log
.isTraceEnabled())
106 log
.warn("Content type " + acceptHeader
+ " in Accept header is not supported. Supported: "
107 + JSON_CONTENT_TYPE
+ " (default), " + XML_CONTENT_TYPE
);
109 resp
.setContentType(JSON_CONTENT_TYPE
);
110 JsonGenerator jsonGenerator
= getObjectMapper().getFactory().createGenerator(resp
.getWriter());
111 jsonGenerator
.writeStartObject();
112 writeNodeChildren(node
, jsonGenerator
, depth
, verbose
);
113 writeNodeProperties(node
, jsonGenerator
, verbose
);
114 jsonGenerator
.writeEndObject();
115 jsonGenerator
.flush();
117 } catch (Exception e
) {
118 new CmsExceptionsChain(e
).writeAsJson(getObjectMapper(), resp
);
120 JcrUtils
.logoutQuietly(session
);
124 protected Session
openJcrSession(HttpServletRequest req
, HttpServletResponse resp
, Repository repository
,
125 String workspace
) throws RepositoryException
{
126 return workspace
!= null ? repository
.login(workspace
) : repository
.login();
129 protected String
getWorkspace(HttpServletRequest req
) {
130 String path
= req
.getPathInfo();
132 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
133 } catch (UnsupportedEncodingException e
) {
134 throw new IllegalArgumentException(e
);
136 String
[] pathTokens
= path
.split("/");
137 return pathTokens
[1];
140 protected String
getJcrPath(HttpServletRequest req
) {
141 String path
= req
.getPathInfo();
143 path
= URLDecoder
.decode(path
, StandardCharsets
.UTF_8
.name());
144 } catch (UnsupportedEncodingException e
) {
145 throw new IllegalArgumentException(e
);
147 String
[] pathTokens
= path
.split("/");
148 String domain
= pathTokens
[1];
149 String jcrPath
= path
.substring(domain
.length() + 1);
153 protected List
<String
> readAcceptHeader(HttpServletRequest req
) {
154 List
<String
> lst
= new ArrayList
<>();
155 String acceptHeader
= req
.getHeader(ACCEPT_HTTP_HEADER
);
156 if (acceptHeader
== null)
158 // Enumeration<String> acceptHeader = req.getHeaders(ACCEPT_HTTP_HEADER);
159 // while (acceptHeader.hasMoreElements()) {
160 String
[] arr
= acceptHeader
.split("\\.");
161 for (int i
= 0; i
< arr
.length
; i
++) {
162 String str
= arr
[i
].trim();
170 protected void writeNodeProperties(Node node
, JsonGenerator jsonGenerator
, boolean verbose
)
171 throws RepositoryException
, IOException
{
172 String jcrPath
= node
.getPath();
173 Map
<String
, Map
<String
, Property
>> namespaces
= new TreeMap
<>();
175 PropertyIterator pit
= node
.getProperties();
176 properties
: while (pit
.hasNext()) {
177 Property property
= pit
.nextProperty();
179 final String propertyName
= property
.getName();
180 int columnIndex
= propertyName
.indexOf(':');
181 if (columnIndex
> 0) {
182 // mark prefix with a '_' before the name of the object, according to JSON
183 // conventions to indicate a special value
184 String prefix
= "_" + propertyName
.substring(0, columnIndex
);
185 String unqualifiedName
= propertyName
.substring(columnIndex
+ 1);
186 if (!namespaces
.containsKey(prefix
))
187 namespaces
.put(prefix
, new LinkedHashMap
<String
, Property
>());
188 Map
<String
, Property
> map
= namespaces
.get(prefix
);
189 assert !map
.containsKey(unqualifiedName
);
190 map
.put(unqualifiedName
, property
);
194 if (property
.getType() == PropertyType
.BINARY
) {
195 if (!(node
instanceof JackrabbitNode
)) {
196 continue properties
;// skip
200 writeProperty(propertyName
, property
, jsonGenerator
);
203 for (String prefix
: namespaces
.keySet()) {
204 Map
<String
, Property
> map
= namespaces
.get(prefix
);
205 jsonGenerator
.writeFieldName(prefix
);
206 jsonGenerator
.writeStartObject();
207 if (_JCR
.equals(prefix
)) {
208 jsonGenerator
.writeStringField(JCR_NAME
, node
.getName());
209 jsonGenerator
.writeStringField(JCR_PATH
, jcrPath
);
211 properties
: for (String unqualifiedName
: map
.keySet()) {
212 Property property
= map
.get(unqualifiedName
);
213 if (property
.getType() == PropertyType
.BINARY
) {
214 if (!(node
instanceof JackrabbitNode
)) {
215 continue properties
;// skip
218 writeProperty(unqualifiedName
, property
, jsonGenerator
);
220 jsonGenerator
.writeEndObject();
224 protected void writeProperty(String fieldName
, Property property
, JsonGenerator jsonGenerator
)
225 throws RepositoryException
, IOException
{
226 if (!property
.isMultiple()) {
227 jsonGenerator
.writeFieldName(fieldName
);
228 writePropertyValue(property
.getType(), property
.getValue(), jsonGenerator
);
230 jsonGenerator
.writeFieldName(fieldName
);
231 jsonGenerator
.writeStartArray();
232 Value
[] values
= property
.getValues();
233 for (Value value
: values
) {
234 writePropertyValue(property
.getType(), value
, jsonGenerator
);
236 jsonGenerator
.writeEndArray();
240 protected void writePropertyValue(int type
, Value value
, JsonGenerator jsonGenerator
)
241 throws RepositoryException
, IOException
{
242 if (type
== PropertyType
.DOUBLE
)
243 jsonGenerator
.writeNumber(value
.getDouble());
244 else if (type
== PropertyType
.LONG
)
245 jsonGenerator
.writeNumber(value
.getLong());
246 else if (type
== PropertyType
.BINARY
) {
247 if (value
instanceof JackrabbitValue
) {
248 String contentIdentity
= ((JackrabbitValue
) value
).getContentIdentity();
249 jsonGenerator
.writeString("SHA256:" + contentIdentity
);
251 // TODO write Base64 ?
252 jsonGenerator
.writeNull();
255 jsonGenerator
.writeString(value
.getString());
258 protected void writeNodeChildren(Node node
, JsonGenerator jsonGenerator
, int depth
, boolean verbose
)
259 throws RepositoryException
, IOException
{
260 if (!node
.hasNodes())
266 nit
= node
.getNodes();
267 children
: while (nit
.hasNext()) {
268 Node child
= nit
.nextNode();
269 if (!verbose
&& child
.getName().startsWith(REP_PREFIX
)) {
270 continue children
;// skip Jackrabbit auth metadata
273 jsonGenerator
.writeFieldName(child
.getName());
274 jsonGenerator
.writeStartObject();
275 writeNodeChildren(child
, jsonGenerator
, depth
- 1, verbose
);
276 writeNodeProperties(child
, jsonGenerator
, verbose
);
277 jsonGenerator
.writeEndObject();
281 public void setRepository(Repository repository
) {
282 this.repository
= repository
;
285 public void setMaxDepth(Integer maxDepth
) {
286 this.maxDepth
= maxDepth
;
289 protected Repository
getRepository() {
293 protected ObjectMapper
getObjectMapper() {