View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 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.util;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.HierarchyManager;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.core.Path;
40  import info.magnolia.cms.core.Content.ContentFilter;
41  import info.magnolia.cms.security.AccessDeniedException;
42  import info.magnolia.context.MgnlContext;
43  
44  import java.io.File;
45  import java.io.FileInputStream;
46  import java.io.FileOutputStream;
47  import java.io.IOException;
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.Comparator;
51  import java.util.Iterator;
52  import java.util.List;
53  import java.util.regex.Matcher;
54  import java.util.regex.Pattern;
55  
56  import javax.jcr.ImportUUIDBehavior;
57  import javax.jcr.PathNotFoundException;
58  import javax.jcr.RepositoryException;
59  import javax.jcr.Session;
60  
61  import org.apache.commons.io.FileUtils;
62  import org.apache.commons.io.IOUtils;
63  import org.apache.commons.lang.StringUtils;
64  import org.slf4j.Logger;
65  import org.slf4j.LoggerFactory;
66  
67  /**
68   * Some easy to use methods to handle with Content objects.
69   */
70  public class ContentUtil {
71      private final static Logger log = LoggerFactory.getLogger(ContentUtil.class);
72  
73      /**
74       * Content filter accepting everything.
75       */
76      public static ContentFilter ALL_NODES_CONTENT_FILTER = new ContentFilter() {
77  
78          public boolean accept(Content content) {
79              return true;
80          }
81      };
82  
83      /**
84       * Content filter accepting everything exept nodes with namespace jcr (version and system store).
85       */
86      public static ContentFilter ALL_NODES_EXCEPT_JCR_CONTENT_FILTER = new ContentFilter() {
87  
88          public boolean accept(Content content) {
89              return !content.getName().startsWith("jcr:");
90          }
91      };
92  
93      /**
94       * Content filter accepting everything except meta data and jcr types.
95       */
96      public static ContentFilter EXCLUDE_META_DATA_CONTENT_FILTER = new ContentFilter() {
97          public boolean accept(Content content) {
98              return !content.getName().startsWith("jcr:") && !content.isNodeType(ItemType.NT_METADATA);
99          }
100     };
101 
102     /**
103      * Content filter accepting all nodes with a nodetype of namespace mgnl.
104      */
105     public static ContentFilter MAGNOLIA_FILTER = new ContentFilter() {
106         public boolean accept(Content content) {
107 
108             try {
109                 String nodetype = content.getNodeType().getName();
110                 // export only "magnolia" nodes
111                 return nodetype.startsWith("mgnl:");
112             }
113             catch (RepositoryException e) {
114                 log.error("Unable to read nodetype for node {}", content.getHandle());
115             }
116             return false;
117         }
118     };
119 
120     /**
121      * Used in {@link #visit(Content)} to visit the hierarchy.
122      * @version $Id: ContentUtil.java 48147 2011-08-10 12:52:05Z gjoseph $
123      */
124     // TODO : throws RepositoryException or none, but not Exception !?
125     public static interface Visitor {
126         void visit(Content node) throws Exception;
127     }
128 
129     /**
130      * Used in {@link #visit(Content)} if the visitor wants to use post order.
131      */
132     public static interface PostVisitor extends Visitor {
133         void postVisit(Content node) throws Exception;
134     }
135 
136 
137     /**
138      * Returns a Content object of the named repository or null if not existing.
139      * @return null if not found
140      */
141     public static Content getContent(String repository, String path) {
142         try {
143             return MgnlContext.getHierarchyManager(repository).getContent(path);
144         }
145         catch (RepositoryException e) {
146             return null;
147         }
148     }
149 
150     /**
151      * @return null if not found
152      */
153     public static Content getContentByUUID(String repository, String uuid) {
154         try {
155             return MgnlContext.getHierarchyManager(repository).getContentByUUID(uuid);
156         }
157         catch (RepositoryException e) {
158             return null;
159         }
160     }
161 
162 
163     /**
164      * Get the node or null if not exists.
165      * @param node
166      * @param name
167      * @return the sub node
168      */
169     public static Content getContent(Content node, String name) {
170         try {
171             return node.getContent(name);
172         }
173         catch (RepositoryException e) {
174             return null;
175         }
176     }
177 
178     /**
179      * If the node doesn't exist just create it. Attention the method does not save the newly created node.
180      */
181     public static Content getOrCreateContent(Content node, String name, ItemType contentType) throws AccessDeniedException, RepositoryException{
182         return getOrCreateContent(node, name, contentType, false);
183     }
184 
185     /**
186      * If the node doesn't exist just create it. If the parameter save is true the parent node is saved.
187      */
188     public static Content getOrCreateContent(Content node, String name, ItemType contentType, boolean save)
189         throws AccessDeniedException, RepositoryException {
190         Content res = null;
191         try {
192             res = node.getContent(name);
193         }
194         catch (PathNotFoundException e) {
195             res = node.createContent(name, contentType);
196             if(save){
197                 res.getParent().save();
198             }
199         }
200         return res;
201     }
202 
203     /**
204      * Get a subnode case insensitive.
205      * @param node
206      * @param name
207      * @return
208      */
209     public static Content getCaseInsensitive(Content node, String name) {
210         if (name == null || node == null) {
211             return null;
212         }
213         name = name.toLowerCase();
214         for (Content child : node.getChildren(ALL_NODES_CONTENT_FILTER)) {
215             if (child.getName().toLowerCase().equals(name)) {
216                 return child;
217             }
218         }
219         return null;
220     }
221 
222     /**
223      * Get all children recursively (content and contentnode).
224      */
225     public static List<Content> collectAllChildren(Content node) {
226         List<Content> nodes = new ArrayList<Content>();
227         return collectAllChildren(nodes, node, new ItemType[]{ItemType.CONTENT, ItemType.CONTENTNODE});
228     }
229 
230     /**
231      * Get all children using a filter.
232      * @param node
233      * @param filter
234      * @return list of all found nodes
235      */
236     public static List<Content> collectAllChildren(Content node, ContentFilter filter) {
237         List<Content> nodes = new ArrayList<Content>();
238         return collectAllChildren(nodes, node, filter);
239     }
240 
241     /**
242      * Get the children using a filter.
243      * @param nodes collection of already found nodes
244      */
245     private static List<Content> collectAllChildren(List<Content> nodes, Content node, ContentFilter filter) {
246         // get filtered sub nodes first
247         Collection<Content> children = node.getChildren(filter);
248         for (Content child : children) {
249             nodes.add(child);
250         }
251 
252         // get all children to find recursively
253         Collection<Content> allChildren = node.getChildren(EXCLUDE_META_DATA_CONTENT_FILTER);
254 
255         // recursion
256         for (Content child : allChildren) {
257             collectAllChildren(nodes, child, filter);
258         }
259 
260         return nodes;
261     }
262 
263     /**
264      * Get all children of a particular type.
265      */
266     public static List<Content> collectAllChildren(Content node, ItemType type) {
267         List<Content> nodes = new ArrayList<Content>();
268         return collectAllChildren(nodes, node, new ItemType[]{type});
269     }
270 
271     /**
272      * Returns all children (not recursively) independent of there type.
273      */
274     public static Collection<Content> getAllChildren(Content node){
275         return node.getChildren(EXCLUDE_META_DATA_CONTENT_FILTER);
276     }
277 
278     /**
279      * Returns all children (not recursively) independent of there type.
280      */
281     public static Collection<Content> getAllChildren(Content node, Comparator<Content> comp){
282         return node.getChildren(EXCLUDE_META_DATA_CONTENT_FILTER, comp);
283     }
284 
285     /**
286      * Get all children of a particular type.
287      */
288     public static List<Content> collectAllChildren(Content node, ItemType[] types) {
289         List<Content> nodes = new ArrayList<Content>();
290         return collectAllChildren(nodes, node, types);
291     }
292 
293     /**
294      * Get all subnodes recursively and add them to the nodes collection.
295      */
296     private static List<Content> collectAllChildren(List<Content> nodes, Content node, ItemType[] types) {
297         for (int i = 0; i < types.length; i++) {
298             ItemType type = types[i];
299 
300             Collection<Content> children = node.getChildren(type);
301             for (Content child : children) {
302                 nodes.add(child);
303                 collectAllChildren(nodes, child, types);
304             }
305         }
306         return nodes;
307     }
308 
309     /**
310      * Returns the first found <strong>ancestor</strong> of the given node which is of the given type,
311      * or the given node itself, it is of the given type.
312      */
313     public static Content getAncestorOfType(final Content firstNode, final String nodeType) throws RepositoryException {
314         Content node = firstNode;
315         while (!node.isNodeType(nodeType)) {
316             node = node.getParent();
317             if (node.getLevel() == 0) {
318                 throw new RepositoryException("No ancestor of type " + nodeType + " found for " + firstNode);
319             }
320         }
321         return node;
322     }
323 
324     /**
325      * Convenient method to order a node before a target node.
326      */
327     public static void orderBefore(Content nodeToMove, String targetNodeName) throws RepositoryException{
328         nodeToMove.getParent().orderBefore(nodeToMove.getName(), targetNodeName);
329     }
330 
331     /**
332      * Convenient method for ordering a node after a specific target node. This is not that simple as jcr only supports ordering before a node.
333      */
334     public static void orderAfter(Content nodeToMove, String targetNodeName) throws RepositoryException{
335         Content parent = nodeToMove.getParent();
336         Boolean readyToMove = false;
337 
338         Collection<Content> children = new ArrayList<Content>(ContentUtil.getAllChildren(parent));
339         for (Content child : children) {
340             if(readyToMove){
341                 parent.orderBefore(nodeToMove.getName(), child.getName());
342                 readyToMove = false;
343                 break;
344             }
345 
346             if(child.getName().equals(targetNodeName)){
347                 readyToMove = true;
348             }
349         }
350 
351         if(readyToMove){
352             for (Content child : children){
353                 if(!nodeToMove.getName().equals(child.getName())){
354                     parent.orderBefore(child.getName(), nodeToMove.getName());
355                 }
356             }
357         }
358     }
359 
360     public static void orderNodes(Content node, String[] nodes) throws RepositoryException{
361         for (int i = nodes.length - 1; i > 0; i--) {
362             node.orderBefore(nodes[i-1], nodes[i]);
363         }
364     }
365 
366     /**
367      * Uses the passed comparator to create the jcr ordering of the children.
368      */
369     public static void orderNodes(Content node, Comparator<Content> comparator) throws RepositoryException {
370         Collection<Content> children = ContentUtil.getAllChildren(node, comparator);
371         String[] names = new String[children.size()];
372 
373         int i = 0;
374         for (Content childNode : children) {
375             names[i] = childNode.getName();
376             i++;
377         }
378         orderNodes(node, names);
379     }
380 
381     public static void visit(Content node, Visitor visitor) throws Exception{
382         visit(node, visitor, EXCLUDE_META_DATA_CONTENT_FILTER);
383     }
384 
385     public static void visit(Content node, Visitor visitor, ContentFilter filter) throws Exception{
386         visitor.visit(node);
387         for (Content content : node.getChildren(filter)) {
388             visit(content, visitor, filter);
389         }
390         if(visitor instanceof PostVisitor){
391             ((PostVisitor)visitor).postVisit(node);
392         }
393     }
394 
395     public static Content createPath(HierarchyManager hm, String path) throws AccessDeniedException,
396         PathNotFoundException, RepositoryException {
397         return createPath(hm, path, false);
398     }
399 
400     public static Content createPath(HierarchyManager hm, String path, boolean save) throws AccessDeniedException,
401         PathNotFoundException, RepositoryException {
402         return createPath(hm, path, ItemType.CONTENT, save);
403     }
404 
405     public static Content createPath(HierarchyManager hm, String path, ItemType type) throws AccessDeniedException,
406         PathNotFoundException, RepositoryException {
407         return createPath(hm, path, type, false);
408     }
409 
410     public static Content createPath(HierarchyManager hm, String path, ItemType type, boolean save) throws AccessDeniedException,
411         PathNotFoundException, RepositoryException {
412         Content root = hm.getRoot();
413         return createPath(root, path, type, save);
414     }
415 
416     public static Content createPath(Content parent, String path, ItemType type) throws RepositoryException,
417         PathNotFoundException, AccessDeniedException {
418         return createPath(parent, path, type, false);
419     }
420 
421     public static Content createPath(Content parent, String path, ItemType type, boolean save) throws RepositoryException,
422         PathNotFoundException, AccessDeniedException {
423         // remove leading /
424         path = StringUtils.removeStart(path, "/");
425 
426         if (StringUtils.isEmpty(path)) {
427             return parent;
428         }
429 
430         String[] names = path.split("/"); //$NON-NLS-1$
431 
432         for (int i = 0; i < names.length; i++) {
433             String name = names[i];
434             if (parent.hasContent(name)) {
435                 parent = parent.getContent(name);
436             }
437             else {
438                 final Content newNode = parent.createContent(name, type);
439                 if(save){
440                     parent.save();
441                 }
442                 parent = newNode;
443             }
444         }
445         return parent;
446     }
447 
448     public static String uuid2path(String repository, String uuid){
449         if(StringUtils.isNotEmpty(uuid)){
450             HierarchyManager hm = MgnlContext.getHierarchyManager(repository);
451             try {
452                 Content node = hm.getContentByUUID(uuid);
453                 return node.getHandle();
454             }
455             catch (Exception e) {
456                 // return the uuid
457             }
458 
459         }
460         return uuid;
461     }
462 
463     public static String path2uuid(String repository, String path) {
464         if(StringUtils.isNotEmpty(path)){
465             HierarchyManager hm = MgnlContext.getHierarchyManager(repository);
466             try {
467                 Content node = hm.getContent(path);
468                 return node.getUUID();
469             }
470             catch (Exception e) {
471                 // return the uuid
472             }
473 
474         }
475         return path;
476     }
477 
478     public static void deleteAndRemoveEmptyParents(Content node) throws PathNotFoundException, RepositoryException,
479     AccessDeniedException {
480         deleteAndRemoveEmptyParents(node, 0);
481     }
482 
483     public static void deleteAndRemoveEmptyParents(Content node, int level) throws PathNotFoundException, RepositoryException,
484         AccessDeniedException {
485         Content parent = null;
486         if(node.getLevel() != 0){
487              parent = node.getParent();
488         }
489         node.delete();
490         if(parent != null && parent.getLevel()>level && parent.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).size()==0){
491             deleteAndRemoveEmptyParents(parent, level);
492         }
493     }
494 
495     /**
496      * Session based copy operation. As JCR only supports workspace based copies this operation is performed
497      * by using export import operations.
498      */
499     public static void copyInSession(Content src, String dest) throws RepositoryException {
500         final String destParentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(dest, "/"), "/");
501         final String destNodeName = StringUtils.substringAfterLast(dest, "/");
502         final Session session = src.getWorkspace().getSession();
503         try{
504             final File file = File.createTempFile("mgnl", null, Path.getTempDirectory());
505             final FileOutputStream outStream = new FileOutputStream(file);
506             session.exportSystemView(src.getHandle(), outStream, false, false);
507             outStream.flush();
508             IOUtils.closeQuietly(outStream);
509             FileInputStream inStream = new FileInputStream(file);
510             session.importXML(
511                 destParentPath,
512                 inStream,
513                 ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
514             IOUtils.closeQuietly(inStream);
515             file.delete();
516             if(!StringUtils.equals(src.getName(), destNodeName)){
517                 String currentPath;
518                 if(destParentPath.equals("/")){
519                     currentPath = "/" + src.getName();
520                 }
521                 else{
522                     currentPath = destParentPath + "/" + src.getName();
523                 }
524                 session.move(currentPath, dest);
525             }
526         }
527         catch (IOException e) {
528             throw new RepositoryException("Can't copy node " + src + " to " + dest, e);
529         }
530     }
531 
532     /**
533      * Magnolia uses by default workspace move operation to move nodes. This is a util method to move a node inside a session.
534      */
535     public static void moveInSession(Content src, String dest) throws RepositoryException {
536         src.getWorkspace().getSession().move(src.getHandle(), dest);
537     }
538 
539     public static void rename(Content node, String newName) throws RepositoryException{
540         Content parent = node.getParent();
541         String placedBefore = null;
542         for (Iterator<Content> iter = parent.getChildren(node.getNodeTypeName()).iterator(); iter.hasNext();) {
543             Content child = iter.next();
544             if (child.getUUID().equals(node.getUUID())) {
545                 if (iter.hasNext()) {
546                     child = iter.next();
547                     placedBefore = child.getName();
548                 }
549             }
550         }
551 
552         moveInSession(node, PathUtil.createPath(node.getParent().getHandle(), newName));
553 
554         // now set at the same place as before
555         if (placedBefore != null) {
556             parent.orderBefore(newName, placedBefore);
557             parent.save();
558         }
559     }
560 
561     /**
562      * Utility method to change the <code>jcr:primaryType</code> value of a node.
563      * @param node - {@link Content} the node whose type has to be changed
564      * @param newType - {@link ItemType} the new node type to be assigned
565      * @param replaceAll - boolean when <code>true</code> replaces all occurrences
566      * of the old node type. When <code>false</code> replaces only the first occurrence.
567      * @throws RepositoryException
568      */
569     public static void changeNodeType(Content node, ItemType newType, boolean replaceAll) throws RepositoryException{
570         if(node == null){
571             throw new IllegalArgumentException("Content can't be null");
572         }
573         if(newType == null){
574             throw new IllegalArgumentException("ItemType can't be null");
575         }
576         final String oldTypeName = node.getNodeTypeName();
577         final String newTypeName = newType.getSystemName();
578         if(newTypeName.equals(oldTypeName)){
579             log.info("Old node type and new one are the same {}. Nothing to change.", newTypeName);
580             return;
581         }
582         final Pattern nodeTypePattern = Pattern.compile("(<sv:property\\s+sv:name=\"jcr:primaryType\"\\s+sv:type=\"Name\"><sv:value>)("+oldTypeName+")(</sv:value>)");
583         final String replacement = "$1"+newTypeName+"$3";
584 
585         log.debug("pattern is {}", nodeTypePattern.pattern());
586         log.debug("replacement string is {}", replacement);
587         log.debug("replaceAll? {}", replaceAll);
588 
589         final String destParentPath = StringUtils.defaultIfEmpty(StringUtils.substringBeforeLast(node.getHandle(), "/"), "/");
590         final Session session = node.getWorkspace().getSession();
591         FileOutputStream outStream = null;
592         FileInputStream inStream = null;
593         File file = null;
594 
595         try {
596             file = File.createTempFile("mgnl", null, Path.getTempDirectory());
597             outStream = new FileOutputStream(file);
598             session.exportSystemView(node.getHandle(), outStream, false, false);
599             outStream.flush();
600             final String fileContents = FileUtils.readFileToString(file);
601             log.debug("content string is {}", fileContents);
602             final Matcher matcher = nodeTypePattern.matcher(fileContents);
603             String replaced = null;
604 
605             log.debug("starting find&replace...");
606             long start = System.currentTimeMillis();
607             if(matcher.find()) {
608                 log.debug("{} will be replaced", node.getHandle());
609                 if(replaceAll){
610                     replaced = matcher.replaceAll(replacement);
611                 } else {
612                     replaced = matcher.replaceFirst(replacement);
613                 }
614                 log.debug("replaced string is {}", replaced);
615             } else {
616                 log.debug("{} won't be replaced", node.getHandle());
617                 return;
618             }
619             log.debug("find&replace operations took {}ms" + (System.currentTimeMillis() - start) / 1000);
620 
621             FileUtils.writeStringToFile(file, replaced);
622             inStream = new FileInputStream(file);
623             session.importXML(
624                 destParentPath,
625                 inStream,
626                 ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
627 
628         } catch (IOException e) {
629             throw new RepositoryException("Can't replace node " + node.getHandle(), e);
630         } finally {
631             IOUtils.closeQuietly(outStream);
632             IOUtils.closeQuietly(inStream);
633             FileUtils.deleteQuietly(file);
634         }
635     }
636 }