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 if (null == subscriber) {
323 this.activate(activationContent, path);
324 } else {
325 this.activate(subscriber, activationContent, path);
326 }
327 if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
328 final HierarchyManager hm = content.getHierarchyManager();
329 final Session session = content.getJCRNode().getSession();
330 String uuid = content.getUUID();
331 if (StringUtils.isNotBlank(uuid)) {
332 if (content instanceof ContentVersion) {
333
334 content = hm.getContentByUUID(uuid);
335 }
336 Content parentContent = content.getParent();
337 content.delete();
338 parentContent.save();
339 } else {
340 log.warn("Content {}:{} was already removed.", new String[] { content.getWorkspace().getName(), path });
341 }
342 } else {
343 this.updateActivationDetails(path);
344 }
345 log.info("Exchange: activation succeeded [{}]", path);
346 } catch (Exception e) {
347 if (log.isDebugEnabled()) {
348 log.error("Exchange: activation failed for path:" + (path != null ? path : "[null]"), e);
349 long timestamp = System.currentTimeMillis();
350 log.warn("moving files from failed activation to *.failed" + timestamp);
351 Iterator<File> keys = activationContent.getFiles().values().iterator();
352 while (keys.hasNext()) {
353 File f = keys.next();
354 f.renameTo(new File(f.getAbsolutePath() + ".failed" + timestamp));
355 }
356 activationContent.getFiles().clear();
357
358 }
359 throw new ExchangeException(e);
360 } finally {
361 log.debug("Cleaning temporary files");
362 cleanTemporaryStore(activationContent);
363 }
364 }
365
366
367
368
369 public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
370
371
372
373
374 public String activate(Subscriber subscriber, ActivationContent activationContent, String nodePath) throws ExchangeException {
375
376 log.debug("activate");
377 if (null == subscriber) {
378 throw new ExchangeException("Null Subscriber");
379 }
380
381 long start = System.currentTimeMillis();
382
383 boolean success = true;
384 for (File f : activationContent.getFiles().values()) {
385 activationMonitor.addSizeOfActivatedContent(f.length());
386 }
387
388 String parentPath = null;
389
390
391 Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
392 if (null != subscription) {
393
394 parentPath = this.getMappedPath(this.parent, subscription);
395 activationContent.setProperty(PARENT_PATH, parentPath);
396 } else {
397 log.debug("Exchange : subscriber [{}] is not subscribed to {}", subscriber.getName(), nodePath);
398 return "not subscribed";
399 }
400 log.debug("Exchange : sending activation request to {} with user {}", subscriber.getName(), this.user.getName());
401
402 URLConnection urlConnection = null;
403 String versionName = null;
404 try {
405 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
406 versionName = transportActivatedData(activationContent, urlConnection, null);
407
408 String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
409
410 if (StringUtils.equals(status, ACTIVATION_HANDSHAKE)) {
411 String handshakeKey = urlConnection.getHeaderField(ACTIVATION_AUTH);
412
413 urlConnection.getContent();
414
415
416 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
417
418 versionName = transportActivatedData(activationContent, urlConnection, handshakeKey);
419 status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
420 }
421
422
423 if (StringUtils.equals(status, ACTIVATION_FAILED)) {
424 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
425 throw new ExchangeException("Message received from subscriber: " + message);
426 }
427 urlConnection.getContent();
428 log.debug("Exchange : activation request sent to {}", subscriber.getName());
429 } catch (ExchangeException e) {
430 activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
431 success = false;
432 throw e;
433 } catch (IOException e) {
434 log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
435 String url = urlConnection == null ? null : urlConnection.getURL().toString();
436 url = SecurityUtil.stripPasswordFromUrl(url);
437 activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
438 success = false;
439
440 throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
441 } catch (Exception e) {
442 activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
443 success = false;
444 throw new ExchangeException(e);
445 } finally {
446 releaseConnection(urlConnection);
447 long end = System.currentTimeMillis();
448 activationMonitor.addActivationTime(end - start);
449 activationMonitor.logActivation(nodePath, user.getName(), workspaceName, subscriber.getName(), false, success);
450 }
451 return versionName;
452 }
453
454 protected void releaseConnection(URLConnection urlConnection) {
455 if (urlConnection == null) {
456 return;
457 }
458
459 try {
460 IOUtils.closeQuietly(urlConnection.getInputStream());
461 } catch (IOException e) {
462
463 if (log.isDebugEnabled()) {
464 log.error("Failed to release input stream of activation connection with " + e.getMessage(), e);
465 }
466 }
467 if (urlConnection instanceof HttpURLConnection) {
468 ((HttpURLConnection) urlConnection).disconnect();
469 }
470 }
471 private String transportActivatedData(ActivationContent activationContent, URLConnection urlConnection, String handshakeKey) throws ExchangeException {
472 String versionName;
473 this.addActivationHeaders(urlConnection, activationContent, handshakeKey);
474
475 Transporter.transport((HttpURLConnection) urlConnection, activationContent);
476
477 versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
478 return versionName;
479 }
480
481
482
483
484 protected void cleanTemporaryStore(ActivationContent activationContent) {
485 if (activationContent == null) {
486 log.debug("Clean temporary store - nothing to do");
487 return;
488 }
489 if (log.isDebugEnabled()) {
490 log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
491 return;
492 }
493
494 Iterator<String> keys = activationContent.getFiles().keySet().iterator();
495 while (keys.hasNext()) {
496 String key = keys.next();
497 log.debug("Removing temporary file {}", key);
498 activationContent.getFile(key).delete();
499 }
500 }
501
502 public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
503 final Content node = getHierarchyManager().getContent(path);
504 deactivate(node);
505 }
506
507
508
509
510
511
512 @Override
513 public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
514 String nodeUUID = node.getUUID();
515 String path = node.getHandle();
516 this.doDeactivate(nodeUUID, path);
517 updateDeactivationDetails(nodeUUID);
518 }
519
520
521
522
523
524
525
526 @Override
527 public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
528 String nodeUUID = node.getUUID();
529 String path = node.getHandle();
530 this.doDeactivate(subscriber, nodeUUID, path);
531 updateDeactivationDetails(nodeUUID);
532 }
533
534
535
536
537 public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
538
539
540
541
542
543
544
545 public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
546
547
548
549
550
551
552 protected String getDeactivationURL(Subscriber subscriberInfo) {
553 return getActivationURL(subscriberInfo);
554 }
555
556
557
558
559
560
561
562
563 protected void addDeactivationHeaders(URLConnection connection, String nodeUUID, String handshakeKey) {
564 connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
565 connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
566
567 String md5 = "";
568 if (nodeUUID != null) {
569 connection.addRequestProperty(NODE_UUID, nodeUUID);
570
571 md5 = SecurityUtil.getMD5Hex(nodeUUID);
572 }
573
574 String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
575
576
577 addHandshakeInfo(connection, handshakeKey);
578
579 connection.setRequestProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
580 connection.addRequestProperty(ACTION, DEACTIVATE);
581 }
582
583 protected void addHandshakeInfo(URLConnection connection, String handshakeKey) {
584 if (handshakeKey != null) {
585 connection.setRequestProperty(ACTIVATION_AUTH_KEY, SecurityUtil.encrypt(SecurityUtil.getPublicKey(), handshakeKey));
586 }
587 }
588
589
590
591
592 protected String getActivationURL(Subscriber subscriberInfo) {
593 final String url = subscriberInfo.getURL();
594 if (!url.endsWith("/")) {
595 return url + "/" + DEFAULT_HANDLER;
596 }
597 return url + DEFAULT_HANDLER;
598 }
599
600
601
602
603
604
605
606 protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent, String handshakeKey) {
607
608 String md5 = activationContent.getproperty(RESOURCE_MAPPING_MD_ATTRIBUTE);
609 String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
610 activationContent.setProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
611 Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
612 while (headerKeys.hasNext()) {
613 String key = headerKeys.next();
614 if (RESOURCE_MAPPING_MD_ATTRIBUTE.equals(key)) {
615
616 continue;
617 }
618 String value = activationContent.getproperty(key);
619 if (SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
620 try {
621 value = URLEncoder.encode(value, "UTF-8");
622 } catch (UnsupportedEncodingException e) {
623
624 }
625 }
626 connection.setRequestProperty(key, value);
627 }
628 addHandshakeInfo(connection, handshakeKey);
629 }
630
631
632
633
634 protected void updateActivationDetails(String path) throws RepositoryException {
635
636 Content page = getSystemHierarchyManager().getContent(path);
637 updateMetaData(page, ACTIVATE);
638 page.save();
639 AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path);
640 }
641
642
643
644
645 protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
646
647 Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
648 updateMetaData(page, DEACTIVATE);
649 page.save();
650 AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle());
651 }
652
653 private HierarchyManager getHierarchyManager() {
654 return MgnlContext.getHierarchyManager(this.workspaceName);
655 }
656
657 private HierarchyManager getSystemHierarchyManager() {
658 return MgnlContext.getSystemContext().getHierarchyManager(this.workspaceName);
659 }
660
661
662
663
664
665 protected void updateMetaData(Content node, String type) throws AccessDeniedException {
666
667 MetaData md = node.getMetaData();
668 if (type.equals(ACTIVATE)) {
669 md.setActivated();
670 }
671 else {
672 md.setUnActivated();
673 }
674 md.setActivatorId(this.user.getName());
675 md.setLastActivationActionDate();
676
677 if (type.equals(ACTIVATE)) {
678 if (md.getModificationDate() != null && md.getModificationDate().after(contentVersionDate)) {
679 try {
680 Thread.sleep(1);
681 } catch (InterruptedException e) {
682 e.printStackTrace();
683 }
684 md.setModificationDate();
685 }
686 }
687
688 Iterator<Content> children;
689 if (type.equals(ACTIVATE)) {
690
691 children = node.getChildren(new RuleBasedContentFilter(contentFilterRule)).iterator();
692 }
693 else {
694
695 children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
696 }
697
698 while (children.hasNext()) {
699 Content child = children.next();
700 this.updateMetaData(child, type);
701 }
702
703 }
704
705
706
707
708 protected String getMappedPath(String path, Subscription subscription) {
709 String toURI = subscription.getToURI();
710 if (null != toURI) {
711 String fromURI = subscription.getFromURI();
712
713 fromURI = StringUtils.removeEnd(fromURI, "/");
714 toURI = StringUtils.removeEnd(toURI, "/");
715
716 path = path.replaceFirst(fromURI, toURI);
717 if (path.equals("")) {
718 path = "/";
719 }
720 }
721 return path;
722 }
723
724 protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
725
726
727
728 try {
729 URL url = new URL(urlString);
730 URLConnection urlConnection = url.openConnection();
731 urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
732 urlConnection.setReadTimeout(subscriber.getReadTimeout());
733
734 return urlConnection;
735 } catch (MalformedURLException e) {
736 throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + SecurityUtil.stripPasswordFromUrl(urlString) + "]");
737 } catch (IOException e) {
738 throw new ExchangeException("Not able to send the activation request [" + SecurityUtil.stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
739 } catch (Exception e) {
740 throw new ExchangeException(e);
741 }
742 }
743
744 @Inject
745 public void setResouceCollector(ResourceCollector resourceCollector) {
746 this.resourceCollector = resourceCollector;
747 }
748 }