View Javadoc
1   /**
2    * This file Copyright (c) 2003-2016 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.version;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.Path;
38  import info.magnolia.cms.security.JCRSessionOp;
39  import info.magnolia.cms.security.PermissionUtil;
40  import info.magnolia.cms.util.Rule;
41  import info.magnolia.context.MgnlContext;
42  import info.magnolia.context.SystemContext;
43  import info.magnolia.jcr.predicate.RuleBasedNodePredicate;
44  import info.magnolia.jcr.util.NodeTypes;
45  import info.magnolia.jcr.util.NodeUtil;
46  import info.magnolia.jcr.util.VersionUtil;
47  import info.magnolia.repository.RepositoryConstants;
48  import info.magnolia.repository.RepositoryManager;
49  import info.magnolia.repository.definition.RepositoryDefinition;
50  
51  import java.io.ByteArrayInputStream;
52  import java.io.ByteArrayOutputStream;
53  import java.io.IOException;
54  import java.io.InvalidClassException;
55  import java.io.ObjectInput;
56  import java.io.ObjectInputStream;
57  import java.io.ObjectOutput;
58  import java.io.ObjectOutputStream;
59  import java.util.ArrayList;
60  import java.util.Collection;
61  import java.util.Iterator;
62  import java.util.List;
63  
64  import javax.inject.Inject;
65  import javax.jcr.ItemNotFoundException;
66  import javax.jcr.Node;
67  import javax.jcr.NodeIterator;
68  import javax.jcr.PathNotFoundException;
69  import javax.jcr.RepositoryException;
70  import javax.jcr.Session;
71  import javax.jcr.UnsupportedRepositoryOperationException;
72  import javax.jcr.Value;
73  import javax.jcr.nodetype.NodeType;
74  import javax.jcr.version.Version;
75  import javax.jcr.version.VersionHistory;
76  import javax.jcr.version.VersionIterator;
77  import javax.jcr.version.VersionManager;
78  
79  import org.apache.commons.codec.binary.Base64;
80  import org.apache.commons.io.IOUtils;
81  import org.apache.jackrabbit.JcrConstants;
82  import org.slf4j.Logger;
83  import org.slf4j.LoggerFactory;
84  
85  /**
86   * This version manager uses an extra workspace to manage the versions. The
87   * workspace maintains a flat hierarchy. The content is then finally versioned
88   * using JCR versioning which also copies the sub-nodes.
89   *
90   * The mix:versionable is only added on the top level nodes.
91   */
92  public abstract class BaseVersionManager {
93  
94      private static final Logger log = LoggerFactory.getLogger(BaseVersionManager.class);
95  
96      /**
97       * Name of the workspace.
98       */
99      public static final String VERSION_WORKSPACE = "mgnlVersion";
100 
101     /**
102      * Node which contains stubs for referenced nodes. We have to copy them to the workspace as well.
103      */
104     public static final String TMP_REFERENCED_NODES = "mgnl:tmpReferencedNodes";
105 
106     /**
107      * Sub-node containing the data used for the version/restore process.
108      */
109     public static final String SYSTEM_NODE = "mgnl:versionMetaData";
110 
111     /**
112      * Property name for collection rule. The rule defines which sub-nodes belong to a node: page and paragraphs.
113      */
114     public static final String PROPERTY_RULE = "Rule";
115 
116     /**
117      * JCR version store root.
118      */
119     protected static final String ROOT_VERSION = "jcr:rootVersion";
120 
121     /**
122      * Workspace of the source node.
123      */
124     public static final String SOURCE_WORKSPACE = "sourceWorkspace";
125 
126     private Collection<String> versionWorkspaces = new ArrayList<>();
127 
128     private final SystemContext systemContext;
129     private final RepositoryManager repositoryManager;
130     private final CopyUtil copyUtil;
131 
132     @Inject
133     public BaseVersionManager(SystemContext systemContext, RepositoryManager repositoryManager, CopyUtil copyUtil) {
134         this.systemContext = systemContext;
135         this.repositoryManager = repositoryManager;
136         this.copyUtil = copyUtil;
137         for (RepositoryDefinition repositoryDefinition : repositoryManager.getRepositoryDefinitions()) {
138             String repositoryId = repositoryDefinition.getName();
139             String workspaceName = repositoryId + "-" + RepositoryConstants.VERSION_STORE;
140             if (repositoryManager.getWorkspaceMapping(workspaceName) != null) {
141                 versionWorkspaces.add(workspaceName);
142             } else {
143                 throw new RuntimeException(String.format("Something went wrong, version workspace for repository %s does not exist.", repositoryId));
144             }
145         }
146     }
147 
148     /**
149      * Create structure needed for version store workspace.
150      *
151      * @throws RepositoryException if unable to create magnolia system structure
152      */
153     protected void createInitialStructure() throws RepositoryException {
154         for (String workspaceName : versionWorkspaces) {
155             Session session = systemContext.getJCRSession(workspaceName);
156             try {
157                 Node tmp = session.getNode("/" + TMP_REFERENCED_NODES);
158                 // remove nodes if they are no longer referenced within this workspace
159                 NodeIterator children = tmp.getNodes();
160                 while (children.hasNext()) {
161                     Node child = children.nextNode();
162                     if (child.getReferences().getSize() < 1) {
163                         child.remove();
164                     }
165                 }
166             } catch (PathNotFoundException e) {
167                 session.getRootNode().addNode(TMP_REFERENCED_NODES, NodeTypes.System.NAME);
168             }
169             session.save();
170         }
171     }
172 
173     /**
174      * Add version of the specified node and all child nodes while ignoring the same node type.
175      */
176     public synchronized Version addVersion(Node node) throws RepositoryException {
177         Rule rule = new Rule(VersionUtil.getNodeTypeName(node) + "," + NodeTypes.System.NAME, ",");
178         rule.reverse();
179         return this.addVersion(node, rule);
180     }
181 
182     /**
183      * Add version of the specified node and all child nodes based on the given <code>Rule</code>. Reads the <code>userName</code>
184      * from the current context.
185      */
186     public synchronized Version addVersion(final Node node, final Rule rule) throws RepositoryException {
187         final String userName = getSafelyUserNameFromMgnlContext();
188         return addVersion(node, rule, userName);
189     }
190 
191     /**
192      * Add version of the specified node and all child nodes based on the given <code>Rule</code> and <code>userName</code>.
193      */
194     public synchronized Version addVersion(final Node node, final Rule rule, final String userName) throws RepositoryException {
195         final String workspaceName = node.getSession().getWorkspace().getName();
196         Version version = MgnlContext.doInSystemContext(new JCRSessionOp<Version>(workspaceName) {
197 
198             @Override
199             public Version exec(Session session) throws RepositoryException {
200                 try {
201                     return createVersion(session.getNodeByIdentifier(node.getIdentifier()), rule, userName);
202                 } catch (RepositoryException re) {
203                     // since add version is synchronized on a singleton object, its safe to revert all changes made in
204                     // the session attached to workspace - mgnlVersion
205                     log.error("failed to copy versionable node to version store, reverting all changes made in this session", re);
206                     String versionWorkspaceName = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
207                     // We're in SystemContext
208                     MgnlContext.getJCRSession(versionWorkspaceName).refresh(false);
209                     throw re;
210                 }
211 
212             }
213         });
214         return version;
215     }
216 
217     private String getSafelyUserNameFromMgnlContext() {
218         String userName = "";
219         if (MgnlContext.getUser() != null) {
220             userName = MgnlContext.getUser().getName();
221         }
222         return userName;
223     }
224 
225     /**
226      * Create version of the specified node and all child nodes based on the given <code>Rule</code>.
227      * This will return null if can't create new version in case when max version index is lower then one
228      * or when content is marked for deletion.
229      *
230      * @param node to be versioned
231      * @return newly created version node
232      * @throws UnsupportedOperationException if repository implementation does not support Versions API
233      * @throws javax.jcr.RepositoryException if any repository error occurs
234      */
235     protected Version createVersion(Node node, Rule rule, String userName) throws RepositoryException {
236         if (isInvalidMaxVersions()) {
237             log.debug("Ignore create version, MaxVersionIndex < 1");
238             return null;
239         }
240         if (node.isNodeType(NodeTypes.Deleted.NAME)) {
241             log.debug("Don't create version for content marked as deleted");
242             return null;
243         }
244 
245         copyUtil.copyToVersion(node, new RuleBasedNodePredicate(rule));
246         Node versionedNode = this.getVersionedNode(node);
247 
248         checkAndAddMixin(versionedNode);
249         Node systemInfo = this.getSystemNode(versionedNode);
250         // add serialized rule which was used to create this version
251         ByteArrayOutputStream out = new ByteArrayOutputStream();
252         try {
253             ObjectOutput objectOut = new ObjectOutputStream(out);
254             objectOut.writeObject(rule);
255             objectOut.flush();
256             objectOut.close();
257             // PROPERTY_RULE is not a part of MetaData to allow versioning of node types which does NOT support MetaData
258             systemInfo.setProperty(PROPERTY_RULE, new String(Base64.encodeBase64(out.toByteArray())));
259         } catch (IOException e) {
260             throw new RepositoryException("Unable to add serialized Rule to the versioned content");
261         }
262         // add all system properties for this version
263         systemInfo.setProperty(ContentVersion.VERSION_USER, userName);
264         systemInfo.setProperty(ContentVersion.NAME, node.getName());
265         systemInfo.setProperty(SOURCE_WORKSPACE, node.getSession().getWorkspace().getName());
266 
267         versionedNode.save();
268 
269         // add version
270         VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
271         Version newVersion = versionManager.checkin(versionedNode.getPath());
272         versionManager.checkout(versionedNode.getPath());
273 
274         node.addMixin(NodeTypes.HasVersion.NAME);
275         node.getSession().save();
276 
277         try {
278             this.setMaxVersionHistory(versionedNode);
279         } catch (RepositoryException re) {
280             log.error("Failed to limit version history to the maximum configured", re);
281             log.error("New version has already been created");
282         }
283 
284 
285         return newVersion;
286     }
287 
288     /**
289      * Check if max version index is lower then one.
290      */
291     public abstract boolean isInvalidMaxVersions();
292 
293     /**
294      * Get node from version store.
295      */
296     public synchronized Node getVersionedNode(Node node) throws RepositoryException {
297         try {
298             final String versionWorkspace = VersionUtil.getVersionWorkspaceForNode(repositoryManager, node);
299             final Session versionSession = new MgnlVersionSessionDecorator(node.getSession().getWorkspace().getName()).wrapSession(systemContext.getJCRSession(versionWorkspace));
300             return versionSession.getNodeByIdentifier(node.getIdentifier());
301         } catch (ItemNotFoundException e) {
302             // node is not versioned yet
303             return null;
304         }
305     }
306 
307     /**
308      * Get node from version store.
309      */
310     protected Node getVersionedNode(Session session, String uuid) throws RepositoryException {
311         try {
312             return session.getNodeByIdentifier(uuid);
313         } catch (ItemNotFoundException e) {
314             // node is not versioned yet
315             return null;
316         }
317     }
318 
319     /**
320      * Set version history to max version possible.
321      *
322      * @throws RepositoryException if failed to get VersionHistory or fail to remove
323      */
324     public abstract void setMaxVersionHistory(Node node) throws RepositoryException;
325 
326     /**
327      * Get history of this node as recorded in the version store.
328      *
329      * @return version history of the given node
330      * @throws UnsupportedOperationException if repository implementation does not support Versions API
331      * @throws javax.jcr.RepositoryException if any repository error occurs
332      */
333     public synchronized VersionHistory getVersionHistory(Node node) throws RepositoryException {
334         try {
335             Node versionedNode = this.getVersionedNode(node);
336             if (versionedNode == null) {
337                 // node does not exist in version store so no version history
338                 log.debug("No VersionHistory found for {} node.", node);
339                 return null;
340             }
341             VersionHistory versionHistory = versionedNode.getVersionHistory();
342             VersionIterator versionIterator = versionHistory.getAllVersions();
343             long filteredSize = versionIterator.getSize();
344             long unfilteredSize = ((MgnlVersionSessionDecorator.MgnlVersionSessionVersionIteratorWrapper) versionIterator).getUnfilteredSize();
345             // we have just root version for the passed node, but there are other versions from one or more workspaces
346             // so to preserve expected behavior, return null
347             if (filteredSize == 1 && unfilteredSize > filteredSize) {
348                 return null;
349             }
350             return versionHistory;
351         } catch (UnsupportedRepositoryOperationException e) {
352             log.debug("Node {} is not versionable.", node);
353             // node is not versionable or underlying repo doesn't support versioning.
354             return null;
355         }
356     }
357 
358     /**
359      * Get named version.
360      *
361      * @throws UnsupportedOperationException if repository implementation does not support Versions API
362      * @throws javax.jcr.RepositoryException if any repository error occurs
363      */
364     public synchronized Version getVersion(Node node, String name) throws RepositoryException {
365         VersionHistory history = this.getVersionHistory(node);
366         if (history != null) {
367             return new VersionedNode(history.getVersion(name), node);
368         }
369         log.error("Node {} was never versioned", node.getPath());
370         return null;
371     }
372 
373     /**
374      * Returns the current base version of given node.
375      */
376     public Version getBaseVersion(Node node) throws UnsupportedOperationException, RepositoryException {
377         VersionIterator versionIterator = getAllVersions(node);
378         Version baseVersion = null;
379         if (versionIterator != null) {
380             while (versionIterator.hasNext()) {
381                 Version version = versionIterator.nextVersion();
382                 if (baseVersion == null) {
383                     baseVersion = version;
384                 } else if (baseVersion.getCreated().compareTo(version.getCreated()) < 0) {
385                     baseVersion = version;
386                 }
387             }
388         }
389         if (baseVersion == null) {
390             throw new RepositoryException("Node " + node.getPath() + " was never versioned");
391         }
392         return baseVersion;
393     }
394 
395     /**
396      * Get all versions.
397      *
398      * @return Version iterator retrieved from version history
399      * @throws UnsupportedOperationException if repository implementation does not support Versions API
400      * @throws javax.jcr.RepositoryException if any repository error occurs
401      */
402     public synchronized VersionIterator getAllVersions(Node node) throws RepositoryException {
403         Node versionedNode = this.getVersionedNode(node);
404         if (versionedNode == null) {
405             // node does not exist in version store so no versions
406             return null;
407         }
408         VersionHistory versionHistory = getVersionHistory(node);
409         if (versionHistory == null) {
410             // does not have any versions in version store
411             return null;
412         }
413         return versionHistory.getAllVersions();
414     }
415 
416     /**
417      * Restore specified version.
418      *
419      * @param node to be restored
420      * @param version to be used
421      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
422      * node's version history
423      * @throws javax.jcr.RepositoryException if an error occurs
424      * @deprecated since 4.5 use {@link #restore(Node, Version, boolean)} instead
425      */
426     @Deprecated
427     public synchronized void restore(Content node, Version version, boolean removeExisting) throws RepositoryException {
428         restore(node.getJCRNode(), version, removeExisting);
429     }
430 
431     /**
432      * Restore specified version.
433      *
434      * @param node to be restored
435      * @param version to be used
436      * @throws javax.jcr.version.VersionException if the specified <code>versionName</code> does not exist in this
437      * node's version history
438      * @throws javax.jcr.RepositoryException if an error occurs
439      */
440     public synchronized void restore(final Node node, Version version, boolean removeExisting) throws RepositoryException {
441         // FYI: restore is done in SC!!! Check permissions manually
442         PermissionUtil.verifyIsGrantedOrThrowException(node.getSession(), node.getPath(), Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE);
443 
444         // get the cloned node from version store
445         final Node versionedNode = this.getVersionedNode(node);
446 
447         final Version unwrappedVersion;
448         if (version instanceof VersionedNode) {
449             unwrappedVersion = VersionUtil.unwrap(((VersionedNode) version).unwrap());
450         } else {
451             unwrappedVersion = VersionUtil.unwrap(version);
452         }
453 
454         VersionManager versionManager = versionedNode.getSession().getWorkspace().getVersionManager();
455         versionManager.restore(unwrappedVersion, removeExisting);
456         versionManager.checkout(versionedNode.getPath());
457         MgnlContext.doInSystemContext(new JCRSessionOp<Void>(versionedNode.getSession().getWorkspace().getName()) {
458 
459             @Override
460             public Void exec(Session session) throws RepositoryException {
461                 //mixins are NOT restored automatically
462                 List<String> mixins = new ArrayList<>();
463                 for (Value v : unwrappedVersion.getNode(JcrConstants.JCR_FROZENNODE).getProperty("jcr:frozenMixinTypes").getValues()) {
464                     mixins.add(v.getString());
465                 }
466 
467                 final Node systemVersionedNode = session.getNodeByIdentifier(versionedNode.getUUID());
468                 for (NodeType nt : versionedNode.getMixinNodeTypes()) {
469                     if (!mixins.remove(nt.getName())) {
470                         systemVersionedNode.removeMixin(nt.getName());
471                     }
472                 }
473                 for (String mix : mixins) {
474                     systemVersionedNode.addMixin(mix);
475                 }
476                 systemVersionedNode.save();
477 
478                 try {
479                     // using system context here is forced by the fact that JR will use same context to also check source (mgnlVersion) repo READ permission and ordinary user has no rights in this workspace
480                     Session sysDestinationSession = MgnlContext.getJCRSession(node.getSession().getWorkspace().getName());
481                     log.debug("restoring info:{}:{}", sysDestinationSession.getWorkspace().getName(), node.getPath());
482                     Node destinationNode = sysDestinationSession.getNode(node.getPath());
483                     // if restored, update original node with the restored node and its subtree
484                     Rule rule = getUsedFilter(versionedNode);
485                     try {
486                         copyUtil.copyFromVersion(versionedNode, destinationNode, new RuleBasedNodePredicate(rule));
487                         if (NodeUtil.hasMixin(destinationNode, NodeTypes.Deleted.NAME)) {
488                             destinationNode.removeMixin(NodeTypes.Deleted.NAME);
489                         }
490                         // next line is required because restore is done via clone op that preserves last modification date otherwise.
491                         NodeTypes.LastModified.update(destinationNode);
492                         destinationNode.save();
493                         // node was updated in system context and we should make sure it is notified of the changes
494                         node.refresh(false);
495                     } catch (RepositoryException re) {
496                         log.debug("error during restore: {}", re.getMessage(), re);
497                         log.error("failed to restore versioned node, reverting all changes make to this node");
498                         destinationNode.refresh(false);
499                         throw re;
500                     }
501                 } catch (IOException e) {
502                     throw new RepositoryException(e);
503                 } catch (ClassNotFoundException e) {
504                     throw new RepositoryException(e);
505                 }
506                 return null;
507             }
508         });
509     }
510 
511     /**
512      * Removes all versions of the node associated with given UUID.
513      *
514      * @throws RepositoryException if fails to remove versioned node from the version store
515      */
516     public synchronized void removeVersionHistory(final Node node) throws RepositoryException {
517         final String nodePath = Path.getAbsolutePath(node.getPath());
518         final Session session = node.getSession();
519         final String workspaceName = session.getWorkspace().getName();
520         log.debug("removing {} from {}", nodePath, workspaceName);
521         PermissionUtil.verifyIsGrantedOrThrowException(session, nodePath, Session.ACTION_REMOVE);
522 
523         if (!(workspaceName.endsWith("-" + RepositoryConstants.VERSION_STORE) || workspaceName.endsWith("-" + RepositoryConstants.SYSTEM))) {
524             VersionHistory versionHistory = getVersionHistory(node);
525             if (versionHistory != null) {
526                 VersionIterator versionIterator = versionHistory.getAllVersions();
527                 long filteredSize = versionIterator.getSize();
528                 long unfilteredSize = ((MgnlVersionSessionDecorator.MgnlVersionSessionVersionIteratorWrapper) versionIterator).getUnfilteredSize();
529                 if (filteredSize == unfilteredSize) {
530                     Node versioned = getVersionedNode(node);
531                     versioned.remove();
532                     versioned.getSession().save();
533                 } else {
534                     Iterator<Version> iterator = ((MgnlVersionSessionDecorator.MgnlVersionSessionVersionIteratorWrapper) versionIterator).getAllVersionsUnfiltered();
535                     while (iterator.hasNext()) {
536                         Version version = iterator.next();
537                         if (version.getFrozenNode().hasNode(SYSTEM_NODE) && version.getFrozenNode().getNode(SYSTEM_NODE).hasProperty(SOURCE_WORKSPACE)) {
538                             String sourceWorkspace = version.getFrozenNode().getNode(SYSTEM_NODE).getProperty(SOURCE_WORKSPACE).getString();
539                             if (!sourceWorkspace.equals(node.getSession().getWorkspace().getName())) {
540                                 Node nodeToRestore = systemContext.getJCRSession(sourceWorkspace).getNodeByIdentifier(node.getIdentifier());
541                                 Version base = getBaseVersion(nodeToRestore);
542                                 try {
543                                     nodeToRestore.setProperty(NodeTypes.Versionable.COMMENT, "Created by system in order to release base version.");
544                                     nodeToRestore.getSession().save();
545                                     addVersion(nodeToRestore, getUsedFilter(getVersionedNode(base.getFrozenNode())));
546                                 } catch (IOException | ClassNotFoundException e) {
547                                     throw new RepositoryException(e);
548                                 }
549                                 break;
550                             }
551                         }
552                     }
553                 }
554                 versionIterator.nextVersion(); // skip root version
555                 while (versionIterator.hasNext()) {
556                     Version version = versionIterator.nextVersion();
557                     if (version.getName().equals(JcrConstants.JCR_ROOTVERSION)) {
558                         continue;
559                     }
560                     versionHistory.removeVersion(version.getName());
561                 }
562             }
563             session.save();
564         }
565     }
566 
567     public boolean hasVersion(Node node) throws RepositoryException {
568         return NodeUtil.hasMixin(node, NodeTypes.HasVersion.NAME);
569     }
570 
571     /**
572      * Verifies the existence of the mix:versionable and adds it if not.
573      */
574     protected void checkAndAddMixin(Node node) throws RepositoryException {
575         if (!node.isNodeType("mix:versionable")) {
576             log.debug("Add mix:versionable");
577             node.addMixin("mix:versionable");
578         }
579     }
580 
581     /**
582      * Get Rule used for this version.
583      */
584     protected Rule getUsedFilter(Node versionedNode) throws IOException, ClassNotFoundException, RepositoryException {
585         // if restored, update original node with the restored node and its subtree
586         ByteArrayInputStream inStream = null;
587         try {
588             String ruleString = this.getSystemNode(versionedNode).getProperty(PROPERTY_RULE).getString();
589             inStream = new ByteArrayInputStream(Base64.decodeBase64(ruleString.getBytes()));
590             ObjectInput objectInput = new ObjectInputStream(inStream);
591             return (Rule) objectInput.readObject();
592         } catch (InvalidClassException e) {
593             log.debug(e.getMessage());
594             log.debug("Will return default rule -> all child nodes while ignoring versionedNode nodeType.");
595             Rule rule = new Rule(VersionUtil.getNodeTypeName(versionedNode) + "," + NodeTypes.System.NAME, ",");
596             rule.reverse();
597             return rule;
598         } catch (IOException e) {
599             throw e;
600         } catch (ClassNotFoundException e) {
601             throw e;
602         } finally {
603             IOUtils.closeQuietly(inStream);
604         }
605     }
606 
607     /**
608      * Get the Magnolia system node created under the given node.
609      *
610      * @throws RepositoryException if failed to create system node
611      */
612     protected synchronized Node getSystemNode(Node node) throws RepositoryException {
613         if (node.hasNode(SYSTEM_NODE)) {
614             return node.getNode(SYSTEM_NODE);
615         }
616         return node.addNode(SYSTEM_NODE, NodeTypes.System.NAME);
617     }
618 }