1 package org
.argeo
.cms
.jcr
.acr
;
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
;
18 import java
.util
.TreeSet
;
19 import java
.util
.UUID
;
20 import java
.util
.concurrent
.ForkJoinPool
;
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
;
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
;
48 /** A JCR {@link Node} accessed as {@link Content}. */
49 public class JcrContent
extends AbstractContent
{
50 private JcrContentProvider provider
;
52 private String jcrWorkspace
;
53 private String jcrPath
;
55 private final boolean isMountBase
;
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.
63 private Thread lastRetrievingThread
= null;
64 private Node cachedNode
= null;
65 private boolean caching
= true;
67 protected JcrContent(ProvidedSession session
, JcrContentProvider provider
, String jcrWorkspace
, String jcrPath
) {
69 this.provider
= provider
;
70 this.jcrWorkspace
= jcrWorkspace
;
71 this.jcrPath
= jcrPath
;
73 this.isMountBase
= ContentUtils
.SLASH_STRING
.equals(jcrPath
);
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());
88 return NamespaceUtils
.parsePrefixedName(provider
, name
);
91 @SuppressWarnings("unchecked")
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
);
102 public Iterator
<Content
> iterator() {
104 return new JcrContentIterator(getJcrNode().getNodes());
105 } catch (RepositoryException e
) {
106 throw new JcrException("Cannot list children of " + getJcrNode(), e
);
111 protected Iterable
<QName
> keys() {
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());
122 } catch (RepositoryException e
) {
123 throw new JcrException("Cannot list properties of " + getJcrNode(), e
);
127 /** Cast to a standard Java object. */
128 static Object
get(Node node
, String property
) {
130 if (!node
.hasProperty(property
))
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
));
141 Value value
= node
.getProperty(property
).getValue();
142 return convertSingleValue(value
);
144 } catch (RepositoryException e
) {
145 throw new JcrException("Cannot cast value from " + property
+ " of " + node
, e
);
150 public boolean isMultiple(QName key
) {
151 Node node
= getJcrNode();
152 String p
= NamespaceUtils
.toFullyQualified(key
);
154 if (node
.hasProperty(p
)) {
155 Property property
= node
.getProperty(p
);
156 return property
.isMultiple();
160 } catch (RepositoryException e
) {
161 throw new JcrException(
162 "Cannot check multiplicityof property " + p
+ " of " + jcrPath
+ " in " + jcrWorkspace
, e
);
167 public String
getPath() {
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
);
178 public int getDepth() {
180 return getJcrNode().getDepth() + 1;
181 } catch (RepositoryException e
) {
182 throw new JcrException("Cannot get depth of " + getJcrNode(), e
);
187 public Content
getParent() {
189 String mountPath
= provider
.getMountPath();
190 if (mountPath
== null || mountPath
.equals("/"))
192 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
193 return getSession().get(parent
[0]);
195 // if (Jcr.isRoot(getJcrNode())) // root
197 return new JcrContent(getSession(), provider
, jcrWorkspace
, Jcr
.getParentPath(getJcrNode()));
201 public int getSiblingIndex() {
202 return Jcr
.getIndex(getJcrNode());
209 public boolean containsKey(Object key
) {
210 return Jcr
.hasProperty(getJcrNode(), key
.toString());
217 protected Node
openForEdit() {
218 Node node
= getProvider().openForEdit(getSession(), jcrWorkspace
, jcrPath
);
219 getSession().notifyModification(this);
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
++) {
231 child
.addMixin(classes
[i
].toString());
232 } catch (RepositoryException e
) {
233 throw new JcrException("Cannot add child to " + getJcrNode(), e
);
238 Jcr
.addNode(getJcrNode(), name
.toString(), NodeType
.NT_UNSTRUCTURED
);
244 public void remove() {
245 Node node
= openForEdit();
250 protected void removeAttr(QName key
) {
251 Node node
= openForEdit();
252 Property property
= Jcr
.getProperty(node
, key
.toString());
253 if (property
!= null) {
256 } catch (RepositoryException e
) {
257 throw new JcrException("Cannot remove property " + key
+ " from " + getJcrNode(), e
);
264 public Object
put(QName key
, Object value
) {
266 String property
= NamespaceUtils
.toFullyQualified(key
);
267 Node node
= openForEdit();
269 if (node
.hasProperty(property
)) {
270 old
= convertSingleValue(node
.getProperty(property
).getValue());
272 Value newValue
= convertSingleObject(node
.getSession().getValueFactory(), value
);
273 node
.setProperty(property
, newValue
);
274 // FIXME proper edition
275 node
.getSession().save();
277 } catch (RepositoryException e
) {
278 throw new JcrException("Cannot set property " + key
+ " on " + jcrPath
+ " in " + jcrWorkspace
, e
);
283 public void addContentClasses(QName
... contentClass
) throws IllegalArgumentException
, JcrException
{
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
);
294 for (NodeType nodeType
: nodeTypes
) {
295 node
.addMixin(nodeType
.getName());
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
);
308 protected boolean exists() {
310 return getJcrSession().itemExists(jcrPath
);
311 } catch (RepositoryException e
) {
312 throw new JcrException("Cannot check whether " + jcrPath
+ " exists", e
);
317 public boolean isParentAccessible() {
318 String jcrParentPath
= ContentUtils
.getParentPath(jcrPath
)[0];
319 if ("".equals(jcrParentPath
)) // JCR root node
320 jcrParentPath
= ContentUtils
.SLASH_STRING
;
322 return getJcrSession().hasPermission(jcrParentPath
, Session
.ACTION_READ
);
323 } catch (RepositoryException e
) {
324 throw new JcrException("Cannot check whether parent " + jcrParentPath
+ " is accessible", e
);
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
)) {
337 PipedOutputStream out
= new PipedOutputStream();
340 in
= new PipedInputStream(out
);
341 } catch (IOException e
) {
342 throw new RuntimeException("Cannot export " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
345 ForkJoinPool
.commonPool().execute(() -> {
346 // try (PipedOutputStream out = new PipedOutputStream(in)) {
348 getJcrSession().exportDocumentView(jcrPath
, out
, true, false);
351 } catch (IOException
| RepositoryException e
) {
352 throw new RuntimeException("Cannot export " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
356 return (A
) new StreamSource(in
);
357 // } catch (IOException e) {
358 // throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e);
361 return super.adapt(clss
);
365 @SuppressWarnings("unchecked")
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
)) {
372 return (C
) JcrUtils
.getFileAsStream(node
);
373 } catch (RepositoryException e
) {
374 throw new JcrException("Cannot open " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
378 return super.open(clss
);
382 public JcrContentProvider
getProvider() {
387 public String
getSessionLocalId() {
389 return getJcrNode().getIdentifier();
390 } catch (RepositoryException e
) {
391 throw new JcrException("Cannot get identifier for " + getJcrNode(), e
);
399 static Object
convertSingleValue(Value value
) throws JcrException
, IllegalArgumentException
{
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");
416 return value
.getString();
418 } catch (RepositoryException e
) {
419 throw new JcrException("Cannot convert " + value
+ " to an object.", e
);
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
);
441 // TODO or use String by default?
442 throw new IllegalArgumentException("Unsupported value " + value
.getClass());
447 public Class
<?
> getType(QName key
) {
448 Node node
= getJcrNode();
449 String p
= NamespaceUtils
.toFullyQualified(key
);
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
:
459 case PropertyType
.LONG
:
461 case PropertyType
.DOUBLE
:
463 case PropertyType
.BOOLEAN
:
465 case PropertyType
.DATE
:
467 case PropertyType
.WEAKREFERENCE
:
468 case PropertyType
.REFERENCE
:
474 // TODO does it make sense?
477 } catch (RepositoryException e
) {
478 throw new JcrException("Cannot get type of property " + p
+ " of " + jcrPath
+ " in " + jcrWorkspace
, e
);
483 public List
<QName
> getContentClasses() {
485 Node context
= getJcrNode();
487 List
<QName
> res
= new ArrayList
<>();
489 NodeType primaryType
= context
.getPrimaryNodeType();
490 res
.add(nodeTypeToQName(primaryType
));
492 Set
<QName
> secondaryTypes
= new TreeSet
<>(NamespaceUtils
.QNAME_COMPARATOR
);
493 for (NodeType mixinType
: context
.getMixinNodeTypes()) {
494 secondaryTypes
.add(nodeTypeToQName(mixinType
));
496 for (NodeType superType
: primaryType
.getDeclaredSupertypes()) {
497 secondaryTypes
.add(nodeTypeToQName(superType
));
500 for (NodeType mixinType
: context
.getMixinNodeTypes()) {
501 for (NodeType superType
: mixinType
.getDeclaredSupertypes()) {
502 secondaryTypes
.add(nodeTypeToQName(superType
));
505 res
.addAll(secondaryTypes
);
507 } catch (RepositoryException e
) {
508 throw new JcrException("Cannot list node types from " + getJcrNode(), e
);
512 private QName
nodeTypeToQName(NodeType nodeType
) {
513 String name
= nodeType
.getName();
514 return NamespaceUtils
.parsePrefixedName(provider
, name
);
515 // return QName.valueOf(name);
521 protected Session
getJcrSession() {
522 return provider
.getJcrSession(getSession(), jcrWorkspace
);
525 protected Node
getJcrNode() {
528 synchronized (this) {
529 if (lastRetrievingThread
!= Thread
.currentThread()) {
530 cachedNode
= getJcrSession().getNode(jcrPath
);
531 lastRetrievingThread
= Thread
.currentThread();
536 return getJcrSession().getNode(jcrPath
);
538 } catch (RepositoryException e
) {
539 throw new JcrException("Cannot retrieve " + jcrPath
+ " from workspace " + jcrWorkspace
, e
);
546 public static Content
nodeToContent(Node node
) {
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
);
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;
570 protected JcrContentIterator(NodeIterator nodeIterator
) {
571 this.nodeIterator
= nodeIterator
;
575 public boolean hasNext() {
576 return nodeIterator
.hasNext();
580 public Content
next() {
581 current
= new JcrContent(getSession(), provider
, jcrWorkspace
, Jcr
.getPath(nodeIterator
.nextNode()));
586 public void remove() {
587 if (current
!= null) {
588 Jcr
.remove(current
.getJcrNode());