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.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   * Default implementation of {@link Syndicator}. Activates all the content to a subscriber configured on the server.
93   * @author Sameer Charles
94   * $Id: $
95   */
96  public abstract class BaseSyndicatorImpl implements Syndicator {
97      private static final Logger log = LoggerFactory.getLogger(BaseSyndicatorImpl.class);
98  
99      /**
100      * URI used for activation.
101      */
102     public static final String DEFAULT_HANDLER = ".magnolia/activation"; //$NON-NLS-1$
103 
104     public static final String PARENT_PATH = "mgnlExchangeParentPath";
105 
106     public static final String MAPPED_PARENT_PATH = "mgnlExchangeMappedParent";
107 
108     /**
109      * Path to be activated or deactivated.
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      * Name of the resource containing reading sequence for importing the data in activation target.
123      */
124     public static final String RESOURCE_MAPPING_FILE = "mgnlExchangeResourceMappingFile";
125 
126     public static final String UTF8_STATUS = "mgnlUTF8Status";
127 
128     /**
129      * Name of the element in the resource file describing siblings of activated node.
130      * Siblings element will contain all siblings of the same node type which are "before"
131      * this node.
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"; //$NON-NLS-1$
152 
153     public static final String DEACTIVATE = "deactivate"; //$NON-NLS-1$
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"; //$NON-NLS-1$
168 
169     public static final String ACTIVATION_FAILED = "sa_failed"; //$NON-NLS-1$
170 
171     public static final String ACTIVATION_ATTRIBUTE_STATUS = "sa_attribute_status"; //$NON-NLS-1$
172 
173     public static final String ACTIVATION_ATTRIBUTE_MESSAGE = "sa_attribute_message"; //$NON-NLS-1$
174 
175     public static final String ACTIVATION_ATTRIBUTE_VERSION = "sa_attribute_version"; //$NON-NLS-1$
176 
177     /**
178      * Runs a given job in the thread pool.
179      *
180      * @param job the job to run
181      * @throws ExchangeException if the job could not be put in the pool
182      */
183     protected static void executeInPool(Runnable job) throws ExchangeException {
184         try {
185             ThreadPool.getInstance().execute(job);
186         } catch (InterruptedException e) {
187             // this is kind of a problem, we could not add the job to the pool
188             // retrying might or might not work now that the interruption
189             // status is cleared but there is not much we can do so throwing
190             // an ExchangeException seems like the least bad choice
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      * Acquires a {@link Sync} ignoring any interruptions. Should any
199      * interruption occur the interruption status will be set. Might
200      * potentially block/wait forever.
201      *
202      * @see Sync#acquire()
203      *
204      * @param latch the latch on which to wait
205      */
206     protected static void acquireIgnoringInterruption(Sync latch) {
207         try {
208             latch.acquire();
209         } catch (InterruptedException e) {
210             // waken up externally - ignore try again
211             acquireIgnoringInterruption(latch);
212             // be a good citizen and set back the interruption status
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      * @param user
233      * @param repositoryName repository ID
234      * @param workspaceName workspace ID
235      * @param rule content filter rule
236      * @see info.magnolia.cms.exchange.Syndicator#init(info.magnolia.cms.security.User, String, String,
237      * info.magnolia.cms.util.Rule)
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      * This will activate specifies page (sub pages) to all configured subscribers.
251      *
252      * @param parent parent under which this page will be activated
253      * @param content to be activated
254      * @throws javax.jcr.RepositoryException
255      * @throws info.magnolia.cms.exchange.ExchangeException
256      */
257     public void activate(String parent, Content content) throws ExchangeException, RepositoryException {
258         this.activate(parent, content, null);
259     }
260 
261     /**
262      * This will activate specified node to all configured subscribers.
263      *
264      * @param parent parent under which this page will be activated
265      * @param content to be activated
266      * @param orderBefore List of UUID to be used by the implementation to order this node after activation
267      * @throws javax.jcr.RepositoryException
268      * @throws info.magnolia.cms.exchange.ExchangeException
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      * This will activate specifies page (sub pages) to the specified subscriber.
277      *
278      * @param subscriber
279      * @param parent parent under which this page will be activated
280      * @param content to be activated
281      * @throws javax.jcr.RepositoryException
282      * @throws info.magnolia.cms.exchange.ExchangeException
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      * This will activate specifies node to the specified subscriber.
290      *
291      * @param subscriber
292      * @param parent      parent under which this page will be activated
293      * @param content     to be activated
294      * @param orderBefore List of UUID to be used by the subscriber to order this node after activation
295      * @throws javax.jcr.RepositoryException
296      * @throws info.magnolia.cms.exchange.ExchangeException
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 
302         ActivationContent activationContent = null;
303         try {
304             activationContent = this.collect(content, orderBefore);
305             if (null == subscriber) {
306                 this.activate(activationContent, path);
307             } else {
308                 this.activate(subscriber, activationContent, path);
309             }
310             if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
311                 final HierarchyManager hm = content.getHierarchyManager();
312                 String uuid = content.getUUID();
313                 if (StringUtils.isNotBlank(uuid)) {
314                     if (content instanceof ContentVersion) {
315                         // replace versioned content with the real node
316                         content = hm.getContentByUUID(uuid);
317                     }
318                     Content parentContent = content.getParent();
319                     content.delete();
320                     parentContent.save();
321                 } else {
322                     log.warn("Content {}:{} was already removed.", new String[] {hm.getName(), path});
323                 }
324             } else {
325                 this.updateActivationDetails(path);
326             }
327             log.info("Exchange: activation succeeded [{}]", path);
328         } catch (Exception e) {
329             if (log.isDebugEnabled()) {
330                 log.error("Exchange: activation failed for path:" + ((path != null) ? path : "[null]"), e);
331                 long timestamp = System.currentTimeMillis();
332                 log.warn("moving files from failed activation to *.failed" + timestamp );
333                 Iterator<File> keys = activationContent.getFiles().values().iterator();
334                 while (keys.hasNext()) {
335                     File f = keys.next();
336                     f.renameTo(new File(f.getAbsolutePath()+".failed" + timestamp));
337                 }
338                 activationContent.getFiles().clear();
339 
340             }
341             throw new ExchangeException(e);
342         } finally {
343             log.debug("Cleaning temporary files");
344             cleanTemporaryStore(activationContent);
345         }
346     }
347 
348     /**
349      * @throws ExchangeException
350      */
351     public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
352 
353 
354     /**
355      * 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.
356      */
357     public String activate(Subscriber subscriber, ActivationContent activationContent, String nodePath) throws ExchangeException {
358         // 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) )
359         log.debug("activate");
360         if (null == subscriber) {
361             throw new ExchangeException("Null Subscriber");
362         }
363 
364         String parentPath = null;
365 
366         // concurrency: from path and repo name are same for all subscribers
367         Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
368         if (null != subscription) {
369             // its subscribed since we found the matching subscription
370             parentPath = this.getMappedPath(this.parent, subscription);
371             activationContent.setProperty(PARENT_PATH, parentPath);
372         } else {
373             log.debug("Exchange : subscriber [{}] is not subscribed to {}", subscriber.getName(), nodePath);
374             return "not subscribed";
375         }
376         log.debug("Exchange : sending activation request to {} with user {}", subscriber.getName(), this.user.getName());
377 
378         URLConnection urlConnection = null;
379         String versionName = null;
380         try {
381             urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
382             this.addActivationHeaders(urlConnection, activationContent);
383 
384             Transporter.transport((HttpURLConnection) urlConnection, activationContent);
385 
386             String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
387             versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
388 
389             // check if the activation failed
390             if (StringUtils.equals(status, ACTIVATION_FAILED)) {
391                 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
392                 throw new ExchangeException("Message received from subscriber: " + message);
393             }
394             urlConnection.getContent();
395             log.debug("Exchange : activation request sent to {}", subscriber.getName());
396         }
397         catch (ExchangeException e) {
398             throw e;
399         }
400         catch (IOException e) {
401             log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
402             String url = (urlConnection == null ? null : urlConnection.getURL().toString());
403             url = stripPasswordFromUrl(url);
404             // hide pwd if present
405             throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
406         }
407         catch (Exception e) {
408             throw new ExchangeException(e);
409         }
410         return versionName;
411     }
412 
413     protected static String stripPasswordFromUrl(String escapedUrl) {
414         if (escapedUrl != null) {
415             int idx = escapedUrl.indexOf("mgnlUserPSWD");
416             if (idx > 0) {
417                 int endIdx = escapedUrl.indexOf("&", idx);
418                 if (endIdx > 0) {
419                     escapedUrl = escapedUrl.substring(0, idx) + escapedUrl.substring(endIdx + 1);
420                 } else {
421                     escapedUrl = escapedUrl.substring(0, idx - 1);
422                 }
423             }
424         }
425         return escapedUrl;
426     }
427 
428 
429     /**
430      * Cleans up temporary file store after activation.
431      */
432     protected void cleanTemporaryStore(ActivationContent activationContent) {
433         if (activationContent == null) {
434             log.debug("Clean temporary store - nothing to do");
435             return;
436         }
437         if (log.isDebugEnabled()) {
438             log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
439             return;
440         }
441 
442         Iterator<String> keys = activationContent.getFiles().keySet().iterator();
443         while (keys.hasNext()) {
444             String key = keys.next();
445             log.debug("Removing temporary file {}", key);
446             activationContent.getFile(key).delete();
447         }
448     }
449 
450     public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
451         final Content node = getHierarchyManager().getContent(path);
452         deactivate(node);
453     }
454 
455     /**
456      * @param node to deactivate
457      * @throws RepositoryException
458      * @throws ExchangeException
459      */
460     public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
461         String nodeUUID = node.getUUID();
462         String path = node.getHandle();
463         this.doDeactivate(nodeUUID, path);
464         updateDeactivationDetails(nodeUUID);
465     }
466 
467     /**
468      * @param node , to deactivate
469      * @param subscriber
470      * @throws RepositoryException
471      * @throws ExchangeException
472      */
473     public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
474         String nodeUUID = node.getUUID();
475         String path = node.getHandle();
476         this.doDeactivate(subscriber, nodeUUID, path);
477         updateDeactivationDetails(nodeUUID);
478     }
479 
480     /**
481      * @throws ExchangeException
482      */
483     public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
484 
485     /**
486      * Deactivate content from specified subscriber.
487      * @param subscriber
488      * @throws ExchangeException
489      */
490     public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
491 
492     /**
493      * Return URI set for deactivation.
494      * @param subscriberInfo
495      */
496     protected String getDeactivationURL(Subscriber subscriberInfo) {
497         return getActivationURL(subscriberInfo);
498     }
499 
500     /**
501      * Adds header fields describing deactivation request.
502      * @param connection
503      */
504     protected void addDeactivationHeaders(URLConnection connection, String nodeUUID) {
505         connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
506         connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
507         if (nodeUUID != null) {
508             connection.addRequestProperty(NODE_UUID, nodeUUID);
509         }
510         connection.addRequestProperty(ACTION, DEACTIVATE);
511     }
512 
513     /**
514      * Retrieves URL subscriber is listening on for (de)activation requests.
515      */
516     protected String getActivationURL(Subscriber subscriberInfo) {
517         final String url = subscriberInfo.getURL();
518         if (!url.endsWith("/")) {
519             return url + "/" + DEFAULT_HANDLER;
520         }
521         return url + DEFAULT_HANDLER;
522     }
523 
524     /**
525      * Adds headers fields describing activation request.
526      */
527     protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent) {
528         Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
529         while (headerKeys.hasNext()) {
530             String key = headerKeys.next();
531             String value = activationContent.getproperty(key);
532             if(SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
533                 try {
534                     value = URLEncoder.encode(value, "UTF-8");
535                 }
536                 catch (UnsupportedEncodingException e) {
537                     // do nothing
538                 }
539             }
540             connection.setRequestProperty(key, value);
541         }
542     }
543 
544     /**
545      * Updates current content activation meta data with the time stamp and user details of the activation.
546      */
547     protected void updateActivationDetails(String path) throws RepositoryException {
548         // page activated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
549         Content page = getSystemHierarchyManager().getContent(path);
550         updateMetaData(page, ACTIVATE);
551         page.save();
552         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path );
553     }
554 
555     /**
556      * Updates current content activation meta data with the timestamp and user details of the deactivation.
557      */
558     protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
559         // page deactivated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
560         Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
561         updateMetaData(page, DEACTIVATE);
562         page.save();
563         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle() );
564     }
565 
566 
567     private HierarchyManager getHierarchyManager() {
568         return MgnlContext.getHierarchyManager(this.repositoryName, this.workspaceName);
569     }
570 
571     private HierarchyManager getSystemHierarchyManager() {
572         return MgnlContext.getSystemContext().getHierarchyManager(this.repositoryName, this.workspaceName);
573     }
574 
575     /**
576      * @param node
577      * @param type (activate / deactivate)
578      */
579     protected void updateMetaData(Content node, String type) throws AccessDeniedException {
580         // update the passed node
581         MetaData md = node.getMetaData();
582         if (type.equals(ACTIVATE)) {
583             md.setActivated();
584         }
585         else {
586             md.setUnActivated();
587         }
588         md.setActivatorId(this.user.getName());
589         md.setLastActivationActionDate();
590 
591         Iterator<Content> children;
592         if (type.equals(ACTIVATE)) {
593             // use syndicator rule based filter
594             children = node.getChildren(this.contentFilter).iterator();
595         }
596         else {
597             // all children
598             children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
599         }
600 
601         while (children.hasNext()) {
602             Content child = children.next();
603             this.updateMetaData(child, type);
604         }
605 
606 
607     }
608 
609     /**
610      * Collects all information about activated content and its children (those that are set to be activated with the parent by filter rules).
611      * @throws Exception
612      */
613     protected ActivationContent collect(Content node, List<String> orderBefore) throws Exception {
614         // make sure resource file is unique
615         File resourceFile = File.createTempFile("resources", ".xml", Path.getTempDirectory());
616 
617         ActivationContent activationContent = new ActivationContent();
618         // add global properties true for this path/hierarchy
619         activationContent.addProperty(PARENT_PATH, this.parent);
620         activationContent.addProperty(WORKSPACE_NAME, this.workspaceName);
621         activationContent.addProperty(REPOSITORY_NAME, this.repositoryName);
622         activationContent.addProperty(RESOURCE_MAPPING_FILE, resourceFile.getName());//"resources.xml");
623         activationContent.addProperty(ACTION, ACTIVATE);
624         activationContent.addProperty(CONTENT_FILTER_RULE, this.contentFilterRule.toString());
625         activationContent.addProperty(NODE_UUID, node.getUUID());
626         activationContent.addProperty(UTF8_STATUS, SystemProperty.getProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED));
627 
628 
629         Document document = new Document();
630         Element root = new Element(RESOURCE_MAPPING_ROOT_ELEMENT);
631         document.setRootElement(root);
632         // collect exact order of this node within its same nodeType siblings
633         addOrderingInfo(root, orderBefore);
634 
635         this.addResources(root, node.getWorkspace().getSession(), node, this.contentFilter, activationContent);
636         XMLOutputter outputter = new XMLOutputter();
637         outputter.output(document, new FileOutputStream(resourceFile));
638         // add resource file to the list
639         activationContent.addFile(resourceFile.getName(), resourceFile);
640 
641         // add deletion info
642         activationContent.addProperty(ItemType.DELETED_NODE_MIXIN, "" + node.hasMixin(ItemType.DELETED_NODE_MIXIN));
643 
644         return activationContent;
645     }
646 
647     /**
648      * Adds ordering information to the resource mapping file.
649      * @param root element of the resource file under which ordering info must be added
650      * @param orderBefore
651      */
652     protected void addOrderingInfo(Element root, List<String> orderBefore) {
653         //do not use magnolia Content class since these objects are only meant for a single use to read UUID
654         Element siblingRoot = new Element(SIBLINGS_ROOT_ELEMENT);
655         root.addContent(siblingRoot);
656         if (orderBefore == null) {
657             return;
658         }
659         Iterator<String> siblings = orderBefore.iterator();
660         while (siblings.hasNext()) {
661             String uuid = siblings.next();
662             Element e = new Element(SIBLINGS_ELEMENT);
663             e.setAttribute(SIBLING_UUID, uuid);
664             siblingRoot.addContent(e);
665         }
666     }
667 
668     protected void addResources(Element resourceElement, Session session, final Content content, Content.ContentFilter filter, ActivationContent activationContent) throws IOException, RepositoryException, SAXException, Exception {
669         final String workspaceName = content.getWorkspace().getName();
670         log.debug("Preparing content {}:{} for publishing.", new String[] {workspaceName, content.getHandle()});
671         final String uuid = content.getUUID();
672 
673         File file = File.createTempFile("exchange_" + uuid, ".xml.gz", Path.getTempDirectory());
674         GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(file));
675 
676         // 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
677         if (content.isNodeType("nt:frozenNode") || workspaceName.equals(ContentRepository.VERSION_STORE)) {
678             XMLReader elementfilter = new FrozenElementFilter(XMLReaderFactory
679                     .createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName()));
680             ((FrozenElementFilter) elementfilter).setNodeName(content.getName());
681             /**
682              * nt:file node type has mandatory sub nodes
683              */
684             boolean noRecurse = !content.isNodeType(ItemType.NT_FILE);
685             exportAndParse(session, content, elementfilter, gzipOutputStream, noRecurse);
686         } else {
687             /**
688              * nt:file node type has mandatory sub nodes
689              */
690             if (content.isNodeType(ItemType.NT_FILE)) {
691                 session.exportSystemView(content.getJCRNode().getPath(), gzipOutputStream, false, false);
692             } else {
693                 session.exportSystemView(content.getJCRNode().getPath(), gzipOutputStream, false, true);
694             }
695         }
696 
697         IOUtils.closeQuietly(gzipOutputStream);
698         // add file entry in mapping.xml
699         Element element = new Element(RESOURCE_MAPPING_FILE_ELEMENT);
700         element.setAttribute(RESOURCE_MAPPING_NAME_ATTRIBUTE, content.getName());
701         element.setAttribute(RESOURCE_MAPPING_UUID_ATTRIBUTE, uuid);
702         element.setAttribute(RESOURCE_MAPPING_ID_ATTRIBUTE, file.getName());
703         resourceElement.addContent(element);
704         // add this file element as resource in activation content
705         activationContent.addFile(file.getName(), file);
706 
707         Iterator<Content> children = content.getChildren(filter).iterator();
708         while (children.hasNext()) {
709             Content child = children.next();
710             this.addResources(element, session, child, filter, activationContent);
711         }
712     }
713 
714     protected void exportAndParse(Session session, Content content, XMLReader elementfilter, OutputStream os, boolean noRecurse) throws Exception {
715         File tempFile = File.createTempFile("Frozen_"+content.getName(), ".xml"); //$NON-NLS-1$ //$NON-NLS-2$
716         OutputStream tmpFileOutStream = null;
717         FileInputStream tmpFileInStream = null;
718         try {
719             tmpFileOutStream = new FileOutputStream(tempFile);
720             // has to get path via JCR node since if "content" is of type ContentVersion, getHandle() call would have returned path to the base
721             session.exportSystemView(content.getJCRNode().getPath(), tmpFileOutStream, false, noRecurse);
722             tmpFileOutStream.flush();
723             tmpFileOutStream.close();
724 
725             OutputFormat outputFormat = new OutputFormat();
726             outputFormat.setPreserveSpace(false);
727 
728             tmpFileInStream = new FileInputStream(tempFile);
729             elementfilter.setContentHandler(new XMLSerializer(os, outputFormat));
730             elementfilter.parse(new InputSource(tmpFileInStream));
731             tmpFileInStream.close();
732         } catch (Throwable t) {
733             log.error("Failed to parse XML using FrozenElementFilter",t);
734             throw new Exception(t);
735         } finally {
736             IOUtils.closeQuietly(tmpFileInStream);
737             IOUtils.closeQuietly(tmpFileOutStream);
738             tempFile.delete();
739         }
740     }
741 
742     /**
743      * Gets target path to which the current path is mapped in given subscription. Provided path should be without trailing slash.
744      */
745     protected String getMappedPath(String path, Subscription subscription) {
746         String toURI = subscription.getToURI();
747         if (null != toURI) {
748             String fromURI = subscription.getFromURI();
749             // remove trailing slash if any
750             fromURI = StringUtils.removeEnd(fromURI, "/");
751             toURI = StringUtils.removeEnd(toURI, "/");
752             // apply path transformation if any
753             path = path.replaceFirst(fromURI, toURI);
754             if (path.equals("")) {
755                 path = "/";
756             }
757         }
758         return path;
759     }
760 
761     protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
762 
763         //String handle = getActivationURL(subscriber);
764 
765         try {
766             String authMethod = subscriber.getAuthenticationMethod();
767             // authentication headers
768             if (authMethod != null && "form".equalsIgnoreCase(authMethod)) {
769                 urlString += (urlString.indexOf('?') > 0 ? "&" : "?") + AUTH_USER + "=" + this.user.getName();
770                 urlString += "&" + AUTH_CREDENTIALS + "=" + this.user.getPassword();
771             }
772             URL url = new URL(urlString);
773             URLConnection urlConnection = url.openConnection();
774             urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
775             urlConnection.setReadTimeout(subscriber.getReadTimeout());
776             // authentication headers
777             if (authMethod == null || "basic".equalsIgnoreCase(authMethod)) {
778                 urlConnection.setRequestProperty(AUTHORIZATION, this.basicCredentials);
779             } else if (!"form".equalsIgnoreCase(subscriber.getAuthenticationMethod())) {
780                 log.info("Unknown Authentication method for deactivation: " + subscriber.getAuthenticationMethod());
781             }
782 
783             return urlConnection;
784         } catch (MalformedURLException e) {
785             throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + stripPasswordFromUrl(urlString) + "]");
786         } catch (IOException e) {
787             throw new ExchangeException("Not able to send the activation request [" + stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
788         } catch (Exception e) {
789             throw new ExchangeException(e);
790         }
791     }
792 
793 
794 }