View Javadoc
1   /*
2    * Copyright 2016-2024 Bloomreach B.V. (https://www.bloomreach.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.onehippo.forge.content.exim.core.util;
17  
18  import java.rmi.RemoteException;
19  import java.util.HashMap;
20  import java.util.Map;
21  import java.util.Map.Entry;
22  import java.util.Set;
23  
24  import javax.jcr.ItemNotFoundException;
25  import javax.jcr.Node;
26  import javax.jcr.NodeIterator;
27  import javax.jcr.RepositoryException;
28  import javax.jcr.Session;
29  import javax.jcr.Workspace;
30  
31  import org.apache.commons.lang3.ArrayUtils;
32  import org.apache.commons.lang3.StringUtils;
33  import org.hippoecm.repository.HippoStdNodeType;
34  import org.hippoecm.repository.api.HippoNode;
35  import org.hippoecm.repository.api.HippoNodeType;
36  import org.hippoecm.repository.api.HippoWorkspace;
37  import org.hippoecm.repository.api.StringCodec;
38  import org.hippoecm.repository.api.StringCodecFactory;
39  import org.hippoecm.repository.api.Workflow;
40  import org.hippoecm.repository.api.WorkflowException;
41  import org.hippoecm.repository.api.WorkflowManager;
42  import org.hippoecm.repository.standardworkflow.DefaultWorkflow;
43  import org.hippoecm.repository.standardworkflow.FolderWorkflow;
44  import org.hippoecm.repository.util.JcrUtils;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  /**
49   * Hippo specific node related utilities.
50   */
51  public class HippoNodeUtils {
52  
53      private static Logger log = LoggerFactory.getLogger(HippoNodeUtils.class);
54  
55      /**
56       * Hippo document path prefix.
57       */
58      private static final String DOCUMENT_PATH_PREFIX = "/content/documents/";
59  
60      /**
61       * Hippo gallery path prefix.
62       */
63      private static final String GALLERY_PATH_PREFIX = "/content/gallery/";
64  
65      /**
66       * Hippo asset path prefix.
67       */
68      private static final String ASSET_PATH_PREFIX = "/content/assets/";
69  
70      /**
71       * Hippo Repository specific predefined folder node type name
72       */
73      private static final String DEFAULT_HIPPO_FOLDER_NODE_TYPE = "hippostd:folder";
74  
75      /**
76       * The workflow category name to get a folder workflow. We use threepane as this is the same as the CMS uses
77       */
78      private static final String DEFAULT_HIPPO_FOLDER_WORKFLOW_CATEGORY = "threepane";
79  
80      /**
81       * The workflow category name to add a new document.
82       */
83      private static final String DEFAULT_NEW_DOCUMENT_WORKFLOW_CATEGORY = "new-document";
84  
85      /**
86       * The workflow category name to add a new folder.
87       */
88      private static final String DEFAULT_NEW_FOLDER_WORKFLOW_CATEGORY = "new-folder";
89  
90      /**
91       * The workflow category name to localize the new document
92       */
93      private static final String DEFAULT_WORKFLOW_CATEGORY = "core";
94  
95      /**
96       * The codec which is used for the node names
97       */
98      private static final StringCodec DEFAULT_URI_ENCODING = new StringCodecFactory.UriEncoding();
99  
100     private HippoNodeUtils() {
101     }
102 
103     /**
104      * Return the default {@link StringCodec} used in folder and document node name generation.
105      * @return the default {@link StringCodec} used in folder and document node name generation
106      */
107     public static StringCodec getDefaultUriEncoding() {
108         return DEFAULT_URI_ENCODING;
109     }
110 
111     /**
112      * Finds a child node by {@code childNodeName} and {@code childNodeTypes} under the {@code baseNode}.
113      * @param baseNode base node
114      * @param childNodeName child node name
115      * @param childNodeTypes child node type names
116      * @return a child node by {@code childNodeName} and {@code childNodeTypes} under the {@code baseNode}
117      * @throws RepositoryException if any repository/workflow exception occurs
118      */
119     public static Node getChildNodeOfType(final Node baseNode, final String childNodeName,
120             final String... childNodeTypes) throws RepositoryException {
121         if (!baseNode.hasNode(childNodeName)) {
122             return null;
123         }
124 
125         Node childNode;
126 
127         for (NodeIterator nodeIt = baseNode.getNodes(childNodeName); nodeIt.hasNext();) {
128             childNode = nodeIt.nextNode();
129 
130             if (StringUtils.equals(childNodeName, childNode.getName())) {
131                 if (childNodeTypes == null || childNodeTypes.length == 0) {
132                     return childNode;
133                 } else {
134                     for (String childNodeType : childNodeTypes) {
135                         if (childNode.isNodeType(childNodeType)) {
136                             return childNode;
137                         }
138                     }
139                 }
140             }
141         }
142 
143         return null;
144     }
145 
146     /**
147      * Returns {@link Workflow} instance by the {@code category} for the {@code node}.
148      * @param session JCR session
149      * @param category workflow category
150      * @param node folder or document node
151      * @return {@link Workflow} instance for the {@code node} and the {@code category}
152      * @throws RepositoryException if any repository/workflow exception occurs
153      */
154     public static Workflow getHippoWorkflow(final Session session, final String category, final Node node)
155             throws RepositoryException {
156         Workspace workspace = session.getWorkspace();
157 
158         ClassLoader workspaceClassloader = workspace.getClass().getClassLoader();
159         ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
160 
161         try {
162             if (workspaceClassloader != currentClassloader) {
163                 Thread.currentThread().setContextClassLoader(workspaceClassloader);
164             }
165 
166             WorkflowManager wfm = ((HippoWorkspace) workspace).getWorkflowManager();
167 
168             return wfm.getWorkflow(category, node);
169         } finally {
170             if (workspaceClassloader != currentClassloader) {
171                 Thread.currentThread().setContextClassLoader(currentClassloader);
172             }
173         }
174     }
175 
176     /**
177      * Find and return the first found variant node under the handle node.
178      * @param handle handle node
179      * @return the first found variant node under the handle node
180      * @throws RepositoryException if repository exception occurs
181      */
182     public static Node getFirstVariantNode(final Node handle) throws RepositoryException {
183         if (handle == null) {
184             return null;
185         }
186 
187         for (NodeIterator nodeIt = handle.getNodes(handle.getName()); nodeIt.hasNext();) {
188             Node variantNode = nodeIt.nextNode();
189 
190             if (variantNode != null) {
191                 return variantNode;
192             }
193         }
194 
195         return null;
196     }
197 
198     /**
199      * Returns a map of variant nodes, keyed by variant states such as {@link HippoStdNodeType#PUBLISHED} or {@link HippoStdNodeType#UNPUBLISHED}.
200      * @param handle document handle node
201      * @return a map of variant nodes, keyed by variant states such as {@link HippoStdNodeType#PUBLISHED} or {@link HippoStdNodeType#UNPUBLISHED}
202      * @throws RepositoryException if any repository/workflow exception occurs
203      */
204     public static Map<String, Node> getDocumentVariantsMap(final Node handle) throws RepositoryException {
205         Map<String, Node> variantsMap = new HashMap<>();
206         Node variantNode = null;
207         String hippoState;
208 
209         for (NodeIterator nodeIt = handle.getNodes(handle.getName()); nodeIt.hasNext();) {
210             variantNode = nodeIt.nextNode();
211 
212             if (variantNode.hasProperty(HippoStdNodeType.HIPPOSTD_STATE)) {
213                 hippoState = variantNode.getProperty(HippoStdNodeType.HIPPOSTD_STATE).getString();
214                 variantsMap.put(hippoState, variantNode);
215             }
216         }
217 
218         return variantsMap;
219     }
220 
221     /**
222      * Finds a variant node by the {@link HippoStdNodeType#HIPPOSTD_STATE} property value
223      * such as {@link HippoStdNodeType#PUBLISHED} or {@link HippoStdNodeType#UNPUBLISHED}.
224      * @param handle document handle node
225      * @param hippoStdState {@link HippoStdNodeType#HIPPOSTD_STATE} property value such as {@link HippoStdNodeType#PUBLISHED} or {@link HippoStdNodeType#UNPUBLISHED}
226      * @return a variant node by the {@link HippoStdNodeType#HIPPOSTD_STATE} property value
227      * @throws RepositoryException if any repository/workflow exception occurs
228      */
229     public static Node getDocumentVariantByHippoStdState(final Node handle, final String hippoStdState)
230             throws RepositoryException {
231         Node variantNode = null;
232         String state;
233 
234         for (NodeIterator nodeIt = handle.getNodes(handle.getName()); nodeIt.hasNext();) {
235             variantNode = nodeIt.nextNode();
236 
237             if (variantNode.hasProperty(HippoStdNodeType.HIPPOSTD_STATE)) {
238                 state = variantNode.getProperty(HippoStdNodeType.HIPPOSTD_STATE).getString();
239                 if (StringUtils.equals(hippoStdState, state)) {
240                     return variantNode;
241                 }
242             }
243         }
244 
245         return null;
246     }
247 
248     /**
249      * Detects if the document handle is representing a live document at the moment.
250      * @param handle document handle node
251      * @return true if the document handle is representing a live document at the moment
252      * @throws RepositoryException if any repository/workflow exception occurs
253      */
254     public static boolean isDocumentHandleLive(final Node handle) throws RepositoryException {
255         Node liveVariant = getDocumentVariantByHippoStdState(handle, HippoStdNodeType.PUBLISHED);
256 
257         if (liveVariant != null) {
258             String[] availabilities = JcrUtils.getMultipleStringProperty(liveVariant, HippoNodeType.HIPPO_AVAILABILITY,
259                     ArrayUtils.EMPTY_STRING_ARRAY);
260             for (String availability : availabilities) {
261                 if (StringUtils.equals("live", availability)) {
262                     return true;
263                 }
264             }
265         }
266 
267         return false;
268     }
269 
270     /**
271      * Checks if all the folders exist in the given {@code absPath} and creates folders if not existing.
272      * @param session JCR session
273      * @param absPath absolute folder node path
274      * @return the final folder node if successful
275      * @throws RepositoryException if any repository exception occurs
276      * @throws WorkflowException if any workflow exception occurs
277      */
278     public static Node createMissingHippoFolders(final Session session, String absPath)
279             throws RepositoryException, WorkflowException {
280 
281         String[] folderNames = StringUtils
282                 .split(ContentPathUtils.encodeNodePath(ContentPathUtils.removeIndexNotationInNodePath(absPath)), "/");
283 
284         Node rootNode = session.getRootNode();
285         Node curNode = rootNode;
286         String folderNodePath;
287 
288         for (String folderName : folderNames) {
289             if (curNode == rootNode) {
290                 folderNodePath = "/" + folderName;
291             } else {
292                 folderNodePath = curNode.getPath() + "/" + folderName;
293             }
294 
295             Node existingFolderNode = getExistingHippoFolderNode(session, folderNodePath);
296 
297             if (existingFolderNode == null) {
298                 curNode = session.getNode(
299                         createHippoFolderNodeByWorkflow(session, curNode, DEFAULT_HIPPO_FOLDER_NODE_TYPE, folderName));
300             } else {
301                 curNode = existingFolderNode;
302             }
303 
304             curNode = getHippoCanonicalNode(curNode);
305 
306             if (isHippoMirrorNode(curNode)) {
307                 curNode = getRereferencedNodeByHippoMirror(curNode);
308             }
309         }
310 
311         return curNode;
312     }
313 
314     /**
315      * Returns {@code node} if it is a document handle node or its parent if it is a document variant node.
316      * Otherwise returns null.
317      * @param node JCR node
318      * @return {@code node} if it is a document handle node or its parent if it is a document variant node. Otherwise returns null.
319      * @throws RepositoryException if repository exception occurs
320      */
321     public static Node getHippoDocumentHandle(Node node) throws RepositoryException {
322         if (node.isNodeType("hippo:handle")) {
323             return node;
324         } else if (node.isNodeType("hippo:document")) {
325             if (!node.getSession().getRootNode().isSame(node)) {
326                 Node parentNode = node.getParent();
327 
328                 if (parentNode.isNodeType("hippo:handle")) {
329                     return parentNode;
330                 }
331             }
332         }
333 
334         return null;
335     }
336 
337     /**
338      * Return true if the {@code path} reflects a document path in Hippo.
339      * @param path document path
340      * @return true if the {@code path} reflects a document path in Hippo
341      */
342     public static boolean isDocumentPath(final String path) {
343         return StringUtils.startsWith(path, DOCUMENT_PATH_PREFIX);
344     }
345 
346     /**
347      * Return true if the {@code path} reflects a gallery path in Hippo.
348      * @param path gallery path
349      * @return true if the {@code path} reflects a gallery path in Hippo
350      */
351     public static boolean isGalleryPath(final String path) {
352         return StringUtils.startsWith(path, GALLERY_PATH_PREFIX);
353     }
354 
355     /**
356      * Return true if the {@code path} reflects a asset path in Hippo.
357      * @param path asset path
358      * @return true if the {@code path} reflects a asset path in Hippo
359      */
360     public static boolean isAssetPath(final String path) {
361         return StringUtils.startsWith(path, ASSET_PATH_PREFIX);
362     }
363 
364     /**
365      * Return true if the {@code path} reflects a gallery or asset path in Hippo.
366      * @param path gallery or asset path
367      * @return true if the {@code path} reflects a gallery or asset path in Hippo
368      */
369     public static boolean isBinaryPath(final String path) {
370         return isGalleryPath(path) || isAssetPath(path);
371     }
372 
373     /**
374      * Finds and returns the canonical node from the {@code node}.
375      * @param node node
376      * @return the canonical node from the {@code node}
377      */
378     static Node getHippoCanonicalNode(Node node) {
379         if (node instanceof HippoNode) {
380             HippoNode hnode = (HippoNode) node;
381 
382             try {
383                 Node canonical = hnode.getCanonicalNode();
384 
385                 if (canonical == null) {
386                     log.debug("Cannot get canonical node for '{}'. This means there is no phyiscal equivalence of the "
387                             + "virtual node. Return null", node.getPath());
388                 }
389 
390                 return canonical;
391             } catch (RepositoryException e) {
392                 log.error("Repository exception while fetching canonical node. Return null", e);
393                 throw new RuntimeException(e);
394             }
395         }
396 
397         return node;
398     }
399 
400     /**
401      * Returns true if the {@code node} is a either <code>hippo:facetselect</code> or <code>hippo:mirror</code> node.
402      * @param node node
403      * @return true if the {@code node} is a either <code>hippo:facetselect</code> or <code>hippo:mirror</code> node
404      * @throws RepositoryException if unexpected repository exception occurs
405      */
406     static boolean isHippoMirrorNode(Node node) throws RepositoryException {
407         if (node.isNodeType(HippoNodeType.NT_FACETSELECT) || node.isNodeType(HippoNodeType.NT_MIRROR)) {
408             return true;
409         }
410 
411         return false;
412     }
413 
414     /**
415      * Returns the referenced node by the given mirror node ({@code mirrorNode}).
416      * @param mirrorNode mirror node
417      * @return the referenced node by the given mirror node ({@code mirrorNode})
418      */
419     static Node getRereferencedNodeByHippoMirror(Node mirrorNode) {
420         String docBaseUUID = null;
421 
422         try {
423             if (!isHippoMirrorNode(mirrorNode)) {
424                 log.info("Cannot deref a node that is not of (sub)type '{}' or '{}'. Return null",
425                         HippoNodeType.NT_FACETSELECT, HippoNodeType.NT_MIRROR);
426                 return null;
427             }
428 
429             // HippoNodeType.HIPPO_DOCBASE is a mandatory property so no need to test if exists
430             docBaseUUID = mirrorNode.getProperty(HippoNodeType.HIPPO_DOCBASE).getString();
431 
432             try {
433                 return mirrorNode.getSession().getNodeByIdentifier(docBaseUUID);
434             } catch (IllegalArgumentException e) {
435                 log.warn("Docbase cannot be parsed to a valid uuid. Return null");
436                 return null;
437             }
438         } catch (ItemNotFoundException e) {
439             String path = null;
440 
441             try {
442                 path = mirrorNode.getPath();
443             } catch (RepositoryException e1) {
444                 log.error("RepositoryException, cannot return deferenced node: {}", e1);
445             }
446 
447             log.info(
448                     "ItemNotFoundException, cannot return deferenced node because docbase uuid '{}' cannot be found. The docbase property is at '{}/hippo:docbase'. Return null",
449                     docBaseUUID, path);
450         } catch (RepositoryException e) {
451             log.error("RepositoryException, cannot return deferenced node: {}", e);
452         }
453 
454         return null;
455     }
456 
457     /**
458      * Finds and returns a hippo folder node located at {@code absPath} if found. If not found, returns null.
459      * @param session JCR session
460      * @param absPath absolute folder node path
461      * @return a hippo folder node located at {@code absPath} if found. If not found, returns null
462      * @throws RepositoryException if unexpected repository exception occurs
463      */
464     static Node getExistingHippoFolderNode(final Session session, final String absPath) throws RepositoryException {
465 
466         if (StringUtils.isEmpty(absPath)) {
467             return null;
468         }
469 
470         String[] pathSegments = StringUtils
471                 .split(ContentPathUtils.encodeNodePath(ContentPathUtils.removeIndexNotationInNodePath(absPath)), "/");
472 
473         Node curFolder = session.getRootNode();
474 
475         for (String pathSegment : pathSegments) {
476             if (!curFolder.hasNode(pathSegment)) {
477                 return null;
478             }
479 
480             boolean found = false;
481 
482             for (NodeIterator nodeIt = curFolder.getNodes(pathSegment); nodeIt.hasNext();) {
483                 Node childNode = nodeIt.nextNode();
484 
485                 if (childNode != null && !isHippoDocumentHandleOrVariant(childNode)) {
486                     found = true;
487                     curFolder = childNode;
488                     break;
489                 }
490             }
491 
492             if (!found) {
493                 return null;
494             }
495         }
496 
497         if (curFolder == null) {
498             return null;
499         }
500 
501         Node canonicalFolderNode = getHippoCanonicalNode(curFolder);
502 
503         if (isHippoMirrorNode(canonicalFolderNode)) {
504             canonicalFolderNode = getRereferencedNodeByHippoMirror(canonicalFolderNode);
505         }
506 
507         if (canonicalFolderNode == null) {
508             return null;
509         }
510 
511         if (isHippoDocumentHandleOrVariant(canonicalFolderNode)) {
512             return null;
513         }
514 
515         return canonicalFolderNode;
516     }
517 
518     /**
519      * Returns true if {@code node} is either document handle node or document variant node.
520      * @param node node
521      * @return true if {@code node} is either document handle node or document variant node
522      * @throws RepositoryException
523      */
524     static boolean isHippoDocumentHandleOrVariant(Node node) throws RepositoryException {
525         if (node.isNodeType("hippo:handle")) {
526             return true;
527         } else if (node.isNodeType("hippo:document")) {
528             if (!node.getSession().getRootNode().isSame(node)) {
529                 Node parentNode = node.getParent();
530 
531                 if (parentNode.isNodeType("hippo:handle")) {
532                     return true;
533                 }
534             }
535         }
536 
537         return false;
538     }
539 
540     /**
541      * Creates a hippo folder by using Hippo Repository Workflow.
542      * @param session JCR session
543      * @param folderNode base folder node
544      * @param nodeTypeName folder node type name
545      * @param name folder node name
546      * @return absolute path of the created folder
547      * @throws RepositoryException if unexpected repository exception occurs
548      * @throws WorkflowException if unexpected workflow exception occurs
549      */
550     static String createHippoFolderNodeByWorkflow(final Session session, Node folderNode, String nodeTypeName,
551             String name) throws RepositoryException, WorkflowException {
552         try {
553             folderNode = getHippoCanonicalNode(folderNode);
554             Workflow wf = getHippoWorkflow(session, DEFAULT_HIPPO_FOLDER_WORKFLOW_CATEGORY, folderNode);
555 
556             if (wf instanceof FolderWorkflow) {
557                 FolderWorkflow fwf = (FolderWorkflow) wf;
558 
559                 String category = DEFAULT_NEW_DOCUMENT_WORKFLOW_CATEGORY;
560 
561                 if (nodeTypeName.equals(DEFAULT_HIPPO_FOLDER_NODE_TYPE)) {
562                     category = DEFAULT_NEW_FOLDER_WORKFLOW_CATEGORY;
563 
564                     // now check if there is some more specific workflow for hippostd:folder
565                     if (fwf.hints() != null && fwf.hints().get("prototypes") != null) {
566                         Object protypesMap = fwf.hints().get("prototypes");
567                         if (protypesMap instanceof Map) {
568                             for (Object o : ((Map) protypesMap).entrySet()) {
569                                 Entry entry = (Entry) o;
570                                 if (entry.getKey() instanceof String && entry.getValue() instanceof Set) {
571                                     if (((Set) entry.getValue()).contains(DEFAULT_HIPPO_FOLDER_NODE_TYPE)) {
572                                         // we found possibly a more specific workflow for folderNodeTypeName. Use the key as category
573                                         category = (String) entry.getKey();
574                                         break;
575                                     }
576                                 }
577                             }
578                         }
579                     }
580                 }
581 
582                 String nodeName = DEFAULT_URI_ENCODING.encode(name);
583                 String added = fwf.add(category, nodeTypeName, nodeName);
584                 if (added == null) {
585                     throw new WorkflowException("Failed to add document/folder for type '" + nodeTypeName
586                             + "'. Make sure there is a prototype.");
587                 }
588                 Node addedNode = folderNode.getSession().getNode(added);
589                 if (!nodeName.equals(name)) {
590                     DefaultWorkflow defaultWorkflow = (DefaultWorkflow) getHippoWorkflow(session,
591                             DEFAULT_WORKFLOW_CATEGORY, addedNode);
592                     defaultWorkflow.setDisplayName(name);
593                 }
594 
595                 if (DEFAULT_NEW_DOCUMENT_WORKFLOW_CATEGORY.equals(category)) {
596 
597                     // added new document : because the document must be in 'preview' availability, we now set this explicitly
598                     if (addedNode.isNodeType("hippostd:publishable")) {
599                         log.info("Added document '{}' is pusblishable so set status to preview.", addedNode.getPath());
600                         addedNode.setProperty("hippostd:state", "unpublished");
601                         addedNode.setProperty(HippoNodeType.HIPPO_AVAILABILITY, new String[] { "preview" });
602                     } else {
603                         log.info("Added document '{}' is not publishable so set status to live & preview directly.",
604                                 addedNode.getPath());
605                         addedNode.setProperty(HippoNodeType.HIPPO_AVAILABILITY, new String[] { "live", "preview" });
606                     }
607 
608                     if (addedNode.isNodeType("hippostd:publishableSummary")) {
609                         addedNode.setProperty("hippostd:stateSummary", "new");
610                     }
611                     addedNode.getSession().save();
612                 }
613                 return added;
614             } else {
615                 throw new WorkflowException(
616                         "Can't create folder " + name + " [" + nodeTypeName + "] in the folder " + folderNode.getPath()
617                                 + ", because there is no FolderWorkflow possible on the folder node: " + wf);
618             }
619         } catch (RemoteException e) {
620             throw new WorkflowException(e.toString(), e);
621         }
622     }
623 }