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.channel.event;
17  
18  import java.text.MessageFormat;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.HashSet;
22  import java.util.LinkedHashSet;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Set;
26  import java.util.Stack;
27  import java.util.function.Predicate;
28  
29  import javax.jcr.Node;
30  import javax.jcr.NodeIterator;
31  import javax.jcr.RepositoryException;
32  import javax.jcr.Session;
33  import javax.jcr.query.Query;
34  import javax.jcr.query.QueryResult;
35  
36  import org.apache.commons.lang3.StringUtils;
37  import org.hippoecm.hst.configuration.components.HstComponentConfiguration;
38  import org.hippoecm.hst.configuration.hosting.Mount;
39  import org.hippoecm.hst.configuration.site.HstSite;
40  import org.hippoecm.hst.core.jcr.RuntimeRepositoryException;
41  import org.hippoecm.hst.core.linking.DocumentParamsScanner;
42  import org.hippoecm.hst.core.request.HstRequestContext;
43  import org.hippoecm.hst.pagecomposer.jaxrs.api.ChannelEventListenerRegistry;
44  import org.hippoecm.hst.pagecomposer.jaxrs.api.PageCopyContext;
45  import org.hippoecm.hst.pagecomposer.jaxrs.api.PageCopyEvent;
46  import org.hippoecm.hst.pagecomposer.jaxrs.services.exceptions.ClientError;
47  import org.hippoecm.hst.pagecomposer.jaxrs.services.exceptions.ClientException;
48  import org.hippoecm.repository.HippoStdNodeType;
49  import org.hippoecm.repository.api.HippoNodeType;
50  import org.hippoecm.repository.translation.HippoTranslationNodeType;
51  import org.hippoecm.repository.util.JcrUtils;
52  import org.onehippo.cms7.services.eventbus.Subscribe;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import static org.hippoecm.hst.configuration.HstNodeTypes.COMPONENT_PROPERTY_REFERECENCECOMPONENT;
57  
58  /**
59   * <code>org.hippoecm.hst.pagecomposer.jaxrs.api.PageCopyEvent</code> event handler which is to be registered through
60   * {@link ChannelEventListenerRegistry#register(Object)} (Object)} and unregistered through
61   * {@link ChannelEventListenerRegistry#unregister(Object)} (Object)} during the HST-2 based web application lifecycle.
62   * <P>
63   * Basically this event handler scans all the linked documents in a page and its components
64   * and copy each document from the source channel to the target channel if not existing in the target channel.
65   * </P>
66   */
67  public class DocumentCopyingPageCopyEventListener {
68  
69      private static final Logger log = LoggerFactory.getLogger(DocumentCopyingPageCopyEventListener.class);
70  
71      private static final String TRANSLATED_FOLDER_QUERY = "/jcr:root{0}//element(*,hippostd:folder)[@hippotranslation:id=''{1}'']";
72  
73      private static final String TRANSLATED_DOCUMENT_HANDLE_QUERY = "/jcr:root{0}//element(*,hippostdpubwf:document)[@hippotranslation:id=''{1}'']/..";
74  
75      private DocumentManagementServiceClient documentManagementServiceClient;
76  
77      private boolean copyDocumentsLinkedBySourcePage;
78  
79      public void init() {
80          ChannelEventListenerRegistry.get().register(this);
81      }
82  
83      public void destroy() {
84          ChannelEventListenerRegistry.get().unregister(this);
85      }
86  
87      public DocumentManagementServiceClient getDocumentManagementServiceClient() {
88          if (documentManagementServiceClient == null) {
89              documentManagementServiceClient = new DocumentManagementServiceClient();
90          }
91  
92          return documentManagementServiceClient;
93      }
94  
95      public void setDocumentManagementServiceClient(DocumentManagementServiceClient documentManagementServiceClient) {
96          this.documentManagementServiceClient = documentManagementServiceClient;
97      }
98  
99      public boolean isCopyDocumentsLinkedBySourcePage() {
100         return copyDocumentsLinkedBySourcePage;
101     }
102 
103     public void setCopyDocumentsLinkedBySourcePage(boolean copyDocumentsLinkedBySourcePage) {
104         this.copyDocumentsLinkedBySourcePage = copyDocumentsLinkedBySourcePage;
105     }
106 
107     /**
108      * Custom event handler before {@link #onPageCopyEvent(PageCopyEvent)} is invoked.
109      * An extended class from this can implement this method if it needs to process some custom tasks before the
110      * normal page copy event handling.
111      * @param pageCopyEvent page copy event
112      */
113     protected void onBeforePageCopyEvent(PageCopyEvent pageCopyEvent) {
114     }
115 
116     /**
117      * Custom event handler after {@link #onPageCopyEvent(PageCopyEvent)} is invoked.
118      * An extended class from this can implement this method if it needs to process some custom tasks after the
119      * normal page copy event handling.
120      * @param pageCopyEvent page copy event
121      */
122     protected void onAfterPageCopyEvent(PageCopyEvent pageCopyEvent) {
123     }
124 
125     @Subscribe
126     public void onPageCopyEvent(PageCopyEvent pageCopyEvent) {
127         if (pageCopyEvent.getException() != null) {
128             return;
129         }
130 
131         final PageCopyContext pageCopyContext = pageCopyEvent.getPageActionContext();
132         final HstRequestContext requestContext = pageCopyContext.getRequestContext();
133         final Mount sourceMount = pageCopyContext.getEditingMount();
134         final Mount targetMount = pageCopyContext.getTargetMount();
135 
136         final String sourceContentBasePath = sourceMount.getContentPath().intern();
137         final String targetContentBasePath = targetMount.getContentPath().intern();
138 
139         // synchronize interned targetContentBasePath to disallow concurrent document copying on the same target channel
140         synchronized (targetContentBasePath) {
141             try {
142                 onBeforePageCopyEvent(pageCopyEvent);
143 
144                 final Node sourceContentBaseNode = requestContext.getSession().getNode(sourceContentBasePath);
145                 final Node targetContentBaseNode = requestContext.getSession().getNode(targetContentBasePath);
146                 String sourceTranslationLanguage = HippoFolderDocumentUtils
147                         .getHippoTranslationLanguage(sourceContentBaseNode);
148                 String targetTranslationLanguage = HippoFolderDocumentUtils
149                         .getHippoTranslationLanguage(targetContentBaseNode);
150 
151                 if (StringUtils.isBlank(sourceTranslationLanguage)) {
152                     throw new IllegalStateException("Blank translation language in the source base content at '"
153                             + sourceContentBasePath + "'.");
154                 }
155 
156                 if (StringUtils.isBlank(targetTranslationLanguage)) {
157                     throw new IllegalStateException("Blank translation language in the target base content at '"
158                             + targetContentBasePath + "'.");
159                 }
160 
161                 if (isCopyDocumentsLinkedBySourcePage()) {
162                     if (StringUtils.equals(sourceContentBasePath, targetContentBasePath)) {
163                         log.info("No need to copy documents because the source and target channel have the same content base path: {}'",
164                                 sourceContentBasePath);
165                     } else {
166                         if (StringUtils.equals(sourceTranslationLanguage, targetTranslationLanguage)) {
167                             throw new IllegalStateException(
168                                     "The same translation language of the source and the target base content. Source='"
169                                             + sourceContentBasePath + "'. Target='" + targetContentBasePath + "'.");
170                         }
171 
172                         final Set<String> documentPathSet = getDocumentPathSetInPage(pageCopyContext);
173 
174                         if (!documentPathSet.isEmpty()) {
175                             if (!StringUtils.equals(sourceMount.getContentPath(), targetMount.getContentPath())) {
176                                 copyDocuments(pageCopyContext.getRequestContext().getSession(), documentPathSet,
177                                         sourceContentBaseNode, targetContentBaseNode);
178                             } else {
179                                 log.info("Linked document copying step skipped because the content path of the target " +
180                                         "mount is the same as that of the source mount: {}.", sourceMount.getContentPath());
181                             }
182                         } else {
183                             log.info("No linked document founds in the source page.");
184                         }
185                     }
186                 } else {
187                     log.info("Linked document copying step skipped because 'copyDocumentsLinkedBySourcePage' is turned off.");
188                 }
189 
190                 updateTargetHstConfiguration(pageCopyContext);
191 
192                 onAfterPageCopyEvent(pageCopyEvent);
193             } catch (ClientException e) {
194                 log.error("Failed to handle page copy event properly.", e);
195                 pageCopyEvent.setException(e);
196             } catch (Exception e) {
197                 log.error("Failed to handle page copy event properly.", e);
198                 final String clientMessage = "Failed to handle page copy event properly. " + e.toString();
199                 pageCopyEvent.setException(new ClientException(clientMessage, ClientError.ITEM_CANNOT_BE_CLONED,
200                         Collections.singletonMap("errorReason", clientMessage)));
201             }
202         }
203     }
204 
205     protected void updateTargetHstConfiguration(final PageCopyContext pageCopyContext) {
206 
207         if (pageCopyContext.getEditingMount().getIdentifier().equals(pageCopyContext.getTargetMount().getIdentifier())) {
208             log.debug("No need to update HST parameters when copying to the same mount {} with id {}.",
209                     pageCopyContext.getEditingMount().getMountPath(), pageCopyContext.getEditingMount().getIdentifier());
210             return;
211         }
212 
213         // delegate to static utility
214         HstDocumentParamsUpdater.updateTargetDocumentPaths(pageCopyContext.getEditingMount(),
215                                                             pageCopyContext.getSourcePage(),
216                                                             pageCopyContext.getTargetMount(),
217                                                             pageCopyContext.getNewPageNode(),
218                                                             pageCopyContext.getRequestContext());
219     }
220 
221     private void copyDocuments(final Session session, final Set<String> sourceDocumentPathSet,
222             final Node sourceContentBaseNode, final Node targetContentBaseNode) {
223         try {
224             final String sourceContentBasePath = sourceContentBaseNode.getPath();
225             final String targetContentBasePath = targetContentBaseNode.getPath();
226             final String targetTranslationLanguage = HippoFolderDocumentUtils
227                     .getHippoTranslationLanguage(targetContentBaseNode);
228 
229             Node sourceDocumentHandleNode;
230             Node targetDocumentHandleNode;
231             String targetDocumentAbsPath;
232             String targetFolderAbsPath;
233             String targetFolderRelPath;
234 
235             for (String sourceDocumentPath : sourceDocumentPathSet) {
236                 if (StringUtils.startsWith(sourceDocumentPath, "/")) {
237                     log.info("Skipping '{}' because it's an absolute jcr path, not relative to source mount content base",
238                             sourceDocumentPath);
239                     continue;
240                 }
241 
242                 if (!sourceContentBaseNode.hasNode(sourceDocumentPath)) {
243                     log.info("Skipping '{}' because it doesn't exist under '{}'.", sourceDocumentPath, sourceContentBasePath);
244                     continue;
245                 }
246 
247                 sourceDocumentHandleNode = HippoFolderDocumentUtils
248                         .getHippoDocumentHandle(sourceContentBaseNode.getNode(sourceDocumentPath));
249 
250                 if (sourceDocumentHandleNode == null) {
251                     log.info("Skipping '{}' because there's no document at the location under '{}'.", sourceDocumentPath,
252                             sourceContentBasePath);
253                     continue;
254                 }
255 
256                 targetDocumentHandleNode = findTargetTranslatedDocumentHandleNode(targetContentBaseNode, sourceDocumentHandleNode);
257 
258                 if (targetDocumentHandleNode != null) {
259                     log.info("Skipping '{}' because there exists a translated document at '{}'.", sourceDocumentPath,
260                             targetDocumentHandleNode.getPath());
261                     continue;
262                 }
263 
264                 targetDocumentAbsPath = resolveTargetDocumentAbsPath(sourceContentBaseNode, targetContentBaseNode, sourceDocumentPath);
265 
266                 if (HippoFolderDocumentUtils.documentExists(session, targetDocumentAbsPath)) {
267                     log.info("Skipping '{}' because it already exists under '{}'.", sourceDocumentPath, targetContentBasePath);
268                     continue;
269                 }
270 
271                 targetFolderAbsPath = StringUtils.substringBeforeLast(targetDocumentAbsPath, "/");
272                 targetFolderRelPath = StringUtils.substringAfter(targetFolderAbsPath, targetContentBasePath + "/");
273 
274                 if (HippoFolderDocumentUtils.folderExists(session, targetFolderAbsPath)) {
275                     Node targetFolderNode = session.getNode(targetFolderAbsPath);
276 
277                     if (!targetFolderNode.isNodeType(HippoTranslationNodeType.NT_TRANSLATED)) {
278                         final String clientMessage = "Cannot copy documents because the target folder at '"
279                                 + targetFolderAbsPath + "' is not type of " + HippoTranslationNodeType.NT_TRANSLATED
280                                 + ".";
281                         throw new ClientException(clientMessage, ClientError.INVALID_NODE_TYPE,
282                                 Collections.singletonMap("errorReason", clientMessage));
283                     } else {
284                         Node sourceFolderNode = sourceDocumentHandleNode.getParent();
285                         String sourceFolderTranslationId = JcrUtils.getStringProperty(sourceFolderNode,
286                                 HippoTranslationNodeType.ID, null);
287                         String targetFolderTranslationId = JcrUtils.getStringProperty(targetFolderNode,
288                                 HippoTranslationNodeType.ID, null);
289                         if (!StringUtils.equals(sourceFolderTranslationId, targetFolderTranslationId)) {
290                             final String clientMessage = "Cannot copy documents because the translation ID of target folder at '"
291                                     + targetFolderAbsPath + "' doesn't match with that of source folder at '"
292                                     + sourceFolderNode.getPath() + "'. '" + targetFolderTranslationId
293                                     + "' (target) vs. '" + sourceFolderTranslationId + "' (source).";
294                             throw new ClientException(clientMessage, ClientError.ITEM_CANNOT_BE_CLONED,
295                                     Collections.singletonMap("errorReason", clientMessage));
296                         }
297                     }
298                 } else {
299                     String sourceFolderRelPath = sourceDocumentHandleNode.getParent().getPath()
300                             .substring(sourceContentBasePath.length() + 1);
301 
302                     translateFolders(session, sourceContentBaseNode, sourceFolderRelPath, targetContentBaseNode,
303                             targetFolderRelPath, targetTranslationLanguage);
304                 }
305 
306                 final String translateDocumentPath = getDocumentManagementServiceClient().translateDocument(sourceDocumentHandleNode.getPath(),
307                         targetTranslationLanguage, sourceDocumentHandleNode.getName());
308                 getDocumentManagementServiceClient().commitEditableDocument(translateDocumentPath);
309             }
310         } catch (ClientException e) {
311             throw e;
312         } catch (Exception e) {
313             final String clientMessage = "Failed to copy all the linked documents. " + e.toString();
314             throw new ClientException(clientMessage, ClientError.ITEM_CANNOT_BE_CLONED,
315                     Collections.singletonMap("errorReason", clientMessage));
316         }
317     }
318 
319     /**
320      * Resolves target document absolute path under {@code targetContentBaseNode},
321      * corresponding to the {@code sourceDocumentPath} under {@code sourceContentBaseNode}.
322      * @param sourceContentBaseNode source content base folder node
323      * @param targetContentBaseNode target content base folder node
324      * @param sourceDocumentPath source document relative path
325      * @return corresponding target document absolute path
326      * @throws RepositoryException if any repository exception occurs
327      */
328     private String resolveTargetDocumentAbsPath(final Node sourceContentBaseNode, final Node targetContentBaseNode,
329             final String sourceDocumentPath) throws RepositoryException {
330         Node sourceDocumentHandleNode = sourceContentBaseNode.getNode(sourceDocumentPath);
331         Node sourceFolderNode = sourceDocumentHandleNode.getParent();
332         Node targetFolderNode = findTargetTranslatedFolderNode(targetContentBaseNode, sourceFolderNode);
333 
334         if (targetFolderNode != null) {
335             return targetFolderNode.getPath() + "/" + sourceDocumentHandleNode.getName();
336         }
337 
338         Stack<String> targetFolderNameStack = new Stack<>();
339         targetFolderNameStack.push(sourceFolderNode.getName());
340 
341         sourceFolderNode = sourceFolderNode.getParent();
342 
343         while (!sourceFolderNode.isSame(sourceContentBaseNode)) {
344             targetFolderNode = findTargetTranslatedFolderNode(targetContentBaseNode, sourceFolderNode);
345 
346             if (targetFolderNode != null) {
347                 String folderPath = StringUtils.removeStart(targetFolderNode.getPath(),
348                         targetContentBaseNode.getPath() + "/");
349                 targetFolderNameStack.push(folderPath);
350                 break;
351             } else {
352                 targetFolderNameStack.push(sourceFolderNode.getName());
353             }
354 
355             sourceFolderNode = sourceFolderNode.getParent();
356         }
357 
358         return targetContentBaseNode.getPath() + "/" + StringUtils.join(popAllToList(targetFolderNameStack), "/") + "/"
359                 + sourceDocumentHandleNode.getName();
360     }
361 
362     /**
363      * Find translated folder node under {@code targetContentBaseNode} for the {@code sourceFolderNode}.
364      * @param targetContentBaseNode target content base folder node
365      * @param sourceFolderNode source folder node
366      * @return translated folder node under {@code targetContentBaseNode} for the {@code sourceFolderNode}
367      * @throws RepositoryException if repository exception occurs
368      */
369     @SuppressWarnings("deprecation")
370     private Node findTargetTranslatedFolderNode(final Node targetContentBaseNode, final Node sourceFolderNode)
371             throws RepositoryException {
372         if (!sourceFolderNode.isNodeType(HippoStdNodeType.NT_FOLDER)
373                 || !sourceFolderNode.isNodeType(HippoTranslationNodeType.NT_TRANSLATED)) {
374             return null;
375         }
376 
377         Node translatedFolderNode = null;
378 
379         final String translationId = JcrUtils.getStringProperty(sourceFolderNode, HippoTranslationNodeType.ID, null);
380         final String statement = MessageFormat.format(TRANSLATED_FOLDER_QUERY, targetContentBaseNode.getPath(),
381                 translationId);
382         final Query query = targetContentBaseNode.getSession().getWorkspace().getQueryManager().createQuery(statement,
383                 Query.XPATH);
384 
385         final List<Node> translatedFolderNodes = new ArrayList<>();
386         final QueryResult result = query.execute();
387         Node node;
388 
389         for (NodeIterator nodeIt = result.getNodes(); nodeIt.hasNext();) {
390             node = nodeIt.nextNode();
391             if (node != null) {
392                 translatedFolderNodes.add(node);
393             }
394         }
395 
396         if (!translatedFolderNodes.isEmpty()) {
397             translatedFolderNode = translatedFolderNodes.get(0);
398 
399             if (translatedFolderNodes.size() > 1) {
400                 List<String> translatedFolderNodePaths = new ArrayList<>();
401                 for (Node folderNode : translatedFolderNodes) {
402                     translatedFolderNodePaths.add(folderNode.getPath());
403                 }
404                 log.warn("Multiple translated folder nodes found for translation ID, '{}': {}", translationId,
405                         translatedFolderNodePaths);
406             }
407         }
408 
409         return translatedFolderNode;
410     }
411 
412     /**
413      * Find translated document handle node under {@code targetContentBaseNode} for the {@code sourceDocumentHandleNode}.
414      * @param targetContentBaseNode target content base folder node
415      * @param sourceDocumentHandleNode source document handle node
416      * @return translated document handle node under {@code targetContentBaseNode} for the {@code sourceDocumentHandleNode}
417      * @throws RepositoryException if repository exception occurs
418      */
419     @SuppressWarnings("deprecation")
420     private Node findTargetTranslatedDocumentHandleNode(final Node targetContentBaseNode, final Node sourceDocumentHandleNode)
421             throws RepositoryException {
422         if (!sourceDocumentHandleNode.isNodeType(HippoNodeType.NT_HANDLE)
423                 || !sourceDocumentHandleNode.hasNode(sourceDocumentHandleNode.getName())) {
424             return null;
425         }
426 
427         final Node sourceDocumentVariantNode = sourceDocumentHandleNode.getNode(sourceDocumentHandleNode.getName());
428 
429         if (!sourceDocumentVariantNode.isNodeType(HippoTranslationNodeType.NT_TRANSLATED)) {
430             return null;
431         }
432 
433         final String translationId = JcrUtils.getStringProperty(sourceDocumentVariantNode, HippoTranslationNodeType.ID, null);
434 
435         final String statement = MessageFormat.format(TRANSLATED_DOCUMENT_HANDLE_QUERY, targetContentBaseNode.getPath(),
436                 translationId);
437         final Query query = targetContentBaseNode.getSession().getWorkspace().getQueryManager().createQuery(statement,
438                 Query.XPATH);
439 
440         final List<Node> translatedDocumentHandleNodes = new ArrayList<>();
441         final QueryResult result = query.execute();
442         Node node;
443 
444         for (NodeIterator nodeIt = result.getNodes(); nodeIt.hasNext();) {
445             node = nodeIt.nextNode();
446             if (node != null) {
447                 translatedDocumentHandleNodes.add(node);
448             }
449         }
450 
451         Node translatedDocumentHandleNode = null;
452 
453         if (!translatedDocumentHandleNodes.isEmpty()) {
454             translatedDocumentHandleNode = translatedDocumentHandleNodes.get(0);
455 
456             if (translatedDocumentHandleNodes.size() > 1) {
457                 List<String> translatedDocumentHandleNodePaths = new ArrayList<>();
458                 for (Node handleNode : translatedDocumentHandleNodes) {
459                     translatedDocumentHandleNodePaths.add(handleNode.getPath());
460                 }
461                 log.warn("Multiple translated document handle nodes found for translation ID, '{}': {}", translationId,
462                         translatedDocumentHandleNodePaths);
463             }
464         }
465 
466         return translatedDocumentHandleNode;
467     }
468 
469     /**
470      * In case of targeting, you also get all the locations for the
471      * variants in the list. The returned set can have values that start
472      * with a '/' or without. If they start with a '/', they are absolute
473      * paths (from jcr root). If they don't start with a '/', they are
474      * relative to the channel content root.
475      */
476     private Set<String> getDocumentPathSetInPage(final PageCopyContext pageCopyContext) throws RepositoryException {
477         final FilterPresentComponentConfigurations filterPresentComponentConfigurations
478                 = new FilterPresentComponentConfigurations(pageCopyContext.getSourcePage(), pageCopyContext.getEditingMount().getHstSite(),
479                 pageCopyContext.getTargetMount().getHstSite(),
480                 pageCopyContext.getRequestContext().getSession());
481 
482         List<String> documentPathList = DocumentParamsScanner.findDocumentPathsRecursive(pageCopyContext.getSourcePage(),
483                 Thread.currentThread().getContextClassLoader(), filterPresentComponentConfigurations);
484 
485         return new LinkedHashSet<String>(documentPathList);
486     }
487 
488     public static class FilterPresentComponentConfigurations implements Predicate<HstComponentConfiguration> {
489 
490         private final Set<String> filteredConfigurationUUIDs;
491         public FilterPresentComponentConfigurations(final HstComponentConfiguration sourceConfig, final HstSite sourceSite, final HstSite targetSite, final Session session) {
492             filteredConfigurationUUIDs =  new HashSet<>();
493             populateSkipSet(sourceConfig, sourceSite, targetSite, session, filteredConfigurationUUIDs);
494         }
495 
496         private void populateSkipSet(final HstComponentConfiguration sourceConfig,
497                                      final HstSite sourceSite,
498                                      final HstSite targetSite,
499                                      final Session session,
500                                      final Set<String> skipSet) {
501             try {
502                 final Node sourceNode = session.getNodeByIdentifier(sourceConfig.getCanonicalIdentifier());
503                 if (sourceNode.hasProperty(COMPONENT_PROPERTY_REFERECENCECOMPONENT)) {
504                     String reference = sourceNode.getProperty(COMPONENT_PROPERTY_REFERECENCECOMPONENT).getString();
505                     if (!StringUtils.isBlank(reference)) {
506                         final HstComponentConfiguration targetReference = targetSite.getComponentsConfiguration().getComponentConfiguration(reference);
507                         final HstComponentConfiguration sourceReference = sourceSite.getComponentsConfiguration().getComponentConfiguration(reference);
508                         if (targetReference != null) {
509                             log.debug("Skipping '{}' and descendants because targetSite '{}' already has a resolvable reference for '{}'",
510                                     sourceConfig, targetSite, reference);
511                             if (sourceReference != null) {
512                                 // sourceReference is never expected to be null, but just in case a null check
513                                 populateSelfAndDescending(sourceReference, skipSet);
514                             }
515                             // no need to check descendant configurations
516                             return;
517                         }
518                     }
519                 }
520             } catch (RepositoryException e) {
521                 throw new RuntimeRepositoryException(e);
522             }
523             for (HstComponentConfiguration child : sourceConfig.getChildren().values()) {
524                 populateSkipSet(child, sourceSite, targetSite, session, skipSet);
525             }
526         }
527 
528         private void populateSelfAndDescending(final HstComponentConfiguration current,
529                                                final Set<String> skipSet) {
530             skipSet.add(current.getCanonicalIdentifier());
531             for (HstComponentConfiguration child : current.getChildren().values()) {
532                 populateSelfAndDescending(child, skipSet);
533             }
534         }
535 
536         @Override
537         public boolean test(final HstComponentConfiguration sourceConfig) {
538             return !filteredConfigurationUUIDs.contains(sourceConfig.getCanonicalIdentifier());
539         }
540     }
541 
542     private void translateFolders(final Session session, final Node sourceBaseFolderNode,
543             final String sourceFolderRelPath, final Node targetBaseFolderNode, final String targetFolderRelPath,
544             final String targetTranslationLanguage) throws Exception {
545         String[] sourceFolderNodeNames = StringUtils.split(sourceFolderRelPath, "/");
546         String[] targetFolderNodeNames = StringUtils.split(targetFolderRelPath, "/");
547         String sourceFolderLocation = sourceBaseFolderNode.getPath();
548         String targetFolderLocation = targetBaseFolderNode.getPath();
549 
550         String sourceFolderNodeName;
551         String targetFolderNodeName;
552 
553         for (int i = 0; i < sourceFolderNodeNames.length; i++) {
554             sourceFolderNodeName = sourceFolderNodeNames[i];
555             targetFolderNodeName = (targetFolderNodeNames.length > i) ? targetFolderNodeNames[i] : sourceFolderNodeName;
556 
557             sourceFolderLocation += "/" + sourceFolderNodeName;
558 
559             if (!HippoFolderDocumentUtils.folderExists(session, sourceFolderLocation)) {
560                 throw new IllegalArgumentException("Source folder doesn't exist at '" + sourceFolderLocation + "'.");
561             }
562 
563             targetFolderLocation += "/" + targetFolderNodeName;
564 
565             if (!HippoFolderDocumentUtils.folderExists(session, targetFolderLocation)) {
566                 getDocumentManagementServiceClient().translateFolder(sourceFolderLocation, targetTranslationLanguage,
567                         targetFolderNodeName);
568             }
569         }
570     }
571 
572     private static <T> List<T> popAllToList(final Stack<T> stack) {
573         if (stack == null) {
574             return Collections.emptyList();
575         }
576 
577         List<T> list = new LinkedList<>();
578         T object;
579 
580         while (!stack.empty()) {
581             object = stack.pop();
582             list.add(object);
583         }
584 
585         return list;
586     }
587 }