View Javadoc

1   /**
2    * This file Copyright (c) 2003-2010 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.core;
35  
36  import info.magnolia.cms.core.version.ContentVersion;
37  import info.magnolia.cms.core.version.VersionManager;
38  import info.magnolia.cms.security.AccessDeniedException;
39  import info.magnolia.cms.security.Permission;
40  import info.magnolia.cms.util.Rule;
41  import info.magnolia.logging.AuditLoggingUtil;
42  
43  import java.util.ArrayList;
44  import java.util.Collection;
45  import java.util.Comparator;
46  import java.util.List;
47  import java.util.TreeSet;
48  
49  import javax.jcr.Item;
50  import javax.jcr.Node;
51  import javax.jcr.NodeIterator;
52  import javax.jcr.PathNotFoundException;
53  import javax.jcr.Property;
54  import javax.jcr.PropertyIterator;
55  import javax.jcr.PropertyType;
56  import javax.jcr.RepositoryException;
57  import javax.jcr.UnsupportedRepositoryOperationException;
58  import javax.jcr.lock.Lock;
59  import javax.jcr.lock.LockException;
60  import javax.jcr.nodetype.NodeType;
61  import javax.jcr.version.Version;
62  import javax.jcr.version.VersionException;
63  import javax.jcr.version.VersionHistory;
64  import javax.jcr.version.VersionIterator;
65  
66  import org.apache.commons.lang.StringUtils;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  
71  /**
72   * Default, JCR-based, implementation of {@link Content}.
73   *
74   * @author Sameer Charles
75   * @version $Revision:2719 $ ($Author:scharles $)
76   */
77  public class DefaultContent extends AbstractContent {
78      private static final Logger log = LoggerFactory.getLogger(DefaultContent.class);
79  
80      /**
81       * Wrapped jcr node.
82       */
83      protected Node node;
84  
85      /**
86       * Path for the jcr node.
87       */
88      private String path;
89  
90      /**
91       * root node.
92       */
93      private Node rootNode;
94  
95      /**
96       * node metadata.
97       */
98      private MetaData metaData;
99  
100     /**
101      * Empty constructor. Should NEVER be used for standard use, test only.
102      */
103     protected DefaultContent() {
104     }
105 
106     /**
107      * Constructor to get existing node.
108      * @param rootNode node to start with
109      * @param path absolute (primary) path to this <code>Node</code>
110      * @param hierarchyManager HierarchyManager instance
111      * @throws PathNotFoundException if the node at <code>path</code> does not exist
112      * @throws AccessDeniedException if the current session does not have sufficient access rights to complete the
113      * operation
114      * @throws RepositoryException if an error occurs
115      */
116     DefaultContent(Node rootNode, String path, HierarchyManager hierarchyManager) throws PathNotFoundException, RepositoryException, AccessDeniedException {
117         this.setHierarchyManager(hierarchyManager);
118         Access.isGranted(hierarchyManager.getAccessManager(), Path.getAbsolutePath(rootNode.getPath(), path), Permission.READ);
119         this.setPath(path);
120         this.setRootNode(rootNode);
121         this.setNode(this.rootNode.getNode(this.path));
122     }
123 
124     /**
125      * Constructor to get existing node.
126      * @param elem initialized node object
127      * @param hierarchyManager HierarchyManager instance
128      * @throws AccessDeniedException if the current session does not have sufficient access rights to complete the
129      * operation
130      * @throws RepositoryException if an error occurs
131      */
132     public DefaultContent(Item elem,HierarchyManager hierarchyManager) throws RepositoryException, AccessDeniedException {
133         this.setHierarchyManager(hierarchyManager);
134         Access.isGranted(hierarchyManager.getAccessManager(), Path.getAbsolutePath(elem.getPath()), Permission.READ);
135         this.setNode((Node) elem);
136         this.setPath(this.getHandle());
137     }
138 
139     /**
140      * Creates contentNode of type <b>contentType </b> contentType must be defined in item type definition of Magnolia
141      * as well as JCR implementation.
142      * @param rootNode node to start with
143      * @param path absolute (primary) path to this <code>Node</code>
144      * @param contentType JCR node type as configured
145      * @param hierarchyManager HierarchyManager instance
146      * @throws PathNotFoundException if the node at <code>path</code> does not exist
147      * @throws RepositoryException if an error occurs
148      * @throws AccessDeniedException if the current session does not have sufficient access rights to complete the
149      * operation
150      */
151     DefaultContent(Node rootNode, String path, String contentType, HierarchyManager hierarchyManager)
152         throws PathNotFoundException,
153         RepositoryException,
154         AccessDeniedException {
155         this.setHierarchyManager(hierarchyManager);
156         Access.isGranted(hierarchyManager.getAccessManager(), Path.getAbsolutePath(rootNode.getPath(), path), Permission.WRITE);
157         this.setPath(path);
158         this.setRootNode(rootNode);
159         this.node = this.rootNode.addNode(this.path, contentType);
160         // add mix:lockable as default for all nodes created using this manager
161         // for version 3.5 we cannot change node type definitions because of compatibility reasons
162         // MAGNOLIA-1518
163         this.addMixin(ItemType.MIX_LOCKABLE);
164         AuditLoggingUtil.log( AuditLoggingUtil.ACTION_CREATE, hierarchyManager.getName(), this.getItemType(), Path.getAbsolutePath(node.getPath()));
165     }
166 
167     /**
168      * @param node
169      */
170     protected void setNode(Node node) {
171         this.node = node;
172     }
173 
174     /**
175      * @param node
176      */
177     protected void setRootNode(Node node) {
178         this.rootNode = node;
179     }
180 
181     /**
182      * @param path
183      */
184     protected void setPath(String path) {
185         this.path = path;
186     }
187 
188     public Content getContent(String name) throws PathNotFoundException, RepositoryException, AccessDeniedException {
189         return (new DefaultContent(this.node, name, this.hierarchyManager));
190     }
191 
192     public Content createContent(String name, String contentType) throws PathNotFoundException, RepositoryException,
193         AccessDeniedException {
194         Content content = new DefaultContent(this.node, name, contentType, this.hierarchyManager);
195         MetaData metaData = content.getMetaData();
196         metaData.setCreationDate();
197         return content;
198     }
199 
200     @Override
201     public boolean hasNodeData(String name) throws RepositoryException {
202         if (this.node.hasProperty(name)) {
203             return true;
204         }
205         else { // check for mgnl:resource node
206             if (this.node.hasNode(name) && (this.node.getNode(name).isNodeType(ItemType.NT_RESOURCE) || (this.node.hasProperty("jcr:frozenPrimaryType") && this.node.getNode(name).getProperty("jcr:frozenPrimaryType").getValue().getString().equals(ItemType.NT_RESOURCE)))) {
207                 return true;
208             }
209         }
210         return false;
211     }
212 
213     public NodeData newNodeDataInstance(String name, int type, boolean createIfNotExisting) throws AccessDeniedException, RepositoryException {
214         try {
215             Access.isGranted(getAccessManager(), Path.getAbsolutePath(getHandle(), name), Permission.READ);
216         }
217         // FIXME: should be thrown but return a dummy node data
218         catch (AccessDeniedException e) {
219             return new NonExistingNodeData(this, name);
220         }
221 
222         // create an empty dummy
223         if(!hasNodeData(name) && !createIfNotExisting){
224             return new NonExistingNodeData(this, name);
225         }
226 
227         if(type == PropertyType.UNDEFINED){
228             type = determineNodeDataType(name);
229         }
230 
231         if(type == PropertyType.BINARY){
232             return new BinaryNodeData(this, name);
233         }
234         else{
235             return new DefaultNodeData(this, name);
236         }
237     }
238 
239     protected int determineNodeDataType(String name) {
240         // FIXME: maybe delegate to NodeDataImplementations?
241         try {
242             if (this.node.hasProperty(name)) {
243                 return this.node.getProperty(name).getType();
244             }
245             else { // check for mgnl:resource node
246                 if (this.node.hasNode(name) && (this.node.getNode(name).isNodeType(ItemType.NT_RESOURCE) || (this.node.getNode(name).hasProperty("jcr:frozenPrimaryType") && this.node.getNode(name).getProperty("jcr:frozenPrimaryType").getValue().getString().equals(ItemType.NT_RESOURCE)))) {
247                     return PropertyType.BINARY;
248                 }
249             }
250         }
251         catch (RepositoryException e) {
252             throw new IllegalStateException("Can't determine property type of [" + getHandle() + "/" + name + "]", e);
253         }
254         return PropertyType.UNDEFINED;
255     }
256 
257 
258     public MetaData getMetaData() {
259         if (this.metaData == null) {
260             this.metaData = new MetaData(this.node, this.hierarchyManager.getAccessManager());
261         }
262         return this.metaData;
263     }
264 
265     public String getName() {
266         try {
267             return this.node.getName();
268         }
269         catch (RepositoryException e) {
270             log.error(e.getMessage(), e);
271         }
272         return StringUtils.EMPTY;
273     }
274 
275     public Collection<Content> getChildren(ContentFilter filter, String namePattern, Comparator<Content> orderCriteria) {
276         Collection<Content> children;
277         if (orderCriteria == null) {
278             children = new ArrayList<Content>();
279         }
280         else {
281             children = new TreeSet<Content>(orderCriteria);
282         }
283 
284         try {
285             final NodeIterator nodeIterator;
286             if (namePattern == null) {
287                 nodeIterator = this.node.getNodes();
288             } else {
289                 nodeIterator = this.node.getNodes(namePattern);
290             }
291 
292             while (nodeIterator.hasNext()) {
293                 Node subNode = (Node) nodeIterator.next();
294                 try {
295                     Content content = new DefaultContent(subNode, this.hierarchyManager);
296                     if (filter.accept(content)) {
297                         children.add(content);
298                     }
299                 }
300                 catch (PathNotFoundException e) {
301                     log.error("Exception caught", e);
302                 }
303                 catch (AccessDeniedException e) {
304                     // ignore, simply wont add content in a list
305                 }
306             }
307         }
308         catch (RepositoryException re) {
309             log.error("Exception caught", re);
310         }
311 
312         return children;
313     }
314 
315     public Collection<NodeData> getNodeDataCollection(String namePattern) {
316         final ArrayList<NodeData> all = new ArrayList<NodeData>();
317         try {
318             all.addAll(getPrimitiveNodeDatas(namePattern));
319             all.addAll(getBinaryNodeDatas(namePattern));
320         }
321         catch (RepositoryException e) {
322             throw new IllegalStateException("Can't read node datas of " + toString(), e);
323         }
324         return all;
325     }
326 
327     protected Collection<NodeData> getPrimitiveNodeDatas(String namePattern) throws RepositoryException {
328         final Collection<NodeData> nodeDatas = new ArrayList<NodeData>();
329         final PropertyIterator propertyIterator;
330         if (namePattern == null) {
331             propertyIterator = this.node.getProperties();
332         } else {
333             propertyIterator = this.node.getProperties(namePattern);
334         }
335         while (propertyIterator.hasNext()) {
336             Property property = (Property) propertyIterator.next();
337             try {
338                 if (!property.getName().startsWith("jcr:") && !property.getName().startsWith("mgnl:")) { //$NON-NLS-1$ //$NON-NLS-2$
339                     nodeDatas.add(getNodeData(property.getName()));
340                 }
341             }
342             catch (PathNotFoundException e) {
343                 log.error("Exception caught", e);
344             }
345             catch (AccessDeniedException e) {
346                 // ignore, simply wont add content in a list
347             }
348         }
349         return nodeDatas;
350     }
351 
352 
353     public boolean hasContent(String name) throws RepositoryException {
354         return this.node.hasNode(name);
355     }
356 
357     public String getHandle() {
358         try {
359             return this.node.getPath();
360         }
361         catch (RepositoryException e) {
362             log.error("Failed to get handle: " + e.getMessage(), e); //$NON-NLS-1$
363             return StringUtils.EMPTY;
364         }
365     }
366 
367     public Content getParent() throws PathNotFoundException, RepositoryException, AccessDeniedException {
368         return (new DefaultContent(this.node.getParent(), this.hierarchyManager));
369     }
370 
371     public Content getAncestor(int level) throws PathNotFoundException, RepositoryException, AccessDeniedException {
372         if (level > this.getLevel()) {
373             throw new PathNotFoundException();
374         }
375         return (new DefaultContent(this.node.getAncestor(level), this.hierarchyManager));
376     }
377 
378     public Collection<Content> getAncestors() throws PathNotFoundException, RepositoryException {
379         List<Content> allAncestors = new ArrayList<Content>();
380         int level = this.getLevel();
381         while (level != 0) {
382             try {
383                 allAncestors.add(getAncestor(--level));
384             }
385             catch (AccessDeniedException e) {
386                 // valid
387             }
388         }
389         return allAncestors;
390     }
391 
392     public int getLevel() throws PathNotFoundException, RepositoryException {
393         return this.node.getDepth(); //$NON-NLS-1$
394     }
395 
396     public void orderBefore(String srcName, String beforeName) throws RepositoryException {
397         this.node.orderBefore(srcName, beforeName);
398     }
399 
400     public int getIndex() throws RepositoryException {
401         return this.node.getIndex();
402     }
403 
404     public Node getJCRNode() {
405         return this.node;
406     }
407 
408     public boolean isNodeType(String type) {
409         return isNodeType(this.node, type);
410     }
411 
412     /**
413      * private Helper method to evaluate primary node type of the given node.
414      * @param node
415      * @param type
416      */
417     protected boolean isNodeType(Node node, String type) {
418         try {
419             final String actualType = node.getProperty(ItemType.JCR_PRIMARY_TYPE).getString();
420             // if the node is frozen, and we're not looking specifically for frozen nodes, then we compare with the original node type
421             if (ItemType.NT_FROZENNODE.equals(actualType) && !(ItemType.NT_FROZENNODE.equals(type))) {
422                 final Property p = node.getProperty(ItemType.JCR_FROZEN_PRIMARY_TYPE);
423                 final String s = p.getString();
424                 return s.equalsIgnoreCase(type);
425             } else {
426                 return node.isNodeType(type);
427             }
428         }
429         catch (RepositoryException re) {
430             log.error(re.getMessage());
431             log.debug(re.getMessage(), re);
432             return false;
433         }
434     }
435 
436     public NodeType getNodeType() throws RepositoryException {
437         return this.node.getPrimaryNodeType();
438     }
439 
440     public String getNodeTypeName() throws RepositoryException {
441 
442         if (this.node.hasProperty(ItemType.JCR_FROZEN_PRIMARY_TYPE)) {
443             return this.node.getProperty(ItemType.JCR_FROZEN_PRIMARY_TYPE).getString();
444         }
445         return this.node.getProperty(ItemType.JCR_PRIMARY_TYPE).getString();
446     }
447 
448     public ItemType getItemType() throws RepositoryException {
449         return new ItemType(getNodeTypeName());
450     }
451 
452     public void restore(String versionName, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
453         Access.isGranted(this.hierarchyManager.getAccessManager(), this.getHandle(), Permission.WRITE);
454         Version version = this.getVersionHistory().getVersion(versionName);
455         this.restore(version, removeExisting);
456     }
457 
458     public void restore(Version version, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
459         Access.isGranted(this.hierarchyManager.getAccessManager(), this.getHandle(), Permission.WRITE);
460         VersionManager.getInstance().restore(this, version, removeExisting);
461     }
462 
463     public void restore(Version version, String relPath, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
464         throw new UnsupportedRepositoryOperationException("Not implemented in 3.0 Beta");
465     }
466 
467     public void restoreByLabel(String versionLabel, boolean removeExisting) throws VersionException, UnsupportedRepositoryOperationException, RepositoryException {
468         this.node.restoreByLabel(versionLabel, removeExisting);
469         throw new UnsupportedRepositoryOperationException("Not implemented in 3.0 Beta");
470     }
471 
472     public Version addVersion() throws UnsupportedRepositoryOperationException, RepositoryException {
473         return VersionManager.getInstance().addVersion(this);
474     }
475 
476     public Version addVersion(Rule rule) throws UnsupportedRepositoryOperationException, RepositoryException {
477         return VersionManager.getInstance().addVersion(this, rule);
478     }
479 
480     /**
481      * Returns true if this node is either
482      * <ul>
483      * <li/>versionable and currently checked-out, <li/>non-versionable and its nearest versionable ancestor is
484      * checked-out or <li/>non-versionable and it has no versionable ancestor.
485      * </ul>
486      * Returns false if this node is either
487      * <ul>
488      * <li/>versionable and currently checked-in or <li/>non-versionable and its nearest versionable ancestor is
489      * checked-in.
490      * </ul>
491      * @return true if the node is checked out
492      * @throws RepositoryException
493      */
494     protected boolean isCheckedOut() throws RepositoryException {
495         return this.node.isCheckedOut();
496     }
497 
498     public boolean isModified() {
499         return this.node.isModified();
500     }
501 
502     public VersionHistory getVersionHistory() throws UnsupportedRepositoryOperationException, RepositoryException {
503         return VersionManager.getInstance().getVersionHistory(this);
504     }
505 
506     public VersionIterator getAllVersions() throws UnsupportedRepositoryOperationException, RepositoryException {
507         return VersionManager.getInstance().getAllVersions(this);
508     }
509 
510     public ContentVersion getBaseVersion() throws UnsupportedRepositoryOperationException, RepositoryException {
511         return new ContentVersion(VersionManager.getInstance().getBaseVersion(this), this);
512     }
513 
514     public ContentVersion getVersionedContent(Version version) throws RepositoryException {
515         return new ContentVersion(version, this);
516     }
517 
518     public ContentVersion getVersionedContent(String versionName) throws RepositoryException {
519         return new ContentVersion(VersionManager.getInstance().getVersion(this, versionName), this);
520     }
521 
522     public void removeVersionHistory() throws AccessDeniedException, RepositoryException {
523         Access.isGranted(this.hierarchyManager.getAccessManager(), Path.getAbsolutePath(node.getPath()), Permission.WRITE);
524         VersionManager.getInstance().removeVersionHistory(this.node.getUUID());
525     }
526 
527     public void save() throws RepositoryException {
528         this.node.save();
529     }
530 
531     public void delete() throws RepositoryException {
532         Access.isGranted(this.hierarchyManager.getAccessManager(), Path.getAbsolutePath(this.node.getPath()), Permission.REMOVE);
533         String nodePath = Path.getAbsolutePath(this.node.getPath());
534         ItemType nodeType = this.getItemType();
535         this.node.remove();
536         AuditLoggingUtil.log( AuditLoggingUtil.ACTION_DELETE, hierarchyManager.getName(), nodeType, nodePath);
537     }
538 
539 
540     public void refresh(boolean keepChanges) throws RepositoryException {
541         this.node.refresh(keepChanges);
542     }
543 
544     public String getUUID() {
545         try {
546             return this.node.getUUID();
547         }
548         catch (UnsupportedOperationException e) {
549             log.error(e.getMessage());
550         }
551         catch (RepositoryException re) {
552             log.error("Exception caught", re);
553         }
554         return StringUtils.EMPTY;
555     }
556 
557     public void addMixin(String type) throws RepositoryException {
558         Access.isGranted(this.hierarchyManager.getAccessManager(), Path.getAbsolutePath(this.node.getPath()), Permission.SET);
559         // TODO: there seems to be bug somewhere as we are able to add mixins even when the method below returns false
560         if (!this.node.canAddMixin(type)) {
561             log.debug("Node - " + this.node.getPath() + " does not allow mixin type - " + type); //$NON-NLS-1$ //$NON-NLS-2$
562         }
563         try {
564             this.node.addMixin(type);
565         } catch (Exception e) {
566             log.error("Failed to add  mixin type - " + type + " to a node " + this.node.getPath()); //$NON-NLS-1$ //$NON-NLS-2$
567         }
568     }
569 
570     public void removeMixin(String type) throws RepositoryException {
571         Access.isGranted(this.hierarchyManager.getAccessManager(), Path.getAbsolutePath(this.node.getPath()), Permission.SET);
572         this.node.removeMixin(type);
573     }
574 
575     public NodeType[] getMixinNodeTypes() throws RepositoryException {
576         return this.node.getMixinNodeTypes();
577     }
578 
579     public Lock lock(boolean isDeep, boolean isSessionScoped) throws LockException, RepositoryException {
580         return this.node.lock(isDeep, isSessionScoped);
581     }
582 
583     public Lock lock(boolean isDeep, boolean isSessionScoped, long yieldFor) throws LockException, RepositoryException {
584         long finalTime = System.currentTimeMillis() + yieldFor;
585         LockException lockException = null;
586         while (System.currentTimeMillis() <= finalTime) {
587             try {
588                 return this.node.lock(isDeep, isSessionScoped);
589             }
590             catch (LockException e) {
591                 // its not an exception yet, still got time
592                 lockException = e;
593             }
594             Thread.yield();
595         }
596         // could not get lock
597         throw lockException;
598     }
599 
600     public Lock getLock() throws LockException, RepositoryException {
601         return this.node.getLock();
602     }
603 
604     public void unlock() throws LockException, RepositoryException {
605         this.node.unlock();
606     }
607 
608     public boolean holdsLock() throws RepositoryException {
609         return this.node.holdsLock();
610     }
611 
612     public boolean isLocked() throws RepositoryException {
613         return this.node.isLocked();
614     }
615 
616     public boolean hasMetaData() {
617         try {
618             return this.node.hasNode("MetaData");
619         }
620         catch (RepositoryException re) {
621             log.debug(re.getMessage(), re);
622         }
623         return false;
624     }
625 
626     public boolean hasMixin(String mixinName) throws RepositoryException {
627         if (StringUtils.isBlank(mixinName)) {
628             throw new IllegalArgumentException("Mixin name can't be empty.");
629         }
630         for (NodeType type : getMixinNodeTypes()) {
631             if (mixinName.equals(type.getName())) {
632                 return true;
633             }
634         }
635         return false;
636     }
637 }