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