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