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