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