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
.ForkJoinPool
;
26 import javax
.jcr
.Binary
;
27 import javax
.jcr
.Node
;
28 import javax
.jcr
.NodeIterator
;
29 import javax
.jcr
.Property
;
30 import javax
.jcr
.PropertyIterator
;
31 import javax
.jcr
.PropertyType
;
32 import javax
.jcr
.RepositoryException
;
33 import javax
.jcr
.Session
;
34 import javax
.jcr
.Value
;
35 import javax
.jcr
.ValueFactory
;
36 import javax
.jcr
.nodetype
.NodeType
;
37 import javax
.jcr
.nodetype
.NodeTypeManager
;
38 import javax
.xml
.namespace
.QName
;
39 import javax
.xml
.transform
.Source
;
40 import javax
.xml
.transform
.stream
.StreamSource
;
42 import org
.argeo
.api
.acr
.Content
;
43 import org
.argeo
.api
.acr
.CrAttributeType
;
44 import org
.argeo
.api
.acr
.DName
;
45 import org
.argeo
.api
.acr
.NamespaceUtils
;
46 import org
.argeo
.api
.acr
.spi
.ProvidedSession
;
47 import org
.argeo
.api
.cms
.CmsConstants
;
48 import org
.argeo
.cms
.acr
.AbstractContent
;
49 import org
.argeo
.cms
.acr
.CmsContent
;
50 import org
.argeo
.cms
.util
.AsyncPipedOutputStream
;
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 one
68 * thread and only one session 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
= "/".equals(jcrPath
);
89 public QName
getName() {
90 String name
= Jcr
.getName(getJcrNode());
91 if (name
.equals("")) {// root
92 String mountPath
= provider
.getMountPath();
93 name
= CmsContent
.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
= CmsContent
.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
) {
293 Node node
= openForEdit();
295 if (classes
.length
> 0) {
296 classes
: for (int i
= 0; i
< classes
.length
; i
++) {
297 if (classes
[i
].equals(DName
.collection
.qName())) {
298 List
<QName
> lst
= new ArrayList
<>(Arrays
.asList(classes
));
299 lst
.add(0, NtType
.folder
.qName());
300 lst
.remove(DName
.collection
.qName());
301 classes
= lst
.toArray(new QName
[lst
.size()]);
305 QName primaryType
= classes
[0];
306 child
= Jcr
.addNode(node
, name
.toString(), primaryType
.toString());
308 for (int i
= 1; i
< classes
.length
; i
++)
309 child
.addMixin(classes
[i
].toString());
311 if (NtType
.file
.qName().equals(primaryType
)) {
312 // TODO optimise when we have a proper save mechanism
313 child
.addNode(Node
.JCR_CONTENT
, NodeType
.NT_UNSTRUCTURED
);
316 child
= Jcr
.addNode(node
, name
.toString(), NodeType
.NT_UNSTRUCTURED
);
318 saveEditedNode(node
);
319 return new JcrContent(getSession(), provider
, jcrWorkspace
, child
.getPath());
320 } catch (RepositoryException e
) {
321 throw new JcrException("Cannot add child to " + jcrPath
+ " in " + jcrWorkspace
, e
);
326 public Content
add(QName name
, Map
<QName
, Object
> attrs
, QName
... classes
) {
327 if (attrs
.containsKey(DName
.getcontenttype
.qName())) {
328 List
<QName
> lst
= new ArrayList
<>(Arrays
.asList(classes
));
329 lst
.add(0, NtType
.file
.qName());
330 classes
= lst
.toArray(new QName
[lst
.size()]);
333 Content child
= add(name
, classes
);
339 public void remove() {
340 Node node
= openForEdit();
342 saveEditedNode(node
);
345 private void saveEditedNode(Node node
) {
347 node
.getSession().save();
348 getJcrSession().refresh(true);
349 } catch (RepositoryException e
) {
350 throw new JcrException("Cannot persist " + jcrPath
+ " in " + jcrWorkspace
, e
);
355 protected void removeAttr(QName key
) {
356 Node node
= openForEdit();
357 Property property
= Jcr
.getProperty(node
, key
.toString());
358 if (property
!= null) {
361 } catch (RepositoryException e
) {
362 throw new JcrException("Cannot remove property " + key
+ " from " + getJcrNode(), e
);
365 saveEditedNode(node
);
369 public Object
put(QName key
, Object value
) {
370 Objects
.requireNonNull(value
, "Value cannot be null");
371 Node node
= openForEdit();
373 if (DName
.checkedIn
.equals(key
) || DName
.checkedOut
.equals(key
))
374 throw new IllegalArgumentException(
375 key
+ " cannot be set, use the openForEdit/freeze methods of the related content provider.");
379 if (DName
.creationdate
.equals(key
))
380 property
= Property
.JCR_CREATED
;
381 else if (DName
.getlastmodified
.equals(key
))
382 property
= Property
.JCR_LAST_MODIFIED
;
383 else if (DName
.getcontenttype
.equals(key
)) {
384 if (!node
.isNodeType(NodeType
.NT_FILE
))
385 throw new IllegalStateException(DName
.getcontenttype
+ " can only be set on a file");
386 Node content
= node
.getNode(Node
.JCR_CONTENT
);
387 old
= Jcr
.get(content
, Property
.JCR_MIMETYPE
);
388 if (old
!= null && Jcr
.hasProperty(content
, Property
.JCR_ENCODING
))
389 old
= old
+ ";encoding=" + Jcr
.get(content
, Property
.JCR_ENCODING
);
390 String
[] str
= value
.toString().split(";");
391 String mimeType
= str
[0].trim();
392 String encoding
= null;
393 if (str
.length
> 1) {
394 value
= str
[0].trim();
395 String
[] eq
= str
[1].split("=");
396 assert eq
.length
== 2;
397 if ("encoding".equals(eq
[0].trim()))
400 content
.setProperty(Property
.JCR_MIMETYPE
, mimeType
);
401 if (encoding
!= null)
402 content
.setProperty(Property
.JCR_ENCODING
, encoding
);
405 property
= NamespaceUtils
.toFullyQualified(key
);
407 if (property
!= null) {
408 if (node
.hasProperty(property
)) {
409 old
= convertSingleValue(node
.getProperty(property
).getValue());
411 Value newValue
= convertSingleObject(node
.getSession().getValueFactory(), value
);
412 node
.setProperty(property
, newValue
);
414 // FIXME proper edition
415 saveEditedNode(node
);
417 } catch (RepositoryException e
) {
418 throw new JcrException("Cannot set property " + key
+ " on " + jcrPath
+ " in " + jcrWorkspace
, e
);
423 public void addContentClasses(QName
... contentClass
) throws IllegalArgumentException
, JcrException
{
425 Node node
= openForEdit();
426 NodeTypeManager ntm
= node
.getSession().getWorkspace().getNodeTypeManager();
427 List
<NodeType
> nodeTypes
= new ArrayList
<>();
428 for (QName clss
: contentClass
) {
429 NodeType nodeType
= ntm
.getNodeType(NamespaceUtils
.toFullyQualified(clss
));
430 if (!nodeType
.isMixin())
431 throw new IllegalArgumentException(clss
+ " is not a mixin");
432 nodeTypes
.add(nodeType
);
434 for (NodeType nodeType
: nodeTypes
) {
435 node
.addMixin(nodeType
.getName());
437 // FIXME proper edition
438 saveEditedNode(node
);
439 } catch (RepositoryException e
) {
440 throw new JcrException(
441 "Cannot add content classes " + contentClass
+ " to " + jcrPath
+ " in " + jcrWorkspace
, e
);
448 protected boolean exists() {
450 return getJcrSession().itemExists(jcrPath
);
451 } catch (RepositoryException e
) {
452 throw new JcrException("Cannot check whether " + jcrPath
+ " exists", e
);
457 public boolean isParentAccessible() {
458 String jcrParentPath
= CmsContent
.getParentPath(jcrPath
)[0];
459 if ("".equals(jcrParentPath
)) // JCR root node
462 return getJcrSession().hasPermission(jcrParentPath
, Session
.ACTION_READ
);
463 } catch (RepositoryException e
) {
464 throw new JcrException("Cannot check whether parent " + jcrParentPath
+ " is accessible", e
);
471 @SuppressWarnings("unchecked")
472 public <A
> A
adapt(Class
<A
> clss
) {
473 if (Node
.class.isAssignableFrom(clss
)) {
474 return (A
) getJcrNode();
475 } else if (Source
.class.isAssignableFrom(clss
)) {
477 PipedOutputStream out
= new PipedOutputStream();
480 in
= new PipedInputStream(out
);
481 } catch (IOException e
) {
482 throw new RuntimeException("Cannot export " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
485 ForkJoinPool
.commonPool().execute(() -> {
486 // try (PipedOutputStream out = new PipedOutputStream(in)) {
488 getJcrSession().exportDocumentView(jcrPath
, out
, true, false);
491 } catch (IOException
| RepositoryException e
) {
492 throw new RuntimeException("Cannot export " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
496 return (A
) new StreamSource(in
);
497 // } catch (IOException e) {
498 // throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e);
501 return super.adapt(clss
);
505 @SuppressWarnings("unchecked")
507 public <C
extends Closeable
> C
open(Class
<C
> clss
) throws IOException
, IllegalArgumentException
{
509 if (InputStream
.class.isAssignableFrom(clss
)) {
510 Node node
= getJcrNode();
511 // System.out.println(node.getSession());
512 if (Jcr
.isNodeType(node
, NodeType
.NT_FILE
)) {
513 return (C
) JcrUtils
.getFileAsStream(node
);
515 } else if (OutputStream
.class.isAssignableFrom(clss
)) {
516 Node node
= openForEdit();
517 // System.out.println(node.getSession());
518 if (Jcr
.isNodeType(node
, NodeType
.NT_FILE
)) {
519 Node content
= node
.getNode(Node
.JCR_CONTENT
);
520 AsyncPipedOutputStream out
= new AsyncPipedOutputStream();
522 ValueFactory valueFactory
= getJcrSession().getValueFactory();
523 out
.asyncRead((in
) -> {
525 Binary binary
= valueFactory
.createBinary(in
);
526 content
.setProperty(Property
.JCR_DATA
, binary
);
527 saveEditedNode(node
);
528 } catch (RepositoryException e
) {
529 throw new JcrException(
530 "Cannot create binary in " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
536 } catch (RepositoryException e
) {
537 throw new JcrException("Cannot open " + jcrPath
+ " in workspace " + jcrWorkspace
, e
);
539 return super.open(clss
);
543 public JcrContentProvider
getProvider() {
548 public String
getSessionLocalId() {
550 return getJcrNode().getIdentifier();
551 } catch (RepositoryException e
) {
552 throw new JcrException("Cannot get identifier for " + getJcrNode(), e
);
560 static Object
convertSingleValue(Value value
) throws JcrException
, IllegalArgumentException
{
562 switch (value
.getType()) {
563 case PropertyType
.STRING
:
564 return value
.getString();
565 case PropertyType
.DOUBLE
:
566 return (Double
) value
.getDouble();
567 case PropertyType
.LONG
:
568 return (Long
) value
.getLong();
569 case PropertyType
.BOOLEAN
:
570 return (Boolean
) value
.getBoolean();
571 case PropertyType
.DATE
:
572 Calendar calendar
= value
.getDate();
573 return calendar
.toInstant();
574 case PropertyType
.BINARY
:
575 throw new IllegalArgumentException("Binary is not supported as an attribute");
577 return value
.getString();
579 } catch (RepositoryException e
) {
580 throw new JcrException("Cannot convert " + value
+ " to an object.", e
);
584 static Value
convertSingleObject(ValueFactory factory
, Object value
) {
585 if (value
instanceof String string
) {
586 return factory
.createValue(string
);
587 } else if (value
instanceof Double dbl
) {
588 return factory
.createValue(dbl
);
589 } else if (value
instanceof Float flt
) {
590 return factory
.createValue(flt
);
591 } else if (value
instanceof Long lng
) {
592 return factory
.createValue(lng
);
593 } else if (value
instanceof Integer intg
) {
594 return factory
.createValue(intg
);
595 } else if (value
instanceof Boolean bool
) {
596 return factory
.createValue(bool
);
597 } else if (value
instanceof Instant instant
) {
598 GregorianCalendar calendar
= new GregorianCalendar();
599 calendar
.setTime(Date
.from(instant
));
600 return factory
.createValue(calendar
);
602 // TODO or use String by default?
603 throw new IllegalArgumentException("Unsupported value " + value
.getClass());
608 public Class
<?
> getType(QName key
) {
609 Node node
= getJcrNode();
610 String p
= NamespaceUtils
.toFullyQualified(key
);
612 if (node
.hasProperty(p
)) {
613 Property property
= node
.getProperty(p
);
614 return switch (property
.getType()) {
615 case PropertyType
.STRING
:
616 case PropertyType
.NAME
:
617 case PropertyType
.PATH
:
618 case PropertyType
.DECIMAL
:
620 case PropertyType
.LONG
:
622 case PropertyType
.DOUBLE
:
624 case PropertyType
.BOOLEAN
:
626 case PropertyType
.DATE
:
628 case PropertyType
.WEAKREFERENCE
:
629 case PropertyType
.REFERENCE
:
635 // TODO does it make sense?
638 } catch (RepositoryException e
) {
639 throw new JcrException("Cannot get type of property " + p
+ " of " + jcrPath
+ " in " + jcrWorkspace
, e
);
644 public List
<QName
> getContentClasses() {
646 Node context
= getJcrNode();
648 List
<QName
> res
= new ArrayList
<>();
650 NodeType primaryType
= context
.getPrimaryNodeType();
651 res
.add(nodeTypeToQName(primaryType
));
652 if (primaryType
.getName().equals(NodeType
.NT_FOLDER
))
653 res
.add(DName
.collection
.qName());
655 Set
<QName
> secondaryTypes
= new TreeSet
<>(NamespaceUtils
.QNAME_COMPARATOR
);
656 for (NodeType mixinType
: context
.getMixinNodeTypes()) {
657 secondaryTypes
.add(nodeTypeToQName(mixinType
));
659 for (NodeType superType
: primaryType
.getDeclaredSupertypes()) {
660 secondaryTypes
.add(nodeTypeToQName(superType
));
663 for (NodeType mixinType
: context
.getMixinNodeTypes()) {
664 for (NodeType superType
: mixinType
.getDeclaredSupertypes()) {
665 secondaryTypes
.add(nodeTypeToQName(superType
));
668 res
.addAll(secondaryTypes
);
670 } catch (RepositoryException e
) {
671 throw new JcrException("Cannot list node types from " + getJcrNode(), e
);
675 private QName
nodeTypeToQName(NodeType nodeType
) {
676 String name
= nodeType
.getName();
677 return NamespaceUtils
.parsePrefixedName(provider
, name
);
678 // return QName.valueOf(name);
684 protected Session
getJcrSession() {
685 Session s
= provider
.getJcrSession(getSession(), jcrWorkspace
);
686 // if (getSession().isEditing())
689 // } catch (RepositoryException e) {
690 // throw new JcrException("Cannot refresh session", e);
695 protected Node
getJcrNode() {
698 synchronized (this) {
699 if (lastRetrievingThread
!= Thread
.currentThread()) {
700 cachedNode
= getJcrSession().getNode(jcrPath
);
701 lastRetrievingThread
= Thread
.currentThread();
706 return getJcrSession().getNode(jcrPath
);
708 } catch (RepositoryException e
) {
709 throw new JcrException("Cannot retrieve " + jcrPath
+ " from workspace " + jcrWorkspace
, e
);
716 public static Content
nodeToContent(Node node
) {
720 ProvidedSession contentSession
= (ProvidedSession
) node
.getSession()
721 .getAttribute(ProvidedSession
.class.getName());
722 if (contentSession
== null)
723 throw new IllegalArgumentException(
724 "Cannot adapt " + node
+ " to content, because it was not loaded from a content session");
725 return contentSession
.get(Content
.ROOT_PATH
+ CmsConstants
.SYS_WORKSPACE
+ node
.getPath());
726 } catch (RepositoryException e
) {
727 throw new JcrException("Cannot adapt " + node
+ " to a content", e
);
735 class JcrContentIterator
implements Iterator
<Content
> {
736 private final NodeIterator nodeIterator
;
737 // we keep track in order to be able to delete it
738 private JcrContent current
= null;
740 protected JcrContentIterator(NodeIterator nodeIterator
) {
741 this.nodeIterator
= nodeIterator
;
745 public boolean hasNext() {
746 return nodeIterator
.hasNext();
750 public Content
next() {
751 current
= new JcrContent(getSession(), provider
, jcrWorkspace
, Jcr
.getPath(nodeIterator
.nextNode()));
756 public void remove() {
757 if (current
!= null) {
758 Jcr
.remove(current
.getJcrNode());