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