]> git.argeo.org Git - gpl/argeo-jcr.git/blob - org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java
864fbfb7eb8efead51c7475d7b0ce8453ba15fc3
[gpl/argeo-jcr.git] / org.argeo.cms.jcr / src / org / argeo / cms / jcr / acr / JcrContent.java
1 package org.argeo.cms.jcr.acr;
2
3 import java.io.Closeable;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.PipedInputStream;
7 import java.io.PipedOutputStream;
8 import java.time.Instant;
9 import java.util.ArrayList;
10 import java.util.Calendar;
11 import java.util.Date;
12 import java.util.GregorianCalendar;
13 import java.util.HashSet;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Optional;
17 import java.util.Set;
18 import java.util.TreeSet;
19 import java.util.UUID;
20 import java.util.concurrent.ForkJoinPool;
21
22 import javax.jcr.Node;
23 import javax.jcr.NodeIterator;
24 import javax.jcr.Property;
25 import javax.jcr.PropertyIterator;
26 import javax.jcr.PropertyType;
27 import javax.jcr.RepositoryException;
28 import javax.jcr.Session;
29 import javax.jcr.Value;
30 import javax.jcr.ValueFactory;
31 import javax.jcr.nodetype.NodeType;
32 import javax.jcr.nodetype.NodeTypeManager;
33 import javax.xml.namespace.QName;
34 import javax.xml.transform.Source;
35 import javax.xml.transform.stream.StreamSource;
36
37 import org.argeo.api.acr.Content;
38 import org.argeo.api.acr.CrAttributeType;
39 import org.argeo.api.acr.NamespaceUtils;
40 import org.argeo.api.acr.spi.ProvidedSession;
41 import org.argeo.api.cms.CmsConstants;
42 import org.argeo.cms.acr.AbstractContent;
43 import org.argeo.cms.acr.ContentUtils;
44 import org.argeo.jcr.Jcr;
45 import org.argeo.jcr.JcrException;
46 import org.argeo.jcr.JcrUtils;
47
48 /** A JCR {@link Node} accessed as {@link Content}. */
49 public class JcrContent extends AbstractContent {
50 private JcrContentProvider provider;
51
52 private String jcrWorkspace;
53 private String jcrPath;
54
55 private final boolean isMountBase;
56
57 protected JcrContent(ProvidedSession session, JcrContentProvider provider, String jcrWorkspace, String jcrPath) {
58 super(session);
59 this.provider = provider;
60 this.jcrWorkspace = jcrWorkspace;
61 this.jcrPath = jcrPath;
62
63 this.isMountBase = ContentUtils.SLASH_STRING.equals(jcrPath);
64 }
65
66 /*
67 * READ
68 */
69
70 @Override
71 public QName getName() {
72 String name = Jcr.getName(getJcrNode());
73 if (name.equals("")) {// root
74 String mountPath = provider.getMountPath();
75 name = ContentUtils.getParentPath(mountPath)[1];
76 // name = Jcr.getWorkspaceName(getJcrNode());
77 }
78 return NamespaceUtils.parsePrefixedName(provider, name);
79 }
80
81 // @SuppressWarnings("unchecked")
82 @Override
83 public <A> Optional<A> get(QName key, Class<A> clss) {
84 Object value = get(getJcrNode(), key.toString());
85 return CrAttributeType.cast(clss, value);
86 }
87
88 @Override
89 public Iterator<Content> iterator() {
90 try {
91 return new JcrContentIterator(getJcrNode().getNodes());
92 } catch (RepositoryException e) {
93 throw new JcrException("Cannot list children of " + getJcrNode(), e);
94 }
95 }
96
97 @Override
98 protected Iterable<QName> keys() {
99 try {
100 Set<QName> keys = new HashSet<>();
101 for (PropertyIterator propertyIterator = getJcrNode().getProperties(); propertyIterator.hasNext();) {
102 Property property = propertyIterator.nextProperty();
103 // TODO convert standard names
104 // TODO skip technical properties
105 QName name = NamespaceUtils.parsePrefixedName(provider, property.getName());
106 keys.add(name);
107 }
108 return keys;
109 } catch (RepositoryException e) {
110 throw new JcrException("Cannot list properties of " + getJcrNode(), e);
111 }
112 }
113
114 /** Cast to a standard Java object. */
115 static Object get(Node node, String property) {
116 try {
117 if (!node.hasProperty(property))
118 return null;
119 Property p = node.getProperty(property);
120 if (p.isMultiple()) {
121 Value[] values = p.getValues();
122 List<Object> lst = new ArrayList<>();
123 for (Value value : values) {
124 lst.add(convertSingleValue(value));
125 }
126 return lst;
127 } else {
128 Value value = node.getProperty(property).getValue();
129 return convertSingleValue(value);
130 }
131 } catch (RepositoryException e) {
132 throw new JcrException("Cannot cast value from " + property + " of " + node, e);
133 }
134 }
135
136 @Override
137 public boolean isMultiple(QName key) {
138 Node node = getJcrNode();
139 String p = NamespaceUtils.toFullyQualified(key);
140 try {
141 if (node.hasProperty(p)) {
142 Property property = node.getProperty(p);
143 return property.isMultiple();
144 } else {
145 return false;
146 }
147 } catch (RepositoryException e) {
148 throw new JcrException(
149 "Cannot check multiplicityof property " + p + " of " + jcrPath + " in " + jcrWorkspace, e);
150 }
151 }
152
153 @Override
154 public String getPath() {
155 try {
156 // Note: it is important to to use the default way (recursing through parents),
157 // since the session may not have access to parent nodes
158 return ContentUtils.ROOT_SLASH + jcrWorkspace + getJcrNode().getPath();
159 } catch (RepositoryException e) {
160 throw new JcrException("Cannot get depth of " + getJcrNode(), e);
161 }
162 }
163
164 @Override
165 public int getDepth() {
166 try {
167 return getJcrNode().getDepth() + 1;
168 } catch (RepositoryException e) {
169 throw new JcrException("Cannot get depth of " + getJcrNode(), e);
170 }
171 }
172
173 @Override
174 public Content getParent() {
175 if (isMountBase) {
176 String mountPath = provider.getMountPath();
177 if (mountPath == null || mountPath.equals("/"))
178 return null;
179 String[] parent = ContentUtils.getParentPath(mountPath);
180 return getSession().get(parent[0]);
181 }
182 // if (Jcr.isRoot(getJcrNode())) // root
183 // return null;
184 return new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getParentPath(getJcrNode()));
185 }
186
187 @Override
188 public int getSiblingIndex() {
189 return Jcr.getIndex(getJcrNode());
190 }
191
192 /*
193 * WRITE
194 */
195
196 protected Node openForEdit() {
197 Node node = getProvider().openForEdit(getSession(), jcrWorkspace, jcrPath);
198 getSession().notifyModification(this);
199 return node;
200 }
201
202 @Override
203 public Content add(QName name, QName... classes) {
204 if (classes.length > 0) {
205 QName primaryType = classes[0];
206 Node node = openForEdit();
207 Node child = Jcr.addNode(node, name.toString(), primaryType.toString());
208 for (int i = 1; i < classes.length; i++) {
209 try {
210 child.addMixin(classes[i].toString());
211 } catch (RepositoryException e) {
212 throw new JcrException("Cannot add child to " + getJcrNode(), e);
213 }
214 }
215
216 } else {
217 Jcr.addNode(getJcrNode(), name.toString(), NodeType.NT_UNSTRUCTURED);
218 }
219 return null;
220 }
221
222 @Override
223 public void remove() {
224 Node node = openForEdit();
225 Jcr.remove(node);
226 }
227
228 @Override
229 protected void removeAttr(QName key) {
230 Node node = openForEdit();
231 Property property = Jcr.getProperty(node, key.toString());
232 if (property != null) {
233 try {
234 property.remove();
235 } catch (RepositoryException e) {
236 throw new JcrException("Cannot remove property " + key + " from " + getJcrNode(), e);
237 }
238 }
239
240 }
241
242 @Override
243 public Object put(QName key, Object value) {
244 try {
245 String property = NamespaceUtils.toFullyQualified(key);
246 Node node = openForEdit();
247 Object old = null;
248 if (node.hasProperty(property)) {
249 old = convertSingleValue(node.getProperty(property).getValue());
250 }
251 Value newValue = convertSingleObject(node.getSession().getValueFactory(), value);
252 node.setProperty(property, newValue);
253 // FIXME proper edition
254 node.getSession().save();
255 return old;
256 } catch (RepositoryException e) {
257 throw new JcrException("Cannot set property " + key + " on " + jcrPath + " in " + jcrWorkspace, e);
258 }
259 }
260
261 @Override
262 public void addContentClasses(QName... contentClass) throws IllegalArgumentException, JcrException {
263 try {
264 Node node = openForEdit();
265 NodeTypeManager ntm = node.getSession().getWorkspace().getNodeTypeManager();
266 List<NodeType> nodeTypes = new ArrayList<>();
267 for (QName clss : contentClass) {
268 NodeType nodeType = ntm.getNodeType(NamespaceUtils.toFullyQualified(clss));
269 if (!nodeType.isMixin())
270 throw new IllegalArgumentException(clss + " is not a mixin");
271 nodeTypes.add(nodeType);
272 }
273 for (NodeType nodeType : nodeTypes) {
274 node.addMixin(nodeType.getName());
275 }
276 // FIXME proper edition
277 node.getSession().save();
278 } catch (RepositoryException e) {
279 throw new JcrException(
280 "Cannot add content classes " + contentClass + " to " + jcrPath + " in " + jcrWorkspace, e);
281 }
282 }
283
284 /*
285 * ACCESS
286 */
287 protected boolean exists() {
288 try {
289 return getJcrSession().itemExists(jcrPath);
290 } catch (RepositoryException e) {
291 throw new JcrException("Cannot check whether " + jcrPath + " exists", e);
292 }
293 }
294
295 @Override
296 public boolean isParentAccessible() {
297 String jcrParentPath = ContentUtils.getParentPath(jcrPath)[0];
298 if ("".equals(jcrParentPath)) // JCR root node
299 jcrParentPath = ContentUtils.SLASH_STRING;
300 try {
301 return getJcrSession().hasPermission(jcrParentPath, Session.ACTION_READ);
302 } catch (RepositoryException e) {
303 throw new JcrException("Cannot check whether parent " + jcrParentPath + " is accessible", e);
304 }
305 }
306
307 /*
308 * ADAPTERS
309 */
310 @SuppressWarnings("unchecked")
311 public <A> A adapt(Class<A> clss) {
312 if (Node.class.isAssignableFrom(clss)) {
313 return (A) getJcrNode();
314 } else if (Source.class.isAssignableFrom(clss)) {
315 // try {
316 PipedOutputStream out = new PipedOutputStream();
317 PipedInputStream in;
318 try {
319 in = new PipedInputStream(out);
320 } catch (IOException e) {
321 throw new RuntimeException("Cannot export " + jcrPath + " in workspace " + jcrWorkspace, e);
322 }
323
324 ForkJoinPool.commonPool().execute(() -> {
325 // try (PipedOutputStream out = new PipedOutputStream(in)) {
326 try {
327 getJcrSession().exportDocumentView(jcrPath, out, true, false);
328 out.flush();
329 out.close();
330 } catch (IOException | RepositoryException e) {
331 throw new RuntimeException("Cannot export " + jcrPath + " in workspace " + jcrWorkspace, e);
332 }
333
334 });
335 return (A) new StreamSource(in);
336 // } catch (IOException e) {
337 // throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e);
338 // }
339 } else {
340 return super.adapt(clss);
341 }
342 }
343
344 @SuppressWarnings("unchecked")
345 @Override
346 public <C extends Closeable> C open(Class<C> clss) throws IOException, IllegalArgumentException {
347 if (InputStream.class.isAssignableFrom(clss)) {
348 Node node = getJcrNode();
349 if (Jcr.isNodeType(node, NodeType.NT_FILE)) {
350 try {
351 return (C) JcrUtils.getFileAsStream(node);
352 } catch (RepositoryException e) {
353 throw new JcrException("Cannot open " + jcrPath + " in workspace " + jcrWorkspace, e);
354 }
355 }
356 }
357 return super.open(clss);
358 }
359
360 @Override
361 public JcrContentProvider getProvider() {
362 return provider;
363 }
364
365 @Override
366 public String getSessionLocalId() {
367 try {
368 return getJcrNode().getIdentifier();
369 } catch (RepositoryException e) {
370 throw new JcrException("Cannot get identifier for " + getJcrNode(), e);
371 }
372 }
373
374 /*
375 * TYPING
376 */
377
378 static Object convertSingleValue(Value value) throws JcrException, IllegalArgumentException {
379 try {
380 switch (value.getType()) {
381 case PropertyType.STRING:
382 return value.getString();
383 case PropertyType.DOUBLE:
384 return (Double) value.getDouble();
385 case PropertyType.LONG:
386 return (Long) value.getLong();
387 case PropertyType.BOOLEAN:
388 return (Boolean) value.getBoolean();
389 case PropertyType.DATE:
390 Calendar calendar = value.getDate();
391 return calendar.toInstant();
392 case PropertyType.BINARY:
393 throw new IllegalArgumentException("Binary is not supported as an attribute");
394 default:
395 return value.getString();
396 }
397 } catch (RepositoryException e) {
398 throw new JcrException("Cannot convert " + value + " to an object.", e);
399 }
400 }
401
402 static Value convertSingleObject(ValueFactory factory, Object value) {
403 if (value instanceof String string) {
404 return factory.createValue(string);
405 } else if (value instanceof Double dbl) {
406 return factory.createValue(dbl);
407 } else if (value instanceof Float flt) {
408 return factory.createValue(flt);
409 } else if (value instanceof Long lng) {
410 return factory.createValue(lng);
411 } else if (value instanceof Integer intg) {
412 return factory.createValue(intg);
413 } else if (value instanceof Boolean bool) {
414 return factory.createValue(bool);
415 } else if (value instanceof Instant instant) {
416 GregorianCalendar calendar = new GregorianCalendar();
417 calendar.setTime(Date.from(instant));
418 return factory.createValue(calendar);
419 } else {
420 // TODO or use String by default?
421 throw new IllegalArgumentException("Unsupported value " + value.getClass());
422 }
423 }
424
425 @Override
426 public Class<?> getType(QName key) {
427 Node node = getJcrNode();
428 String p = NamespaceUtils.toFullyQualified(key);
429 try {
430 if (node.hasProperty(p)) {
431 Property property = node.getProperty(p);
432 return switch (property.getType()) {
433 case PropertyType.STRING:
434 case PropertyType.NAME:
435 case PropertyType.PATH:
436 case PropertyType.DECIMAL:
437 yield String.class;
438 case PropertyType.LONG:
439 yield Long.class;
440 case PropertyType.DOUBLE:
441 yield Double.class;
442 case PropertyType.BOOLEAN:
443 yield Boolean.class;
444 case PropertyType.DATE:
445 yield Instant.class;
446 case PropertyType.WEAKREFERENCE:
447 case PropertyType.REFERENCE:
448 yield UUID.class;
449 default:
450 yield Object.class;
451 };
452 } else {
453 // TODO does it make sense?
454 return Object.class;
455 }
456 } catch (RepositoryException e) {
457 throw new JcrException("Cannot get type of property " + p + " of " + jcrPath + " in " + jcrWorkspace, e);
458 }
459 }
460
461 @Override
462 public List<QName> getContentClasses() {
463 try {
464 Node context = getJcrNode();
465
466 List<QName> res = new ArrayList<>();
467 // primary node type
468 NodeType primaryType = context.getPrimaryNodeType();
469 res.add(nodeTypeToQName(primaryType));
470
471 Set<QName> secondaryTypes = new TreeSet<>(NamespaceUtils.QNAME_COMPARATOR);
472 for (NodeType mixinType : context.getMixinNodeTypes()) {
473 secondaryTypes.add(nodeTypeToQName(mixinType));
474 }
475 for (NodeType superType : primaryType.getDeclaredSupertypes()) {
476 secondaryTypes.add(nodeTypeToQName(superType));
477 }
478 // mixins
479 for (NodeType mixinType : context.getMixinNodeTypes()) {
480 for (NodeType superType : mixinType.getDeclaredSupertypes()) {
481 secondaryTypes.add(nodeTypeToQName(superType));
482 }
483 }
484 res.addAll(secondaryTypes);
485 return res;
486 } catch (RepositoryException e) {
487 throw new JcrException("Cannot list node types from " + getJcrNode(), e);
488 }
489 }
490
491 private QName nodeTypeToQName(NodeType nodeType) {
492 String name = nodeType.getName();
493 return NamespaceUtils.parsePrefixedName(provider, name);
494 // return QName.valueOf(name);
495 }
496
497 /*
498 * COMMON UTILITIES
499 */
500 protected Session getJcrSession() {
501 return provider.getJcrSession(getSession(), jcrWorkspace);
502 }
503
504 protected Node getJcrNode() {
505 try {
506 // TODO caching?
507 return getJcrSession().getNode(jcrPath);
508 } catch (RepositoryException e) {
509 throw new JcrException("Cannot retrieve " + jcrPath + " from workspace " + jcrWorkspace, e);
510 }
511 }
512
513 /*
514 * STATIC UTLITIES
515 */
516 public static Content nodeToContent(Node node) {
517 if (node == null)
518 return null;
519 try {
520 ProvidedSession contentSession = (ProvidedSession) node.getSession()
521 .getAttribute(ProvidedSession.class.getName());
522 if (contentSession == null)
523 throw new IllegalArgumentException(
524 "Cannot adapt " + node + " to content, because it was not loaded from a content session");
525 return contentSession.get(ContentUtils.SLASH + CmsConstants.SYS_WORKSPACE + node.getPath());
526 } catch (RepositoryException e) {
527 throw new JcrException("Cannot adapt " + node + " to a content", e);
528 }
529 }
530
531 /*
532 * CONTENT ITERATOR
533 */
534
535 class JcrContentIterator implements Iterator<Content> {
536 private final NodeIterator nodeIterator;
537 // we keep track in order to be able to delete it
538 private JcrContent current = null;
539
540 protected JcrContentIterator(NodeIterator nodeIterator) {
541 this.nodeIterator = nodeIterator;
542 }
543
544 @Override
545 public boolean hasNext() {
546 return nodeIterator.hasNext();
547 }
548
549 @Override
550 public Content next() {
551 current = new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode()));
552 return current;
553 }
554
555 @Override
556 public void remove() {
557 if (current != null) {
558 Jcr.remove(current.getJcrNode());
559 }
560 }
561
562 }
563
564 }