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