View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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.SecurityUtil;
50  import info.magnolia.cms.security.User;
51  import info.magnolia.cms.util.ContentUtil;
52  import info.magnolia.cms.util.Rule;
53  import info.magnolia.cms.util.RuleBasedContentFilter;
54  import info.magnolia.context.MgnlContext;
55  import info.magnolia.logging.AuditLoggingUtil;
56  
57  import java.io.File;
58  import java.io.FileInputStream;
59  import java.io.FileOutputStream;
60  import java.io.IOException;
61  import java.io.OutputStream;
62  import java.io.UnsupportedEncodingException;
63  import java.net.HttpURLConnection;
64  import java.net.MalformedURLException;
65  import java.net.URL;
66  import java.net.URLConnection;
67  import java.net.URLEncoder;
68  import java.util.Calendar;
69  import java.util.Iterator;
70  import java.util.List;
71  import java.util.zip.GZIPOutputStream;
72  
73  import javax.jcr.RepositoryException;
74  import javax.jcr.Session;
75  
76  import org.apache.commons.codec.binary.Base64;
77  import org.apache.commons.io.IOUtils;
78  import org.apache.commons.lang.StringUtils;
79  import org.apache.xml.serialize.OutputFormat;
80  import org.apache.xml.serialize.XMLSerializer;
81  import org.jdom.Document;
82  import org.jdom.Element;
83  import org.jdom.output.XMLOutputter;
84  import org.slf4j.Logger;
85  import org.slf4j.LoggerFactory;
86  import org.xml.sax.InputSource;
87  import org.xml.sax.SAXException;
88  import org.xml.sax.XMLReader;
89  import org.xml.sax.helpers.XMLReaderFactory;
90  
91  import EDU.oswego.cs.dl.util.concurrent.Sync;
92  
93  /**
94   * Default implementation of {@link Syndicator}. Activates all the content to a subscriber configured on the server.
95   * @author Sameer Charles
96   * $Id: $
97   */
98  public abstract class BaseSyndicatorImpl implements Syndicator {
99      private static final Logger log = LoggerFactory.getLogger(BaseSyndicatorImpl.class);
100 
101     /**
102      * URI used for activation.
103      */
104     public static final String DEFAULT_HANDLER = ".magnolia/activation"; //$NON-NLS-1$
105 
106     public static final String PARENT_PATH = "mgnlExchangeParentPath";
107 
108     public static final String MAPPED_PARENT_PATH = "mgnlExchangeMappedParent";
109 
110     /**
111      * Path to be activated or deactivated.
112      */
113     public static final String PATH = "mgnlExchangePath";
114 
115     public static final String NODE_UUID = "mgnlExchangeNodeUUID";
116 
117     public static final String REPOSITORY_NAME = "mgnlExchangeRepositoryName";
118 
119     public static final String WORKSPACE_NAME = "mgnlExchangeWorkspaceName";
120 
121     public static final String VERSION_NAME = "mgnlExchangeVersionName";
122 
123     /**
124      * Name of the resource containing reading sequence for importing the data in activation target.
125      */
126     public static final String RESOURCE_MAPPING_FILE = "mgnlExchangeResourceMappingFile";
127 
128     public static final String UTF8_STATUS = "mgnlUTF8Status";
129 
130     /**
131      * Name of the element in the resource file describing siblings of activated node.
132      * Siblings element will contain all siblings of the same node type which are "before"
133      * this node.
134      */
135     public static final String SIBLINGS_ROOT_ELEMENT = "NodeSiblings";
136 
137     public static final String SIBLINGS_ELEMENT = "sibling";
138 
139     public static final String SIBLING_UUID = "siblingUUID";
140 
141     public static final String RESOURCE_MAPPING_FILE_ELEMENT = "File";
142 
143     public static final String RESOURCE_MAPPING_NAME_ATTRIBUTE = "name";
144 
145     public static final String RESOURCE_MAPPING_UUID_ATTRIBUTE = "contentUUID";
146 
147     public static final String RESOURCE_MAPPING_ID_ATTRIBUTE = "resourceId";
148 
149     public static final String RESOURCE_MAPPING_ROOT_ELEMENT = "Resources";
150 
151     public static final String ACTION = "mgnlExchangeAction";
152 
153     public static final String ACTIVATE = "activate"; //$NON-NLS-1$
154 
155     public static final String DEACTIVATE = "deactivate"; //$NON-NLS-1$
156 
157     public static final String COMMIT = "commit";
158 
159     public static final String ROLLBACK = "rollback";
160 
161     public static final String AUTHORIZATION = "Authorization";
162 
163     public static final String AUTH_CREDENTIALS= "mgnlUserPSWD";
164 
165     public static final String AUTH_USER = "mgnlUserId";
166 
167     public static final String CONTENT_FILTER_RULE = "mgnlExchangeFilterRule";
168 
169     public static final String ACTIVATION_SUCCESSFUL = "sa_success"; //$NON-NLS-1$
170 
171     public static final String ACTIVATION_FAILED = "sa_failed"; //$NON-NLS-1$
172 
173     public static final String ACTIVATION_ATTRIBUTE_STATUS = "sa_attribute_status"; //$NON-NLS-1$
174 
175     public static final String ACTIVATION_ATTRIBUTE_MESSAGE = "sa_attribute_message"; //$NON-NLS-1$
176 
177     public static final String ACTIVATION_ATTRIBUTE_VERSION = "sa_attribute_version"; //$NON-NLS-1$
178 
179     /**
180      * Runs a given job in the thread pool.
181      *
182      * @param job the job to run
183      * @throws ExchangeException if the job could not be put in the pool
184      */
185     protected static void executeInPool(Runnable job) throws ExchangeException {
186         try {
187             ThreadPool.getInstance().execute(job);
188         } catch (InterruptedException e) {
189             // this is kind of a problem, we could not add the job to the pool
190             // retrying might or might not work now that the interruption
191             // status is cleared but there is not much we can do so throwing
192             // an ExchangeException seems like the least bad choice
193             String message = "could not execute job in pool";
194             log.error(message, e);
195             throw new ExchangeException(message, e);
196         }
197     }
198 
199     /**
200      * Acquires a {@link Sync} ignoring any interruptions. Should any
201      * interruption occur the interruption status will be set. Might
202      * potentially block/wait forever.
203      *
204      * @see Sync#acquire()
205      *
206      * @param latch the latch on which to wait
207      */
208     protected static void acquireIgnoringInterruption(Sync latch) {
209         try {
210             latch.acquire();
211         } catch (InterruptedException e) {
212             // waken up externally - ignore try again
213             acquireIgnoringInterruption(latch);
214             // be a good citizen and set back the interruption status
215             Thread.currentThread().interrupt();
216         }
217     }
218 
219     protected String repositoryName;
220 
221     protected String workspaceName;
222 
223     protected String parent;
224 
225     protected Content.ContentFilter contentFilter;
226 
227     protected Rule contentFilterRule;
228 
229     protected User user;
230 
231     protected String basicCredentials;
232 
233     private Calendar contentVersionDate;
234 
235     /**
236      * @param user
237      * @param repositoryName repository ID
238      * @param workspaceName workspace ID
239      * @param rule content filter rule
240      * @see info.magnolia.cms.exchange.Syndicator#init(info.magnolia.cms.security.User, String, String,
241      * info.magnolia.cms.util.Rule)
242      */
243     public void init(User user, String repositoryName, String workspaceName, Rule rule) {
244         this.user = user;
245         this.basicCredentials = "Basic "
246             + new String(Base64.encodeBase64((this.user.getName() + ":" + this.user.getPassword()).getBytes()));
247         this.contentFilter = new RuleBasedContentFilter(rule);
248         this.contentFilterRule = rule;
249         this.repositoryName = repositoryName;
250         this.workspaceName = workspaceName;
251     }
252 
253     /**
254      * This will activate specifies page (sub pages) to all configured subscribers.
255      *
256      * @param parent parent under which this page will be activated
257      * @param content to be activated
258      * @throws javax.jcr.RepositoryException
259      * @throws info.magnolia.cms.exchange.ExchangeException
260      */
261     public void activate(String parent, Content content) throws ExchangeException, RepositoryException {
262         this.activate(parent, content, null);
263     }
264 
265     /**
266      * This will activate specified node to all configured subscribers.
267      *
268      * @param parent parent under which this page will be activated
269      * @param content to be activated
270      * @param orderBefore List of UUID to be used by the implementation to order this node after activation
271      * @throws javax.jcr.RepositoryException
272      * @throws info.magnolia.cms.exchange.ExchangeException
273      *
274      */
275     public void activate(String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
276         this.activate(null, parent, content, orderBefore);
277     }
278 
279     /**
280      * This will activate specifies page (sub pages) to the specified subscriber.
281      *
282      * @param subscriber
283      * @param parent parent under which this page will be activated
284      * @param content to be activated
285      * @throws javax.jcr.RepositoryException
286      * @throws info.magnolia.cms.exchange.ExchangeException
287      */
288     public void activate(Subscriber subscriber, String parent, Content content) throws ExchangeException, RepositoryException {
289         this.activate(subscriber, parent, content, null);
290     }
291 
292     /**
293      * This will activate specifies node to the specified subscriber.
294      *
295      * @param subscriber
296      * @param parent      parent under which this page will be activated
297      * @param content     to be activated
298      * @param orderBefore List of UUID to be used by the subscriber to order this node after activation
299      * @throws javax.jcr.RepositoryException
300      * @throws info.magnolia.cms.exchange.ExchangeException
301      */
302     public void activate(Subscriber subscriber, String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
303         this.parent = parent;
304         String path = content.getHandle();
305         
306         if (content instanceof ContentVersion) {
307             contentVersionDate = ((ContentVersion)content).getCreated();
308         }
309 
310         ActivationContent activationContent = null;
311         try {
312             activationContent = this.collect(content, orderBefore);
313             if (null == subscriber) {
314                 this.activate(activationContent, path);
315             } else {
316                 this.activate(subscriber, activationContent, path);
317             }
318             if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
319                 final HierarchyManager hm = content.getHierarchyManager();
320                 String uuid = content.getUUID();
321                 if (StringUtils.isNotBlank(uuid)) {
322                     if (content instanceof ContentVersion) {
323                         // replace versioned content with the real node
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[] {hm.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      * @throws ExchangeException
358      */
359     public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
360 
361 
362     /**
363      * Send request of activation of activationContent to the subscriber. Subscriber might choose not to react if it is not subscribed to the URI under which activationContent exists.
364      */
365     public String activate(Subscriber subscriber, ActivationContent activationContent, String nodePath) throws ExchangeException {
366         // FYI: this method is invoked from multiple threads at a same time (one for each subscriber, activationContent is assumed to be NOT shared between threads (cloned or by other means replicated) )
367         log.debug("activate");
368         if (null == subscriber) {
369             throw new ExchangeException("Null Subscriber");
370         }
371 
372         String parentPath = null;
373 
374         // concurrency: from path and repo name are same for all subscribers
375         Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
376         if (null != subscription) {
377             // its subscribed since we found the matching subscription
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             this.addActivationHeaders(urlConnection, activationContent);
391 
392             Transporter.transport((HttpURLConnection) urlConnection, activationContent);
393 
394             String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
395             versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
396 
397             // check if the activation failed
398             if (StringUtils.equals(status, ACTIVATION_FAILED)) {
399                 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
400                 throw new ExchangeException("Message received from subscriber: " + message);
401             }
402             urlConnection.getContent();
403             log.debug("Exchange : activation request sent to {}", subscriber.getName());
404         }
405         catch (ExchangeException e) {
406             throw e;
407         }
408         catch (IOException e) {
409             log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
410             String url = (urlConnection == null ? null : urlConnection.getURL().toString());
411             url = SecurityUtil.stripPasswordFromUrl(url);
412             // hide pwd if present
413             throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
414         }
415         catch (Exception e) {
416             throw new ExchangeException(e);
417         }
418         return versionName;
419     }
420 
421     /**
422      * Cleans up temporary file store after activation.
423      */
424     protected void cleanTemporaryStore(ActivationContent activationContent) {
425         if (activationContent == null) {
426             log.debug("Clean temporary store - nothing to do");
427             return;
428         }
429         if (log.isDebugEnabled()) {
430             log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
431             return;
432         }
433 
434         Iterator<String> keys = activationContent.getFiles().keySet().iterator();
435         while (keys.hasNext()) {
436             String key = keys.next();
437             log.debug("Removing temporary file {}", key);
438             activationContent.getFile(key).delete();
439         }
440     }
441 
442     public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
443         final Content node = getHierarchyManager().getContent(path);
444         deactivate(node);
445     }
446 
447     /**
448      * @param node to deactivate
449      * @throws RepositoryException
450      * @throws ExchangeException
451      */
452     public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
453         String nodeUUID = node.getUUID();
454         String path = node.getHandle();
455         this.doDeactivate(nodeUUID, path);
456         updateDeactivationDetails(nodeUUID);
457     }
458 
459     /**
460      * @param node , to deactivate
461      * @param subscriber
462      * @throws RepositoryException
463      * @throws ExchangeException
464      */
465     public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
466         String nodeUUID = node.getUUID();
467         String path = node.getHandle();
468         this.doDeactivate(subscriber, nodeUUID, path);
469         updateDeactivationDetails(nodeUUID);
470     }
471 
472     /**
473      * @throws ExchangeException
474      */
475     public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
476 
477     /**
478      * Deactivate content from specified subscriber.
479      * @param subscriber
480      * @throws ExchangeException
481      */
482     public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
483 
484     /**
485      * Return URI set for deactivation.
486      * @param subscriberInfo
487      */
488     protected String getDeactivationURL(Subscriber subscriberInfo) {
489         return getActivationURL(subscriberInfo);
490     }
491 
492     /**
493      * Adds header fields describing deactivation request.
494      * @param connection
495      */
496     protected void addDeactivationHeaders(URLConnection connection, String nodeUUID) {
497         connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
498         connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
499         if (nodeUUID != null) {
500             connection.addRequestProperty(NODE_UUID, nodeUUID);
501         }
502         connection.addRequestProperty(ACTION, DEACTIVATE);
503     }
504 
505     /**
506      * Retrieves URL subscriber is listening on for (de)activation requests.
507      */
508     protected String getActivationURL(Subscriber subscriberInfo) {
509         final String url = subscriberInfo.getURL();
510         if (!url.endsWith("/")) {
511             return url + "/" + DEFAULT_HANDLER;
512         }
513         return url + DEFAULT_HANDLER;
514     }
515 
516     /**
517      * Adds headers fields describing activation request.
518      */
519     protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent) {
520         Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
521         while (headerKeys.hasNext()) {
522             String key = headerKeys.next();
523             String value = activationContent.getproperty(key);
524             if(SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
525                 try {
526                     value = URLEncoder.encode(value, "UTF-8");
527                 }
528                 catch (UnsupportedEncodingException e) {
529                     // do nothing
530                 }
531             }
532             connection.setRequestProperty(key, value);
533         }
534     }
535 
536     /**
537      * Updates current content activation meta data with the time stamp and user details of the activation.
538      */
539     protected void updateActivationDetails(String path) throws RepositoryException {
540         // page activated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
541         Content page = getSystemHierarchyManager().getContent(path);
542         updateMetaData(page, ACTIVATE);
543         page.save();
544         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path );
545     }
546 
547     /**
548      * Updates current content activation meta data with the timestamp and user details of the deactivation.
549      */
550     protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
551         // page deactivated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
552         Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
553         updateMetaData(page, DEACTIVATE);
554         page.save();
555         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle() );
556     }
557 
558 
559     private HierarchyManager getHierarchyManager() {
560         return MgnlContext.getHierarchyManager(this.repositoryName, this.workspaceName);
561     }
562 
563     private HierarchyManager getSystemHierarchyManager() {
564         return MgnlContext.getSystemContext().getHierarchyManager(this.repositoryName, this.workspaceName);
565     }
566 
567     /**
568      * @param node
569      * @param type (activate / deactivate)
570      */
571     protected void updateMetaData(Content node, String type) throws AccessDeniedException {
572         // update the passed node
573         MetaData md = node.getMetaData();
574         if (type.equals(ACTIVATE)) {
575             md.setActivated();
576         }
577         else {
578             md.setUnActivated();
579         }
580         md.setActivatorId(this.user.getName());
581         md.setLastActivationActionDate();
582 
583         if(type.equals(ACTIVATE)){
584             if(md.getModificationDate() != null && md.getModificationDate().after(contentVersionDate)){
585                 try {
586                     Thread.sleep(1);
587                 } catch (InterruptedException e) {
588                     e.printStackTrace();
589                 }
590                 md.setModificationDate();
591             }
592         }
593 
594         Iterator<Content> children;
595         if (type.equals(ACTIVATE)) {
596             // use syndicator rule based filter
597             children = node.getChildren(this.contentFilter).iterator();
598         }
599         else {
600             // all children
601             children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
602         }
603 
604         while (children.hasNext()) {
605             Content child = children.next();
606             this.updateMetaData(child, type);
607         }
608 
609 
610     }
611 
612     /**
613      * Collects all information about activated content and its children (those that are set to be activated with the parent by filter rules).
614      * @throws Exception
615      */
616     protected ActivationContent collect(Content node, List<String> orderBefore) throws Exception {
617         // make sure resource file is unique
618         File resourceFile = File.createTempFile("resources", ".xml", Path.getTempDirectory());
619 
620         ActivationContent activationContent = new ActivationContent();
621         // add global properties true for this path/hierarchy
622         activationContent.addProperty(PARENT_PATH, this.parent);
623         activationContent.addProperty(WORKSPACE_NAME, this.workspaceName);
624         activationContent.addProperty(REPOSITORY_NAME, this.repositoryName);
625         activationContent.addProperty(RESOURCE_MAPPING_FILE, resourceFile.getName());//"resources.xml");
626         activationContent.addProperty(ACTION, ACTIVATE);
627         activationContent.addProperty(CONTENT_FILTER_RULE, this.contentFilterRule.toString());
628         activationContent.addProperty(NODE_UUID, node.getUUID());
629         activationContent.addProperty(UTF8_STATUS, SystemProperty.getProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED));
630 
631 
632         Document document = new Document();
633         Element root = new Element(RESOURCE_MAPPING_ROOT_ELEMENT);
634         document.setRootElement(root);
635         // collect exact order of this node within its same nodeType siblings
636         addOrderingInfo(root, orderBefore);
637 
638         this.addResources(root, node.getWorkspace().getSession(), node, this.contentFilter, activationContent);
639         XMLOutputter outputter = new XMLOutputter();
640         outputter.output(document, new FileOutputStream(resourceFile));
641         // add resource file to the list
642         activationContent.addFile(resourceFile.getName(), resourceFile);
643 
644         // add deletion info
645         activationContent.addProperty(ItemType.DELETED_NODE_MIXIN, "" + node.hasMixin(ItemType.DELETED_NODE_MIXIN));
646 
647         return activationContent;
648     }
649 
650     /**
651      * Adds ordering information to the resource mapping file.
652      * @param root element of the resource file under which ordering info must be added
653      * @param orderBefore
654      */
655     protected void addOrderingInfo(Element root, List<String> orderBefore) {
656         //do not use magnolia Content class since these objects are only meant for a single use to read UUID
657         Element siblingRoot = new Element(SIBLINGS_ROOT_ELEMENT);
658         root.addContent(siblingRoot);
659         if (orderBefore == null) {
660             return;
661         }
662         Iterator<String> siblings = orderBefore.iterator();
663         while (siblings.hasNext()) {
664             String uuid = siblings.next();
665             Element e = new Element(SIBLINGS_ELEMENT);
666             e.setAttribute(SIBLING_UUID, uuid);
667             siblingRoot.addContent(e);
668         }
669     }
670 
671     protected void addResources(Element resourceElement, Session session, final Content content, Content.ContentFilter filter, ActivationContent activationContent) throws IOException, RepositoryException, SAXException, Exception {
672         final String workspaceName = content.getWorkspace().getName();
673         log.debug("Preparing content {}:{} for publishing.", new String[] {workspaceName, content.getHandle()});
674         final String uuid = content.getUUID();
675 
676         File file = File.createTempFile("exchange_" + uuid, ".xml.gz", Path.getTempDirectory());
677         GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(file));
678 
679         // TODO: remove the second check. It should not be necessary. The only safe way to identify the versioned node is by looking at its type since the type is mandated by spec. and the frozen nodes is what the filter below removes anyway
680         if (content.isNodeType("nt:frozenNode") || workspaceName.equals(ContentRepository.VERSION_STORE)) {
681             XMLReader elementfilter = new FrozenElementFilter(XMLReaderFactory
682                     .createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName()));
683             ((FrozenElementFilter) elementfilter).setNodeName(content.getName());
684             /**
685              * nt:file node type has mandatory sub nodes
686              */
687             boolean noRecurse = !content.isNodeType(ItemType.NT_FILE);
688             exportAndParse(session, content, elementfilter, gzipOutputStream, noRecurse);
689         } else {
690             /**
691              * nt:file node type has mandatory sub nodes
692              */
693             if (content.isNodeType(ItemType.NT_FILE)) {
694                 session.exportSystemView(content.getJCRNode().getPath(), gzipOutputStream, false, false);
695             } else {
696                 session.exportSystemView(content.getJCRNode().getPath(), gzipOutputStream, false, true);
697             }
698         }
699 
700         IOUtils.closeQuietly(gzipOutputStream);
701         // add file entry in mapping.xml
702         Element element = new Element(RESOURCE_MAPPING_FILE_ELEMENT);
703         element.setAttribute(RESOURCE_MAPPING_NAME_ATTRIBUTE, content.getName());
704         element.setAttribute(RESOURCE_MAPPING_UUID_ATTRIBUTE, uuid);
705         element.setAttribute(RESOURCE_MAPPING_ID_ATTRIBUTE, file.getName());
706         resourceElement.addContent(element);
707         // add this file element as resource in activation content
708         activationContent.addFile(file.getName(), file);
709 
710         Iterator<Content> children = content.getChildren(filter).iterator();
711         while (children.hasNext()) {
712             Content child = children.next();
713             this.addResources(element, session, child, filter, activationContent);
714         }
715     }
716 
717     protected void exportAndParse(Session session, Content content, XMLReader elementfilter, OutputStream os, boolean noRecurse) throws Exception {
718         File tempFile = File.createTempFile("Frozen_"+content.getName(), ".xml"); //$NON-NLS-1$ //$NON-NLS-2$
719         OutputStream tmpFileOutStream = null;
720         FileInputStream tmpFileInStream = null;
721         try {
722             tmpFileOutStream = new FileOutputStream(tempFile);
723             // has to get path via JCR node since if "content" is of type ContentVersion, getHandle() call would have returned path to the base
724             session.exportSystemView(content.getJCRNode().getPath(), tmpFileOutStream, false, noRecurse);
725             tmpFileOutStream.flush();
726             tmpFileOutStream.close();
727 
728             OutputFormat outputFormat = new OutputFormat();
729             outputFormat.setPreserveSpace(false);
730 
731             tmpFileInStream = new FileInputStream(tempFile);
732             elementfilter.setContentHandler(new XMLSerializer(os, outputFormat));
733             elementfilter.parse(new InputSource(tmpFileInStream));
734             tmpFileInStream.close();
735         } catch (Throwable t) {
736             log.error("Failed to parse XML using FrozenElementFilter",t);
737             throw new Exception(t);
738         } finally {
739             IOUtils.closeQuietly(tmpFileInStream);
740             IOUtils.closeQuietly(tmpFileOutStream);
741             tempFile.delete();
742         }
743     }
744 
745     /**
746      * Gets target path to which the current path is mapped in given subscription. Provided path should be without trailing slash.
747      */
748     protected String getMappedPath(String path, Subscription subscription) {
749         String toURI = subscription.getToURI();
750         if (null != toURI) {
751             String fromURI = subscription.getFromURI();
752             // remove trailing slash if any
753             fromURI = StringUtils.removeEnd(fromURI, "/");
754             toURI = StringUtils.removeEnd(toURI, "/");
755             // apply path transformation if any
756             path = path.replaceFirst(fromURI, toURI);
757             if (path.equals("")) {
758                 path = "/";
759             }
760         }
761         return path;
762     }
763 
764     protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
765 
766         //String handle = getActivationURL(subscriber);
767 
768         try {
769             String authMethod = subscriber.getAuthenticationMethod();
770             // authentication headers
771             if (authMethod != null && "form".equalsIgnoreCase(authMethod)) {
772                 urlString += (urlString.indexOf('?') > 0 ? "&" : "?") + AUTH_USER + "=" + this.user.getName();
773                 urlString += "&" + AUTH_CREDENTIALS + "=" + this.user.getPassword();
774             }
775             URL url = new URL(urlString);
776             URLConnection urlConnection = url.openConnection();
777             urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
778             urlConnection.setReadTimeout(subscriber.getReadTimeout());
779             // authentication headers
780             if (authMethod == null || "basic".equalsIgnoreCase(authMethod)) {
781                 urlConnection.setRequestProperty(AUTHORIZATION, this.basicCredentials);
782             } else if (!"form".equalsIgnoreCase(subscriber.getAuthenticationMethod())) {
783                 log.info("Unknown Authentication method for deactivation: " + subscriber.getAuthenticationMethod());
784             }
785 
786             return urlConnection;
787         } catch (MalformedURLException e) {
788             throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + SecurityUtil.stripPasswordFromUrl(urlString) + "]");
789         } catch (IOException e) {
790             throw new ExchangeException("Not able to send the activation request [" + SecurityUtil.stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
791         } catch (Exception e) {
792             throw new ExchangeException(e);
793         }
794     }
795 
796 
797 }