1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
60
61
62
63
64
65
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
109
110
111
112
113 protected void onBeforePageCopyEvent(PageCopyEvent pageCopyEvent) {
114 }
115
116
117
118
119
120
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
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
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
321
322
323
324
325
326
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
364
365
366
367
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
414
415
416
417
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
471
472
473
474
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
513 populateSelfAndDescending(sourceReference, skipSet);
514 }
515
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 }