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
.OutputStream
;
7 import java
.io
.PipedInputStream
;
8 import java
.io
.PipedOutputStream
;
9 import java
.time
.Instant
;
10 import java
.util
.ArrayList
;
11 import java
.util
.Arrays
;
12 import java
.util
.Calendar
;
13 import java
.util
.Date
;
14 import java
.util
.GregorianCalendar
;
15 import java
.util
.HashSet
;
16 import java
.util
.Iterator
;
17 import java
.util
.List
;
19 import java
.util
.Objects
;
20 import java
.util
.Optional
;
22 import java
.util
.TreeSet
;
23 import java
.util
.UUID
;
24 import java
.util
.concurrent
.CompletableFuture
;
25 import java
.util
.concurrent
.ForkJoinPool
;
27 import javax
.jcr
.Binary
;
28 import javax
.jcr
.Node
;
29 import javax
.jcr
.NodeIterator
;
30 import javax
.jcr
.Property
;
31 import javax
.jcr
.PropertyIterator
;
32 import javax
.jcr
.PropertyType
;
33 import javax
.jcr
.RepositoryException
;
34 import javax
.jcr
.Session
;
35 import javax
.jcr
.Value
;
36 import javax
.jcr
.ValueFactory
;
37 import javax
.jcr
.nodetype
.NodeType
;
38 import javax
.jcr
.nodetype
.NodeTypeManager
;
39 import javax
.xml
.namespace
.QName
;
40 import javax
.xml
.transform
.Source
;
41 import javax
.xml
.transform
.stream
.StreamSource
;
43 import org
.argeo
.api
.acr
.Content
;
44 import org
.argeo
.api
.acr
.CrAttributeType
;
45 import org
.argeo
.api
.acr
.DName
;
46 import org
.argeo
.api
.acr
.NamespaceUtils
;
47 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
48 import org
.argeo
.api
.cms
.CmsConstants
;
49 import org
.argeo
.cms
.acr
.AbstractContent
;
50 import org
.argeo
.cms
.acr
.ContentUtils
;
51 import org
.argeo
.jcr
.Jcr
;
52 import org
.argeo
.jcr
.JcrException
;
53 import org
.argeo
.jcr
.JcrUtils
;
54 import org
.argeo
.jcr
.JcrxApi
;
56 /** A JCR {@link Node} accessed as {@link Content}. */
57 public class JcrContent
extends AbstractContent
{
58 private JcrContentProvider provider
;
60 private String jcrWorkspace
;
61 private String jcrPath
;
63 private final boolean isMountBase
;
67 * While we want to support thread-safe access, it is very likely that only
68 * thread and only one sesssion will be used (typically from a single-threaded
69 * UI). We therefore cache was long as the same thread is calling.
71 private Thread lastRetrievingThread
= null;
72 private Node cachedNode
= null;
73 private boolean caching
= true;
75 protected JcrContent(ProvidedSession session
, JcrContentProvider provider
, String jcrWorkspace
, String jcrPath
) {
77 this.provider
= provider
;
78 this.jcrWorkspace
= jcrWorkspace
;
79 this.jcrPath
= jcrPath
;
81 this.isMountBase
= ContentUtils
.SLASH_STRING
.equals(jcrPath
);
89 public QName
getName() {
90 String name
= Jcr
.getName(getJcrNode());
91 if (name
.equals("")) {// root
92 String mountPath
= provider
.getMountPath();
93 name
= ContentUtils
.getParentPath(mountPath
)[1];
94 // name = Jcr.getWorkspaceName(getJcrNode());
96 return NamespaceUtils
.parsePrefixedName(provider
, name
);
99 @SuppressWarnings("unchecked")
101 public <A
> Optional
<A
> get(QName key
, Class
<A
> clss
) {
103 Node node
= getJcrNode();
104 if (DName
.creationdate
.equals(key
))
105 key
= JcrName
.created
.qName();
106 else if (DName
.getlastmodified
.equals(key
))
107 key
= JcrName
.lastModified
.qName();
108 else if (DName
.getcontenttype
.equals(key
)) {
109 String contentType
= null;
110 if (node
.isNodeType(NodeType
.NT_FILE
)) {
111 Node content
= node
.getNode(Node
.JCR_CONTENT
);
112 if (content
.isNodeType(NodeType
.MIX_MIMETYPE
)) {
113 contentType
= content
.getProperty(Property
.JCR_MIMETYPE
).getString();
114 if (content
.hasProperty(Property
.JCR_ENCODING
))
115 contentType
= contentType
+ ";encoding="
116 + content
.getProperty(Property
.JCR_ENCODING
).getString();
119 if (contentType
== null)
120 contentType
= "application/octet-stream";
121 return CrAttributeType
.cast(clss
, contentType
);
122 } else if (DName
.checkedOut
.equals(key
)) {
123 if (!node
.hasProperty(Property
.JCR_IS_CHECKED_OUT
))
124 return Optional
.empty();
125 boolean isCheckedOut
= node
.getProperty(Property
.JCR_IS_CHECKED_OUT
).getBoolean();
127 return Optional
.empty();
129 return (Optional
<A
>) Optional
.of(new Object());
130 } else if (DName
.checkedIn
.equals(key
)) {
131 if (!node
.hasProperty(Property
.JCR_IS_CHECKED_OUT
))
132 return Optional
.empty();
133 boolean isCheckedOut
= node
.getProperty(Property
.JCR_IS_CHECKED_OUT
).getBoolean();
135 return Optional
.empty();
137 return (Optional
<A
>) Optional
.of(new Object());
140 Object value
= get(node
, key
.toString());
141 if (value
instanceof List
<?
> lst
)
142 return Optional
.of((A
) lst
);
143 // TODO check other collections?
144 return CrAttributeType
.cast(clss
, value
);
145 } catch (RepositoryException e
) {
146 throw new JcrException(e
);
151 public Iterator
<Content
> iterator() {
153 return new JcrContentIterator(getJcrNode().getNodes());
154 } catch (RepositoryException e
) {
155 throw new JcrException("Cannot list children of " + getJcrNode(), e
);
160 protected Iterable
<QName
> keys() {
162 Node node
= getJcrNode();
163 Set
<QName
> keys
= new HashSet
<>();
164 for (PropertyIterator propertyIterator
= node
.getProperties(); propertyIterator
.hasNext();) {
165 Property property
= propertyIterator
.nextProperty();
166 QName name
= NamespaceUtils
.parsePrefixedName(provider
, property
.getName());
168 // TODO convert standard names
169 if (property
.getName().equals(Property
.JCR_CREATED
))
170 name
= DName
.creationdate
.qName();
171 else if (property
.getName().equals(Property
.JCR_LAST_MODIFIED
))
172 name
= DName
.getlastmodified
.qName();
173 else if (property
.getName().equals(Property
.JCR_MIMETYPE
))
174 name
= DName
.getcontenttype
.qName();
175 else if (property
.getName().equals(Property
.JCR_IS_CHECKED_OUT
)) {
176 boolean isCheckedOut
= node
.getProperty(Property
.JCR_IS_CHECKED_OUT
).getBoolean();
177 name
= isCheckedOut ? DName
.checkedOut
.qName() : DName
.checkedIn
.qName();
180 // TODO skip technical properties
184 } catch (RepositoryException e
) {
185 throw new JcrException("Cannot list properties of " + getJcrNode(), e
);
189 /** Cast to a standard Java object. */
190 static Object
get(Node node
, String property
) {
192 if (!node
.hasProperty(property
))
194 Property p
= node
.getProperty(property
);
195 if (p
.isMultiple()) {
196 Value
[] values
= p
.getValues();
197 List
<Object
> lst
= new ArrayList
<>();
198 for (Value value
: values
) {
199 lst
.add(convertSingleValue(value
));
203 Value value
= node
.getProperty(property
).getValue();
204 return convertSingleValue(value
);
206 } catch (RepositoryException e
) {
207 throw new JcrException("Cannot cast value from " + property
+ " of " + node
, e
);
212 public boolean isMultiple(QName key
) {
213 Node node
= getJcrNode();
214 String p
= NamespaceUtils
.toFullyQualified(key
);
216 if (node
.hasProperty(p
)) {
217 Property property
= node
.getProperty(p
);
218 return property
.isMultiple();
222 } catch (RepositoryException e
) {
223 throw new JcrException(
224 "Cannot check multiplicity of property " + p
+ " of " + jcrPath
+ " in " + jcrWorkspace
, e
);
229 public String
getPath() {
231 // Note: it is important to to use the default way (recursing through parents),
232 // since the session may not have access to parent nodes
233 return Content
.ROOT_PATH
+ jcrWorkspace
+ getJcrNode().getPath();
234 } catch (RepositoryException e
) {
235 throw new JcrException("Cannot get depth of " + getJcrNode(), e
);
240 public int getDepth() {
242 return getJcrNode().getDepth() + 1;
243 } catch (RepositoryException e
) {
244 throw new JcrException("Cannot get depth of " + getJcrNode(), e
);
249 public Content
getParent() {
251 String mountPath
= provider
.getMountPath();
252 if (mountPath
== null || mountPath
.equals("/"))
254 String
[] parent
= ContentUtils
.getParentPath(mountPath
);
255 return getSession().get(parent
[0]);
257 // if (Jcr.isRoot(getJcrNode())) // root
259 return new JcrContent(getSession(), provider
, jcrWorkspace
, Jcr
.getParentPath(getJcrNode()));
263 public int getSiblingIndex() {
264 return Jcr
.getIndex(getJcrNode());
268 public String
getText() {
269 return JcrxApi
.getXmlValue(getJcrNode());
276 public boolean containsKey(Object key
) {
277 return Jcr
.hasProperty(getJcrNode(), key
.toString());
284 protected Node
openForEdit() {
285 Node node
= getProvider().openForEdit(getSession(), jcrWorkspace
, jcrPath
);
286 getSession().notifyModification(this);
291 public Content
add(QName name
, QName
... classes
) {
294 if (classes
.length
> 0) {
295 QName primaryType
= classes
[0];
296 Node node
= openForEdit();
297 child
= Jcr
.addNode(node
, name
.toString(), primaryType
.toString());
299 for (int i
= 1; i
< classes
.length
; i
++)
300 child
.addMixin(classes
[i
].toString());
302 if (NtType
.file
.qName().equals(primaryType
)) {
303 // TODO optimise when we have a proper save mechanism
304 Node content
= child
.addNode(Node
.JCR_CONTENT
, NodeType
.NT_UNSTRUCTURED
);
306 // try (InputStream in = new ByteArrayInputStream(new byte[0])) {
307 // binary = content.getSession().getValueFactory().createBinary(in);
308 // content.setProperty(Property.JCR_DATA, binary);
309 // } catch (IOException e) {
310 // throw new UncheckedIOException(e);
312 child
.getSession().save();
315 child
= Jcr
.addNode(getJcrNode(), name
.toString(), NodeType
.NT_UNSTRUCTURED
);
317 return new JcrContent(getSession(), provider
, jcrWorkspace
, child
.getPath());
318 } catch (RepositoryException e
) {
319 throw new JcrException("Cannot add child to " + jcrPath
+ " in " + jcrWorkspace
, e
);
324 public Content
add(QName name
, Map
<QName
, Object
> attrs
, QName
... classes
) {
325 if (attrs
.containsKey(DName
.getcontenttype
.qName())) {
326 List
<QName
> lst
= new ArrayList
<>(Arrays
.asList(classes
));
327 lst
.add(0, NtType
.file
.qName());
328 classes
= lst
.toArray(new QName
[lst
.size()]);
330 if (attrs
.containsKey(DName
.collection
.qName())) {
331 List
<QName
> lst
= Arrays
.asList(classes
);
332 lst
.add(0, NtType
.folder
.qName());
333 classes
= lst
.toArray(new QName
[lst
.size()]);
335 Content child
= add(name
, classes
);
341 public void remove() {
342 Node node
= openForEdit();
347 private void saveJcrSession() {
349 getJcrSession().save();
350 } catch (RepositoryException e
) {
351 throw new JcrException("Cannot persist " + jcrPath
+ " in " + jcrWorkspace
, e
);
356 protected void removeAttr(QName key
) {
357 Node node
= openForEdit();
358 Property property
= Jcr
.getProperty(node
, key
.toString());
359 if (property
!= null) {
362 } catch (RepositoryException e
) {
363 throw new JcrException("Cannot remove property " + key
+ " from " + getJcrNode(), e
);
370 public Object
put(QName key
, Object value
) {
371 Objects
.requireNonNull(value
, "Value cannot be null");
372 Node node
= openForEdit();
374 if (DName
.checkedIn
.equals(key
) || DName
.checkedOut
.equals(key
))
375 throw new IllegalArgumentException(
376 key
+ " cannot be set, use the openForEdit/freeze methods of the related content provider.");
380 if (DName
.creationdate
.equals(key
))
381 property
= Property
.JCR_CREATED
;
382 else if (DName
.getlastmodified
.equals(key
))
383 property
= Property
.JCR_LAST_MODIFIED
;
384 else if (DName
.getcontenttype
.equals(key
)) {
385 if (!node
.isNodeType(NodeType
.NT_FILE
))
386 throw new IllegalStateException(DName
.getcontenttype
+ " can only be set on a file");
387 Node content
= node
.getNode(Node
.JCR_CONTENT
);
388 old
= Jcr
.get(content
, Property
.JCR_MIMETYPE
);
389 if (old
!= null && Jcr
.hasProperty(content
, Property
.JCR_ENCODING
))
390 old
= old
+ ";encoding=" + Jcr
.get(content
, Property
.JCR_ENCODING
);
391 String
[] str
= value
.toString().split(";");
392 String mimeType
= str
[0].trim();
393 String encoding
= null;
394 if (str
.length
> 1) {
395 value
= str
[0].trim();
396 String
[] eq
= str
[1].split("=");
397 assert eq
.length
== 2;
398 if ("encoding".equals(eq
[0].trim()))
401 content
.setProperty(Property
.JCR_MIMETYPE
, mimeType
);
402 if (encoding
!= null)
403 content
.setProperty(Property
.JCR_ENCODING
, encoding
);
406 property
= NamespaceUtils
.toFullyQualified(key
);
408 if (property
!= null) {
409 if (node
.hasProperty(property
)) {
410 old
= convertSingleValue(node
.getProperty(property
).getValue());
412 Value newValue
= convertSingleObject(node
.getSession().getValueFactory(), value
);
413 node
.setProperty(property
, newValue
);
415 // FIXME proper edition
418 } catch (RepositoryException e
) {
419 throw new JcrException("Cannot set property " + key
+ " on " + jcrPath
+ " in " + jcrWorkspace
, e
);
424 public void addContentClasses(QName
... contentClass
) throws IllegalArgumentException
, JcrException
{
426 Node node
= openForEdit();
427 NodeTypeManager ntm
= node
.getSession().getWorkspace().getNodeTypeManager();
428 List
<NodeType
> nodeTypes
= new ArrayList
<>();
429 for (QName clss
: contentClass
) {
430 NodeType nodeType
= ntm
.getNodeType(NamespaceUtils
.toFullyQualified(clss
));
431 if (!nodeType
.isMixin())
432 throw new IllegalArgumentException(clss
+ " is not a mixin");
433 nodeTypes
.add(nodeType
);
435 for (NodeType nodeType
: nodeTypes
) {
436 node
.addMixin(nodeType
.getName());
438 // FIXME proper edition
440 } catch (RepositoryException e
) {
441 throw new JcrException(
442 "Cannot add content classes " + contentClass
+ " to " + jcrPath
+ " in " + jcrWorkspace
, e
);
449 protected boolean exists() {
451 return getJcrSession().itemExists(jcrPath
);
452 } catch (RepositoryException e
) {
453 throw new JcrException("Cannot check whether " + jcrPath
+ " exists", e
);
458 public boolean isParentAccessible() {
459 String jcrParentPath
= ContentUtils
.getParentPath(jcrPath
)[0];
460 if ("".equals(jcrParentPath
)) // JCR root node
461 jcrParentPath
= ContentUtils
.SLASH_STRING
;
463 return getJcrSession().hasPermission(jcrParentPath
, Session
.ACTION_READ
);
464 } catch (RepositoryException e
) {
465 throw new JcrException("Cannot check whether parent " + jcrParentPath
+ " is accessible", e
);
472 @SuppressWarnings("unchecked")
473 public <A
> A
adapt(Class
<A
> clss
) {
474 if (Node
.class.isAssignableFrom(clss
)) {
475 return (A
) getJcrNode();
476 } else if (Source
.class.isAssignableFrom(clss
)) {
478 PipedOutputStream out
= new PipedOutputStream();
481 in
= new PipedInputStream(out
);
482 } catch (IOException e
) {
483 throw new RuntimeException("Cannot export " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
486 ForkJoinPool
.commonPool().execute(() -> {
487 // try (PipedOutputStream out = new PipedOutputStream(in)) {
489 getJcrSession().exportDocumentView(jcrPath
, out
, true, false);
492 } catch (IOException
| RepositoryException e
) {
493 throw new RuntimeException("Cannot export " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
497 return (A
) new StreamSource(in
);
498 // } catch (IOException e) {
499 // throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e);
502 return super.adapt(clss
);
506 @SuppressWarnings("unchecked")
508 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
510 if (InputStream
.class.isAssignableFrom(clss
)) {
511 Node node
= getJcrNode();
512 if (Jcr
.isNodeType(node
, NodeType
.NT_FILE
)) {
513 return (C
) JcrUtils
.getFileAsStream(node
);
515 } else if (OutputStream
.class.isAssignableFrom(clss
)) {
516 Node node
= getJcrNode();
517 if (Jcr
.isNodeType(node
, NodeType
.NT_FILE
)) {
518 Node content
= node
.getNode(Node
.JCR_CONTENT
);
519 PipedInputStream in
= new PipedInputStream();
520 ValueFactory valueFactory
= getJcrSession().getValueFactory();
521 CompletableFuture
<Void
> done
= CompletableFuture
.runAsync(() -> {
523 Binary binary
= valueFactory
.createBinary(in
);
524 content
.setProperty(Property
.JCR_DATA
, binary
);
526 } catch (RepositoryException e
) {
527 throw new JcrException(
528 "Cannot create binary in " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
531 PipedOutputStream out
= new PipedOutputStream(in
) {
534 public void close() throws IOException
{
538 // Binary binary = done.join();
540 // content.setProperty(Property.JCR_DATA, binary);
541 // } catch (RepositoryException e) {
542 // throw new JcrException(
543 // "Cannot write binary to " + jcrPath + " in workspace " + jcrWorkspace, e);
551 } catch (RepositoryException e
) {
552 throw new JcrException("Cannot open " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
554 return super.open(clss
);
558 public JcrContentProvider
getProvider() {
563 public String
getSessionLocalId() {
565 return getJcrNode().getIdentifier();
566 } catch (RepositoryException e
) {
567 throw new JcrException("Cannot get identifier for " + getJcrNode(), e
);
575 static Object
convertSingleValue(Value value
) throws JcrException
, IllegalArgumentException
{
577 switch (value
.getType()) {
578 case PropertyType
.STRING
:
579 return value
.getString();
580 case PropertyType
.DOUBLE
:
581 return (Double
) value
.getDouble();
582 case PropertyType
.LONG
:
583 return (Long
) value
.getLong();
584 case PropertyType
.BOOLEAN
:
585 return (Boolean
) value
.getBoolean();
586 case PropertyType
.DATE
:
587 Calendar calendar
= value
.getDate();
588 return calendar
.toInstant();
589 case PropertyType
.BINARY
:
590 throw new IllegalArgumentException("Binary is not supported as an attribute");
592 return value
.getString();
594 } catch (RepositoryException e
) {
595 throw new JcrException("Cannot convert " + value
+ " to an object.", e
);
599 static Value
convertSingleObject(ValueFactory factory
, Object value
) {
600 if (value
instanceof String string
) {
601 return factory
.createValue(string
);
602 } else if (value
instanceof Double dbl
) {
603 return factory
.createValue(dbl
);
604 } else if (value
instanceof Float flt
) {
605 return factory
.createValue(flt
);
606 } else if (value
instanceof Long lng
) {
607 return factory
.createValue(lng
);
608 } else if (value
instanceof Integer intg
) {
609 return factory
.createValue(intg
);
610 } else if (value
instanceof Boolean bool
) {
611 return factory
.createValue(bool
);
612 } else if (value
instanceof Instant instant
) {
613 GregorianCalendar calendar
= new GregorianCalendar();
614 calendar
.setTime(Date
.from(instant
));
615 return factory
.createValue(calendar
);
617 // TODO or use String by default?
618 throw new IllegalArgumentException("Unsupported value " + value
.getClass());
623 public Class
<?
> getType(QName key
) {
624 Node node
= getJcrNode();
625 String p
= NamespaceUtils
.toFullyQualified(key
);
627 if (node
.hasProperty(p
)) {
628 Property property
= node
.getProperty(p
);
629 return switch (property
.getType()) {
630 case PropertyType
.STRING
:
631 case PropertyType
.NAME
:
632 case PropertyType
.PATH
:
633 case PropertyType
.DECIMAL
:
635 case PropertyType
.LONG
:
637 case PropertyType
.DOUBLE
:
639 case PropertyType
.BOOLEAN
:
641 case PropertyType
.DATE
:
643 case PropertyType
.WEAKREFERENCE
:
644 case PropertyType
.REFERENCE
:
650 // TODO does it make sense?
653 } catch (RepositoryException e
) {
654 throw new JcrException("Cannot get type of property " + p
+ " of " + jcrPath
+ " in " + jcrWorkspace
, e
);
659 public List
<QName
> getContentClasses() {
661 Node context
= getJcrNode();
663 List
<QName
> res
= new ArrayList
<>();
665 NodeType primaryType
= context
.getPrimaryNodeType();
666 res
.add(nodeTypeToQName(primaryType
));
667 if (primaryType
.getName().equals(NodeType
.NT_FOLDER
))
668 res
.add(DName
.collection
.qName());
670 Set
<QName
> secondaryTypes
= new TreeSet
<>(NamespaceUtils
.QNAME_COMPARATOR
);
671 for (NodeType mixinType
: context
.getMixinNodeTypes()) {
672 secondaryTypes
.add(nodeTypeToQName(mixinType
));
674 for (NodeType superType
: primaryType
.getDeclaredSupertypes()) {
675 secondaryTypes
.add(nodeTypeToQName(superType
));
678 for (NodeType mixinType
: context
.getMixinNodeTypes()) {
679 for (NodeType superType
: mixinType
.getDeclaredSupertypes()) {
680 secondaryTypes
.add(nodeTypeToQName(superType
));
683 res
.addAll(secondaryTypes
);
685 } catch (RepositoryException e
) {
686 throw new JcrException("Cannot list node types from " + getJcrNode(), e
);
690 private QName
nodeTypeToQName(NodeType nodeType
) {
691 String name
= nodeType
.getName();
692 return NamespaceUtils
.parsePrefixedName(provider
, name
);
693 // return QName.valueOf(name);
699 protected Session
getJcrSession() {
700 return provider
.getJcrSession(getSession(), jcrWorkspace
);
703 protected Node
getJcrNode() {
706 synchronized (this) {
707 if (lastRetrievingThread
!= Thread
.currentThread()) {
708 cachedNode
= getJcrSession().getNode(jcrPath
);
709 lastRetrievingThread
= Thread
.currentThread();
714 return getJcrSession().getNode(jcrPath
);
716 } catch (RepositoryException e
) {
717 throw new JcrException("Cannot retrieve " + jcrPath
+ " from workspace " + jcrWorkspace
, e
);
724 public static Content
nodeToContent(Node node
) {
728 ProvidedSession contentSession
= (ProvidedSession
) node
.getSession()
729 .getAttribute(ProvidedSession
.class.getName());
730 if (contentSession
== null)
731 throw new IllegalArgumentException(
732 "Cannot adapt " + node
+ " to content, because it was not loaded from a content session");
733 return contentSession
.get(Content
.ROOT_PATH
+ CmsConstants
.SYS_WORKSPACE
+ node
.getPath());
734 } catch (RepositoryException e
) {
735 throw new JcrException("Cannot adapt " + node
+ " to a content", e
);
743 class JcrContentIterator
implements Iterator
<Content
> {
744 private final NodeIterator nodeIterator
;
745 // we keep track in order to be able to delete it
746 private JcrContent current
= null;
748 protected JcrContentIterator(NodeIterator nodeIterator
) {
749 this.nodeIterator
= nodeIterator
;
753 public boolean hasNext() {
754 return nodeIterator
.hasNext();
758 public Content
next() {
759 current
= new JcrContent(getSession(), provider
, jcrWorkspace
, Jcr
.getPath(nodeIterator
.nextNode()));
764 public void remove() {
765 if (current
!= null) {
766 Jcr
.remove(current
.getJcrNode());