1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.module.exchangesimple;
35
36 import info.magnolia.cms.core.Content;
37 import info.magnolia.cms.core.HierarchyManager;
38 import info.magnolia.cms.core.ItemType;
39 import info.magnolia.cms.core.MetaData;
40 import info.magnolia.cms.core.SystemProperty;
41 import info.magnolia.cms.core.version.ContentVersion;
42 import info.magnolia.cms.exchange.ExchangeException;
43 import info.magnolia.cms.exchange.Subscriber;
44 import info.magnolia.cms.exchange.Subscription;
45 import info.magnolia.cms.exchange.Syndicator;
46 import info.magnolia.cms.security.AccessDeniedException;
47 import info.magnolia.cms.security.SecurityUtil;
48 import info.magnolia.cms.security.User;
49 import info.magnolia.cms.util.ContentUtil;
50 import info.magnolia.cms.util.Rule;
51 import info.magnolia.cms.util.RuleBasedContentFilter;
52 import info.magnolia.context.MgnlContext;
53 import info.magnolia.init.MagnoliaConfigurationProperties;
54 import info.magnolia.logging.AuditLoggingUtil;
55 import info.magnolia.module.exchangesimple.monitor.ActivationMonitor;
56 import info.magnolia.objectfactory.Components;
57
58 import java.io.File;
59 import java.io.IOException;
60 import java.io.UnsupportedEncodingException;
61 import java.net.HttpURLConnection;
62 import java.net.MalformedURLException;
63 import java.net.URL;
64 import java.net.URLConnection;
65 import java.net.URLEncoder;
66 import java.util.Calendar;
67 import java.util.Iterator;
68 import java.util.List;
69
70 import javax.jcr.RepositoryException;
71 import javax.jcr.Session;
72
73 import org.apache.commons.io.IOUtils;
74 import org.apache.commons.lang.StringUtils;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 import EDU.oswego.cs.dl.util.concurrent.Sync;
79
80 import com.google.inject.Inject;
81
82
83
84
85
86
87
88 public abstract class BaseSyndicatorImpl implements Syndicator {
89 private static final Logger log = LoggerFactory.getLogger(BaseSyndicatorImpl.class);
90
91
92
93
94 public static final String DEFAULT_HANDLER = ".magnolia/activation";
95
96 public static final String PARENT_PATH = "mgnlExchangeParentPath";
97
98 public static final String MAPPED_PARENT_PATH = "mgnlExchangeMappedParent";
99
100
101
102
103 public static final String PATH = "mgnlExchangePath";
104
105 public static final String NODE_UUID = "mgnlExchangeNodeUUID";
106
107
108
109
110 @Deprecated
111 public static final String REPOSITORY_NAME = "mgnlExchangeRepositoryName";
112
113 public static final String WORKSPACE_NAME = "mgnlExchangeWorkspaceName";
114
115 public static final String VERSION_NAME = "mgnlExchangeVersionName";
116
117
118
119
120 public static final String RESOURCE_MAPPING_FILE = "mgnlExchangeResourceMappingFile";
121
122 public static final String UTF8_STATUS = "mgnlUTF8Status";
123
124
125
126
127
128
129 public static final String SIBLINGS_ROOT_ELEMENT = "NodeSiblings";
130
131 public static final String SIBLINGS_ELEMENT = "sibling";
132
133 public static final String SIBLING_UUID = "siblingUUID";
134
135 public static final String RESOURCE_MAPPING_FILE_ELEMENT = "File";
136
137 public static final String RESOURCE_MAPPING_NAME_ATTRIBUTE = "name";
138
139 public static final String RESOURCE_MAPPING_UUID_ATTRIBUTE = "contentUUID";
140
141 public static final String RESOURCE_MAPPING_ID_ATTRIBUTE = "resourceId";
142
143 public static final String RESOURCE_MAPPING_MD_ATTRIBUTE = "resourceMD";
144
145 public static final String RESOURCE_MAPPING_ROOT_ELEMENT = "Resources";
146
147 public static final String ACTION = "mgnlExchangeAction";
148
149 public static final String ACTIVATE = "activate";
150
151 public static final String DEACTIVATE = "deactivate";
152
153 public static final String COMMIT = "commit";
154
155 public static final String ROLLBACK = "rollback";
156
157 public static final String CONTENT_FILTER_RULE = "mgnlExchangeFilterRule";
158
159 public static final String ACTIVATION_SUCCESSFUL = "sa_success";
160
161 public static final String ACTIVATION_HANDSHAKE = "sa_handshake";
162
163 public static final String ACTIVATION_FAILED = "sa_failed";
164
165 public static final String ACTIVATION_ATTRIBUTE_STATUS = "sa_attribute_status";
166
167 public static final String ACTIVATION_ATTRIBUTE_MESSAGE = "sa_attribute_message";
168
169 public static final String ACTIVATION_ATTRIBUTE_VERSION = "sa_attribute_version";
170
171 public static final String ACTIVATION_AUTH = "X-magnolia-act-auth";
172 public static final String ACTIVATION_AUTH_KEY = "X-magnolia-act-auth-init";
173
174 protected ActivationMonitor activationMonitor;
175
176 public BaseSyndicatorImpl() {
177 this(Components.getComponent(ActivationMonitor.class));
178 }
179
180 @Inject
181 public BaseSyndicatorImpl(ActivationMonitor activationMonitor) {
182 this.activationMonitor = activationMonitor;
183 }
184
185
186
187
188
189
190
191 protected static void executeInPool(Runnable job) throws ExchangeException {
192 try {
193 ThreadPool.getInstance().execute(job);
194 } catch (InterruptedException e) {
195
196
197
198
199 String message = "could not execute job in pool";
200 log.error(message, e);
201 throw new ExchangeException(message, e);
202 }
203 }
204
205
206
207
208
209
210
211
212
213 protected static void acquireIgnoringInterruption(Sync latch) {
214 try {
215 latch.acquire();
216 } catch (InterruptedException e) {
217
218 acquireIgnoringInterruption(latch);
219
220 Thread.currentThread().interrupt();
221 }
222 }
223
224
225
226
227 @Deprecated
228 protected String repositoryName;
229
230 protected String workspaceName;
231
232 protected String parent;
233
234 protected Rule contentFilterRule;
235
236 protected User user;
237
238 private Calendar contentVersionDate;
239
240 private MagnoliaConfigurationProperties properties;
241
242 private ResourceCollector resourceCollector;
243
244
245
246
247
248
249
250
251 @Override
252 public void init(User user, String repositoryName, String workspaceName, Rule rule) {
253 this.user = user;
254 this.contentFilterRule = rule;
255 this.repositoryName = repositoryName;
256 this.workspaceName = workspaceName;
257 }
258
259
260
261
262
263
264
265
266
267 @Override
268 public void activate(String parent, Content content) throws ExchangeException, RepositoryException {
269 this.activate(parent, content, null);
270 }
271
272
273
274
275
276
277
278
279
280
281 @Override
282 public void activate(String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
283 this.activate(null, parent, content, orderBefore);
284 }
285
286
287
288
289
290
291
292
293
294
295 @Override
296 public void activate(Subscriber subscriber, String parent, Content content) throws ExchangeException, RepositoryException {
297 this.activate(subscriber, parent, content, null);
298 }
299
300
301
302
303
304
305
306
307
308
309
310 @Override
311 public void activate(Subscriber subscriber, String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
312 this.parent = parent;
313 String path = content.getHandle();
314
315 if (content instanceof ContentVersion) {
316 contentVersionDate = ((ContentVersion) content).getCreated();
317 }
318
319 ActivationContent activationContent = null;
320 try {
321 activationContent = resourceCollector.collect(content, orderBefore, parent, workspaceName, repositoryName, contentFilterRule);
322 activationContent.prepareTempFile();
323 if (null == subscriber) {
324 this.activate(activationContent, path);
325 } else {
326 this.activate(subscriber, activationContent, path);
327 }
328 if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
329 final HierarchyManager hm = content.getHierarchyManager();
330 final Session session = content.getJCRNode().getSession();
331 String uuid = content.getUUID();
332 if (StringUtils.isNotBlank(uuid)) {
333 if (content instanceof ContentVersion) {
334
335 content = hm.getContentByUUID(uuid);
336 }
337 Content parentContent = content.getParent();
338 content.delete();
339 parentContent.save();
340 } else {
341 log.warn("Content {}:{} was already removed.", new String[] { content.getWorkspace().getName(), path });
342 }
343 } else {
344 this.updateActivationDetails(path);
345 }
346 log.info("Exchange: activation succeeded [{}]", path);
347 } catch (Exception e) {
348 if (log.isDebugEnabled()) {
349 log.error("Exchange: activation failed for path:" + (path != null ? path : "[null]"), e);
350 long timestamp = System.currentTimeMillis();
351 log.warn("moving files from failed activation to *.failed" + timestamp);
352 Iterator<File> keys = activationContent.getFiles().values().iterator();
353 while (keys.hasNext()) {
354 File f = keys.next();
355 f.renameTo(new File(f.getAbsolutePath() + ".failed" + timestamp));
356 }
357 activationContent.getFiles().clear();
358
359 }
360 throw new ExchangeException(e);
361 } finally {
362 log.debug("Cleaning temporary files");
363 cleanTemporaryStore(activationContent);
364 }
365 }
366
367
368
369
370 public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
371
372
373
374
375 public String activate(Subscriber subscriber, ActivationContent activationContent, String nodePath) throws ExchangeException {
376
377 log.debug("activate");
378 if (null == subscriber) {
379 throw new ExchangeException("Null Subscriber");
380 }
381
382
383 long start = System.currentTimeMillis();
384
385 boolean success = true;
386 for (File f : activationContent.getFiles().values()) {
387 activationMonitor.addSizeOfActivatedContent(f.length());
388 }
389
390 String parentPath = null;
391
392
393 Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
394 if (null != subscription) {
395
396 parentPath = this.getMappedPath(this.parent, subscription);
397 activationContent.setProperty(PARENT_PATH, parentPath);
398 } else {
399 log.debug("Exchange : subscriber [{}] is not subscribed to {}", subscriber.getName(), nodePath);
400 return "not subscribed";
401 }
402 log.debug("Exchange : sending activation request to {} with user {}", subscriber.getName(), this.user.getName());
403
404 URLConnection urlConnection = null;
405 String versionName = null;
406 try {
407 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
408 versionName = transportActivatedData(activationContent, urlConnection, null);
409
410 String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
411
412 if (StringUtils.equals(status, ACTIVATION_HANDSHAKE)) {
413 String handshakeKey = urlConnection.getHeaderField(ACTIVATION_AUTH);
414
415 urlConnection.getContent();
416
417
418 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
419
420 versionName = transportActivatedData(activationContent, urlConnection, handshakeKey);
421 status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
422 }
423
424
425 if (StringUtils.equals(status, ACTIVATION_FAILED)) {
426 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
427 throw new ExchangeException("Message received from subscriber: " + message);
428 }
429 urlConnection.getContent();
430 log.debug("Exchange : activation request sent to {}", subscriber.getName());
431 } catch (ExchangeException e) {
432 activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
433 success = false;
434 throw e;
435 } catch (IOException e) {
436 log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
437 String url = urlConnection == null ? null : urlConnection.getURL().toString();
438 url = SecurityUtil.stripPasswordFromUrl(url);
439 activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
440 success = false;
441
442 throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
443 } catch (Exception e) {
444 activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
445 success = false;
446 throw new ExchangeException(e);
447 } finally {
448 releaseConnection(urlConnection);
449 long end = System.currentTimeMillis();
450 activationMonitor.addActivationTime(end - start);
451 activationMonitor.logActivation(nodePath, user.getName(), workspaceName, subscriber.getName(), false, success);
452 }
453 return versionName;
454 }
455
456 protected void releaseConnection(URLConnection urlConnection) {
457 if (urlConnection == null) {
458 return;
459 }
460
461 try {
462 IOUtils.closeQuietly(urlConnection.getInputStream());
463 } catch (IOException e) {
464
465 if (log.isDebugEnabled()) {
466 log.error("Failed to release input stream of activation connection with " + e.getMessage(), e);
467 }
468 }
469 if (urlConnection instanceof HttpURLConnection) {
470 ((HttpURLConnection) urlConnection).disconnect();
471 }
472 }
473 private String transportActivatedData(ActivationContent activationContent, URLConnection urlConnection, String handshakeKey) throws ExchangeException {
474 String versionName;
475
476 this.addActivationHeaders(urlConnection, activationContent, handshakeKey);
477
478 Transporter.transport((HttpURLConnection) urlConnection, activationContent);
479
480 versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
481 return versionName;
482 }
483
484
485
486
487 protected void cleanTemporaryStore(ActivationContent activationContent) {
488 if (activationContent == null) {
489 log.debug("Clean temporary store - nothing to do");
490 return;
491 }
492 if (log.isDebugEnabled()) {
493 log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
494 return;
495 }
496
497 Iterator<String> keys = activationContent.getFiles().keySet().iterator();
498 while (keys.hasNext()) {
499 String key = keys.next();
500 log.debug("Removing temporary file {}", key);
501 activationContent.getFile(key).delete();
502 }
503 }
504
505 public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
506 final Content node = getHierarchyManager().getContent(path);
507 deactivate(node);
508 }
509
510
511
512
513
514
515 @Override
516 public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
517 String nodeUUID = node.getUUID();
518 String path = node.getHandle();
519 this.doDeactivate(nodeUUID, path);
520 updateDeactivationDetails(nodeUUID);
521 }
522
523
524
525
526
527
528
529 @Override
530 public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
531 String nodeUUID = node.getUUID();
532 String path = node.getHandle();
533 this.doDeactivate(subscriber, nodeUUID, path);
534 updateDeactivationDetails(nodeUUID);
535 }
536
537
538
539
540 public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
541
542
543
544
545
546
547
548 public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
549
550
551
552
553
554
555 protected String getDeactivationURL(Subscriber subscriberInfo) {
556 return getActivationURL(subscriberInfo);
557 }
558
559
560
561
562
563
564
565
566 protected void addDeactivationHeaders(URLConnection connection, String nodeUUID, String handshakeKey) {
567 connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
568 connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
569
570 String md5 = "";
571 if (nodeUUID != null) {
572 connection.addRequestProperty(NODE_UUID, nodeUUID);
573
574 md5 = SecurityUtil.getMD5Hex(nodeUUID);
575 }
576
577 String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
578
579
580 addHandshakeInfo(connection, handshakeKey);
581
582 connection.setRequestProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
583 connection.addRequestProperty(ACTION, DEACTIVATE);
584 }
585
586 protected void addHandshakeInfo(URLConnection connection, String handshakeKey) {
587 if (handshakeKey != null) {
588 connection.setRequestProperty(ACTIVATION_AUTH_KEY, SecurityUtil.encrypt(SecurityUtil.getPublicKey(), handshakeKey));
589 }
590 }
591
592
593
594
595 protected String getActivationURL(Subscriber subscriberInfo) {
596 final String url = subscriberInfo.getURL();
597 if (!url.endsWith("/")) {
598 return url + "/" + DEFAULT_HANDLER;
599 }
600 return url + DEFAULT_HANDLER;
601 }
602
603
604
605
606
607
608
609 protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent, String handshakeKey) {
610
611 String md5 = activationContent.getproperty(RESOURCE_MAPPING_MD_ATTRIBUTE);
612 String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
613 activationContent.setProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
614 Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
615 while (headerKeys.hasNext()) {
616 String key = headerKeys.next();
617 if (RESOURCE_MAPPING_MD_ATTRIBUTE.equals(key)) {
618
619 continue;
620 }
621 String value = activationContent.getproperty(key);
622 if (SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
623 try {
624 value = URLEncoder.encode(value, "UTF-8");
625 } catch (UnsupportedEncodingException e) {
626
627 }
628 }
629 connection.setRequestProperty(key, value);
630 }
631 addHandshakeInfo(connection, handshakeKey);
632 }
633
634
635
636
637 protected void updateActivationDetails(String path) throws RepositoryException {
638
639 Content page = getSystemHierarchyManager().getContent(path);
640 updateMetaData(page, ACTIVATE);
641 page.save();
642 AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path);
643 }
644
645
646
647
648 protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
649
650 Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
651 updateMetaData(page, DEACTIVATE);
652 page.save();
653 AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle());
654 }
655
656 private HierarchyManager getHierarchyManager() {
657 return MgnlContext.getHierarchyManager(this.workspaceName);
658 }
659
660 private HierarchyManager getSystemHierarchyManager() {
661 return MgnlContext.getSystemContext().getHierarchyManager(this.workspaceName);
662 }
663
664
665
666
667
668 protected void updateMetaData(Content node, String type) throws AccessDeniedException {
669
670 MetaData md = node.getMetaData();
671 if (type.equals(ACTIVATE)) {
672 md.setActivated();
673 }
674 else {
675 md.setUnActivated();
676 }
677 md.setActivatorId(this.user.getName());
678 md.setLastActivationActionDate();
679
680 if (type.equals(ACTIVATE)) {
681 if (md.getModificationDate() != null && md.getModificationDate().after(contentVersionDate)) {
682 try {
683 Thread.sleep(1);
684 } catch (InterruptedException e) {
685 e.printStackTrace();
686 }
687 md.setModificationDate();
688 }
689 }
690
691 Iterator<Content> children;
692 if (type.equals(ACTIVATE)) {
693
694 children = node.getChildren(new RuleBasedContentFilter(contentFilterRule)).iterator();
695 }
696 else {
697
698 children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
699 }
700
701 while (children.hasNext()) {
702 Content child = children.next();
703 this.updateMetaData(child, type);
704 }
705
706 }
707
708
709
710
711 protected String getMappedPath(String path, Subscription subscription) {
712 String toURI = subscription.getToURI();
713 if (null != toURI) {
714 String fromURI = subscription.getFromURI();
715
716 fromURI = StringUtils.removeEnd(fromURI, "/");
717 toURI = StringUtils.removeEnd(toURI, "/");
718
719 path = path.replaceFirst(fromURI, toURI);
720 if (path.equals("")) {
721 path = "/";
722 }
723 }
724 return path;
725 }
726
727 protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
728
729
730
731 try {
732 URL url = new URL(urlString);
733 URLConnection urlConnection = url.openConnection();
734 urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
735 urlConnection.setReadTimeout(subscriber.getReadTimeout());
736
737 return urlConnection;
738 } catch (MalformedURLException e) {
739 throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + SecurityUtil.stripPasswordFromUrl(urlString) + "]");
740 } catch (IOException e) {
741 throw new ExchangeException("Not able to send the activation request [" + SecurityUtil.stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
742 } catch (Exception e) {
743 throw new ExchangeException(e);
744 }
745 }
746
747 @Inject
748 public void setResouceCollector(ResourceCollector resourceCollector) {
749 this.resourceCollector = resourceCollector;
750 }
751 }