View Javadoc
1   /*
2    * Copyright 2024 Bloomreach (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.channelmanager.pagesupport.document.management.impl;
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.StringUtils;
32  import org.hippoecm.repository.HippoStdNodeType;
33  import org.hippoecm.repository.api.HippoNode;
34  import org.hippoecm.repository.api.HippoNodeType;
35  import org.hippoecm.repository.api.HippoWorkspace;
36  import org.hippoecm.repository.api.StringCodec;
37  import org.hippoecm.repository.api.StringCodecFactory;
38  import org.hippoecm.repository.api.Workflow;
39  import org.hippoecm.repository.api.WorkflowException;
40  import org.hippoecm.repository.api.WorkflowManager;
41  import org.hippoecm.repository.standardworkflow.DefaultWorkflow;
42  import org.hippoecm.repository.standardworkflow.FolderWorkflow;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  /**
47   * Internal utility to invoke Hippo Workflow APIs.
48   */
49  class HippoWorkflowUtils {
50  
51      private static final Logger log = LoggerFactory.getLogger(HippoWorkflowUtils.class);
52  
53      /**
54       * Hippo Repository specific predefined folder node type name
55       */
56      private static final String DEFAULT_HIPPO_FOLDER_NODE_TYPE = "hippostd:folder";
57  
58      /**
59       * The workflow category name to get a folder workflow. We use threepane as this is the same as the CMS uses
60       */
61      private static final String DEFAULT_HIPPO_FOLDER_WORKFLOW_CATEGORY = "threepane";
62  
63      /**
64       * The workflow category name to add a new document.
65       */
66      private static final String DEFAULT_NEW_DOCUMENT_WORKFLOW_CATEGORY = "new-document";
67  
68      /**
69       * The workflow category name to add a new folder.
70       */
71      private static final String DEFAULT_NEW_FOLDER_WORKFLOW_CATEGORY = "new-folder";
72  
73      /**
74       * The workflow category name to localize the new document
75       */
76      private static final String DEFAULT_WORKFLOW_CATEGORY = "core";
77  
78      /**
79       * The codec which is used for the node names
80       */
81      private static final StringCodec DEFAULT_URI_ENCODING = new StringCodecFactory.UriEncoding();
82  
83      private HippoWorkflowUtils() {
84      }
85  
86      /**
87       * Returns {@link Workflow} instance by the {@code category} for the {@code node}.
88       * @param session JCR session
89       * @param category workflow category
90       * @param node folder or document node
91       * @return {@link Workflow} instance for the {@code node} and the {@code category}
92       * @throws RepositoryException if any repository/workflow exception occurs
93       */
94      public static Workflow getHippoWorkflow(final Session session, final String category, final Node node)
95              throws RepositoryException {
96          Workspace workspace = session.getWorkspace();
97  
98          ClassLoader workspaceClassloader = workspace.getClass().getClassLoader();
99          ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
100 
101         try {
102             if (workspaceClassloader != currentClassloader) {
103                 Thread.currentThread().setContextClassLoader(workspaceClassloader);
104             }
105 
106             WorkflowManager wfm = ((HippoWorkspace) workspace).getWorkflowManager();
107 
108             return wfm.getWorkflow(category, node);
109         } finally {
110             if (workspaceClassloader != currentClassloader) {
111                 Thread.currentThread().setContextClassLoader(currentClassloader);
112             }
113         }
114     }
115 
116     /**
117      * Returns a map of variant nodes, keyed by variant states such as {@link HippoStdNodeType.PUBLISHED} or {@link HippoStdNodeType.UNPUBLISHED}.
118      * @param handle document handle node
119      * @return a map of variant nodes, keyed by variant states such as {@link HippoStdNodeType.PUBLISHED} or {@link HippoStdNodeType.UNPUBLISHED}
120      * @throws RepositoryException if any repository/workflow exception occurs
121      */
122     public static Map<String, Node> getDocumentVariantsMap(final Node handle) throws RepositoryException {
123         Map<String, Node> variantsMap = new HashMap<>();
124         Node variantNode = null;
125         String hippoState;
126 
127         for (NodeIterator nodeIt = handle.getNodes(handle.getName()); nodeIt.hasNext(); ) {
128             variantNode = nodeIt.nextNode();
129 
130             if (variantNode.hasProperty(HippoStdNodeType.HIPPOSTD_STATE)) {
131                 hippoState = variantNode.getProperty(HippoStdNodeType.HIPPOSTD_STATE).getString();
132                 variantsMap.put(hippoState, variantNode);
133             }
134         }
135 
136         return variantsMap;
137     }
138 
139     /**
140      * Checks if all the folders exist in the given {@code absPath} and creates folders if not existing.
141      * @param session JCR session
142      * @param absPath absolute folder node path
143      * @return the final folder node if successful
144      * @throws RepositoryException if any repository exception occurs
145      * @throws WorkflowException if any workflow exception occurs
146      */
147     public static Node createMissingHippoFolders(final Session session, String absPath)
148             throws RepositoryException, WorkflowException {
149         String[] folderNames = StringUtils.split(absPath, "/");
150 
151         Node rootNode = session.getRootNode();
152         Node curNode = rootNode;
153         String folderNodePath;
154 
155         for (String folderName : folderNames) {
156             String folderNodeName = DEFAULT_URI_ENCODING.encode(folderName);
157 
158             if (curNode == rootNode) {
159                 folderNodePath = "/" + folderNodeName;
160             } else {
161                 folderNodePath = curNode.getPath() + "/" + folderNodeName;
162             }
163 
164             Node existingFolderNode = getExistingHippoFolderNode(session, folderNodePath);
165 
166             if (existingFolderNode == null) {
167                 curNode = session
168                         .getNode(createHippoFolderNodeByWorkflow(session, curNode, DEFAULT_HIPPO_FOLDER_NODE_TYPE, folderName));
169             } else {
170                 curNode = existingFolderNode;
171             }
172 
173             curNode = getHippoCanonicalNode(curNode);
174 
175             if (isHippoMirrorNode(curNode)) {
176                 curNode = getRereferencedNodeByHippoMirror(curNode);
177             }
178         }
179 
180         return curNode;
181     }
182 
183     /**
184      * Returns {@code node} if it is a document handle node or its parent if it is a document variant node.
185      * Otherwise returns null.
186      * @param node JCR node
187      * @return {@code node} if it is a document handle node or its parent if it is a document variant node. Otherwise returns null.
188      * @throws RepositoryException if repository exception occurs
189      */
190     public static Node getHippoDocumentHandle(Node node) throws RepositoryException {
191         if (node.isNodeType("hippo:handle")) {
192             return node;
193         } else if (node.isNodeType("hippo:document")) {
194             if (!node.getSession().getRootNode().isSame(node)) {
195                 Node parentNode = node.getParent();
196 
197                 if (parentNode.isNodeType("hippo:handle")) {
198                     return parentNode;
199                 }
200             }
201         }
202 
203         return null;
204     }
205 
206     private static Node getHippoCanonicalNode(Node node) {
207         if (node instanceof HippoNode) {
208             HippoNode hnode = (HippoNode) node;
209 
210             try {
211                 Node canonical = hnode.getCanonicalNode();
212 
213                 if (canonical == null) {
214                     log.debug("Cannot get canonical node for '{}'. This means there is no phyiscal equivalence of the "
215                             + "virtual node. Return null", node.getPath());
216                 }
217 
218                 return canonical;
219             } catch (RepositoryException e) {
220                 log.error("Repository exception while fetching canonical node. Return null", e);
221                 throw new RuntimeException(e);
222             }
223         }
224 
225         return node;
226     }
227 
228     private static boolean isHippoMirrorNode(Node node) throws RepositoryException {
229         if (node.isNodeType(HippoNodeType.NT_FACETSELECT) || node.isNodeType(HippoNodeType.NT_MIRROR)) {
230             return true;
231         }
232 
233         return false;
234     }
235 
236     private static Node getRereferencedNodeByHippoMirror(Node mirrorNode) {
237         String docBaseUUID = null;
238 
239         try {
240             if (!isHippoMirrorNode(mirrorNode)) {
241                 log.info("Cannot deref a node that is not of (sub)type '{}' or '{}'. Return null",
242                         HippoNodeType.NT_FACETSELECT, HippoNodeType.NT_MIRROR);
243                 return null;
244             }
245 
246             // HippoNodeType.HIPPO_DOCBASE is a mandatory property so no need to test if exists
247             docBaseUUID = mirrorNode.getProperty(HippoNodeType.HIPPO_DOCBASE).getString();
248 
249             try {
250                 return mirrorNode.getSession().getNodeByIdentifier(docBaseUUID);
251             } catch (IllegalArgumentException e) {
252                 log.warn("Docbase cannot be parsed to a valid uuid. Return null");
253                 return null;
254             }
255         } catch (ItemNotFoundException e) {
256             String path = null;
257 
258             try {
259                 path = mirrorNode.getPath();
260             } catch (RepositoryException e1) {
261                 log.error("RepositoryException, cannot return deferenced node: {}", e1);
262             }
263 
264             log.info(
265                     "ItemNotFoundException, cannot return deferenced node because docbase uuid '{}' cannot be found. The docbase property is at '{}/hippo:docbase'. Return null",
266                     docBaseUUID, path);
267         } catch (RepositoryException e) {
268             log.error("RepositoryException, cannot return deferenced node: {}", e);
269         }
270 
271         return null;
272     }
273 
274     private static Node getExistingHippoFolderNode(final Session session, final String absPath)
275             throws RepositoryException {
276         if (!session.nodeExists(absPath)) {
277             return null;
278         }
279 
280         Node node = session.getNode(absPath);
281         Node candidateNode = null;
282 
283         if (session.getRootNode().isSame(node)) {
284             return session.getRootNode();
285         } else {
286             Node parentNode = node.getParent();
287             for (NodeIterator nodeIt = parentNode.getNodes(node.getName()); nodeIt.hasNext();) {
288                 Node siblingNode = nodeIt.nextNode();
289                 if (!isHippoDocumentHandleOrVariant(siblingNode)) {
290                     candidateNode = siblingNode;
291                     break;
292                 }
293             }
294         }
295 
296         if (candidateNode == null) {
297             return null;
298         }
299 
300         Node canonicalFolderNode = getHippoCanonicalNode(candidateNode);
301 
302         if (isHippoMirrorNode(canonicalFolderNode)) {
303             canonicalFolderNode = getRereferencedNodeByHippoMirror(canonicalFolderNode);
304         }
305 
306         if (canonicalFolderNode == null) {
307             return null;
308         }
309 
310         if (isHippoDocumentHandleOrVariant(canonicalFolderNode)) {
311             return null;
312         }
313 
314         return canonicalFolderNode;
315     }
316 
317     private static boolean isHippoDocumentHandleOrVariant(Node node) throws RepositoryException {
318         if (node.isNodeType("hippo:handle")) {
319             return true;
320         } else if (node.isNodeType("hippo:document")) {
321             if (!node.getSession().getRootNode().isSame(node)) {
322                 Node parentNode = node.getParent();
323 
324                 if (parentNode.isNodeType("hippo:handle")) {
325                     return true;
326                 }
327             }
328         }
329 
330         return false;
331     }
332 
333     private static String createHippoFolderNodeByWorkflow(final Session session, Node folderNode, String nodeTypeName,
334             String name) throws RepositoryException, WorkflowException {
335         try {
336             folderNode = getHippoCanonicalNode(folderNode);
337             Workflow wf = getHippoWorkflow(session, DEFAULT_HIPPO_FOLDER_WORKFLOW_CATEGORY, folderNode);
338 
339             if (wf instanceof FolderWorkflow) {
340                 FolderWorkflow fwf = (FolderWorkflow) wf;
341 
342                 String category = DEFAULT_NEW_DOCUMENT_WORKFLOW_CATEGORY;
343 
344                 if (nodeTypeName.equals(DEFAULT_HIPPO_FOLDER_NODE_TYPE)) {
345                     category = DEFAULT_NEW_FOLDER_WORKFLOW_CATEGORY;
346 
347                     // now check if there is some more specific workflow for hippostd:folder
348                     if (fwf.hints() != null && fwf.hints().get("prototypes") != null) {
349                         Object protypesMap = fwf.hints().get("prototypes");
350                         if (protypesMap instanceof Map) {
351                             for (Object o : ((Map) protypesMap).entrySet()) {
352                                 Entry entry = (Entry) o;
353                                 if (entry.getKey() instanceof String && entry.getValue() instanceof Set) {
354                                     if (((Set) entry.getValue()).contains(DEFAULT_HIPPO_FOLDER_NODE_TYPE)) {
355                                         // we found possibly a more specific workflow for folderNodeTypeName. Use the key as category
356                                         category = (String) entry.getKey();
357                                         break;
358                                     }
359                                 }
360                             }
361                         }
362                     }
363                 }
364 
365                 String nodeName = DEFAULT_URI_ENCODING.encode(name);
366                 String added = fwf.add(category, nodeTypeName, nodeName);
367                 if (added == null) {
368                     throw new WorkflowException("Failed to add document/folder for type '" + nodeTypeName
369                             + "'. Make sure there is a prototype.");
370                 }
371                 Node addedNode = folderNode.getSession().getNode(added);
372                 if (!nodeName.equals(name)) {
373                     DefaultWorkflow defaultWorkflow = (DefaultWorkflow) getHippoWorkflow(session, DEFAULT_WORKFLOW_CATEGORY,
374                             addedNode);
375                     defaultWorkflow.setDisplayName(name);
376                 }
377 
378                 if (DEFAULT_NEW_DOCUMENT_WORKFLOW_CATEGORY.equals(category)) {
379 
380                     // added new document : because the document must be in 'preview' availability, we now set this explicitly
381                     if (addedNode.isNodeType("hippostd:publishable")) {
382                         log.info("Added document '{}' is pusblishable so set status to preview.", addedNode.getPath());
383                         addedNode.setProperty("hippostd:state", "unpublished");
384                         addedNode.setProperty(HippoNodeType.HIPPO_AVAILABILITY, new String[] { "preview" });
385                     } else {
386                         log.info("Added document '{}' is not publishable so set status to live & preview directly.",
387                                 addedNode.getPath());
388                         addedNode.setProperty(HippoNodeType.HIPPO_AVAILABILITY, new String[] { "live", "preview" });
389                     }
390 
391                     if (addedNode.isNodeType("hippostd:publishableSummary")) {
392                         addedNode.setProperty("hippostd:stateSummary", "new");
393                     }
394                     addedNode.getSession().save();
395                 }
396                 return added;
397             } else {
398                 throw new WorkflowException(
399                         "Can't create folder " + name + " [" + nodeTypeName + "] in the folder " + folderNode.getPath()
400                                 + ", because there is no FolderWorkflow possible on the folder node: " + wf);
401             }
402         } catch (RemoteException e) {
403             throw new WorkflowException(e.toString(), e);
404         }
405     }
406 }