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.SystemProperty;
41  import info.magnolia.cms.core.version.ContentVersion;
42  import info.magnolia.cms.exchange.ExchangeException;
43  import info.magnolia.cms.exchange.Subscriber;
44  import info.magnolia.cms.exchange.Subscription;
45  import info.magnolia.cms.exchange.Syndicator;
46  import info.magnolia.cms.security.AccessDeniedException;
47  import info.magnolia.cms.security.SecurityUtil;
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.init.MagnoliaConfigurationProperties;
54  import info.magnolia.logging.AuditLoggingUtil;
55  
56  import java.io.File;
57  import java.io.IOException;
58  import java.io.UnsupportedEncodingException;
59  import java.net.HttpURLConnection;
60  import java.net.MalformedURLException;
61  import java.net.URL;
62  import java.net.URLConnection;
63  import java.net.URLEncoder;
64  import java.security.MessageDigest;
65  import java.security.NoSuchAlgorithmException;
66  import java.util.Calendar;
67  import java.util.Iterator;
68  import java.util.List;
69  
70  import javax.jcr.RepositoryException;
71  import javax.jcr.Session;
72  
73  import org.apache.commons.lang.StringUtils;
74  import org.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  
77  import EDU.oswego.cs.dl.util.concurrent.Sync;
78  
79  import com.google.inject.Inject;
80  
81  /**
82   * Default implementation of {@link Syndicator}. Activates all the content to a subscriber configured on the server.
83   * @author Sameer Charles
84   * $Id: $
85   */
86  public abstract class BaseSyndicatorImpl implements Syndicator {
87      private static final Logger log = LoggerFactory.getLogger(BaseSyndicatorImpl.class);
88  
89      private final MessageDigest md;
90  
91      /**
92       * URI used for activation.
93       */
94      public static final String DEFAULT_HANDLER = ".magnolia/activation";
95  
96      public static final String PARENT_PATH = "mgnlExchangeParentPath";
97  
98      public static final String MAPPED_PARENT_PATH = "mgnlExchangeMappedParent";
99  
100     /**
101      * Path to be activated or deactivated.
102      */
103     public static final String PATH = "mgnlExchangePath";
104 
105     public static final String NODE_UUID = "mgnlExchangeNodeUUID";
106 
107     /**
108      * @deprecated since 4.5 - use logical workspace instead.
109      */
110     @Deprecated
111     public static final String REPOSITORY_NAME = "mgnlExchangeRepositoryName";
112 
113     public static final String WORKSPACE_NAME = "mgnlExchangeWorkspaceName";
114 
115     public static final String VERSION_NAME = "mgnlExchangeVersionName";
116 
117     /**
118      * Name of the resource containing reading sequence for importing the data in activation target.
119      */
120     public static final String RESOURCE_MAPPING_FILE = "mgnlExchangeResourceMappingFile";
121 
122     public static final String UTF8_STATUS = "mgnlUTF8Status";
123 
124     /**
125      * Name of the element in the resource file describing siblings of activated node.
126      * Siblings element will contain all siblings of the same node type which are "before"
127      * this node.
128      */
129     public static final String SIBLINGS_ROOT_ELEMENT = "NodeSiblings";
130 
131     public static final String SIBLINGS_ELEMENT = "sibling";
132 
133     public static final String SIBLING_UUID = "siblingUUID";
134 
135     public static final String RESOURCE_MAPPING_FILE_ELEMENT = "File";
136 
137     public static final String RESOURCE_MAPPING_NAME_ATTRIBUTE = "name";
138 
139     public static final String RESOURCE_MAPPING_UUID_ATTRIBUTE = "contentUUID";
140 
141     public static final String RESOURCE_MAPPING_ID_ATTRIBUTE = "resourceId";
142 
143     public static final String RESOURCE_MAPPING_MD_ATTRIBUTE = "resourceMD";
144 
145     public static final String RESOURCE_MAPPING_ROOT_ELEMENT = "Resources";
146 
147     public static final String ACTION = "mgnlExchangeAction";
148 
149     public static final String ACTIVATE = "activate";
150 
151     public static final String DEACTIVATE = "deactivate";
152 
153     public static final String COMMIT = "commit";
154 
155     public static final String ROLLBACK = "rollback";
156 
157     public static final String CONTENT_FILTER_RULE = "mgnlExchangeFilterRule";
158 
159     public static final String ACTIVATION_SUCCESSFUL = "sa_success";
160 
161     public static final String ACTIVATION_HANDSHAKE = "sa_handshake";
162 
163     public static final String ACTIVATION_FAILED = "sa_failed";
164 
165     public static final String ACTIVATION_ATTRIBUTE_STATUS = "sa_attribute_status";
166 
167     public static final String ACTIVATION_ATTRIBUTE_MESSAGE = "sa_attribute_message";
168 
169     public static final String ACTIVATION_ATTRIBUTE_VERSION = "sa_attribute_version";
170 
171     public static final String ACTIVATION_AUTH = "X-magnolia-act-auth";
172     public static final String ACTIVATION_AUTH_KEY = "X-magnolia-act-auth-init";
173 
174     public BaseSyndicatorImpl() {
175         try {
176             md = MessageDigest.getInstance("MD5");
177         } catch (NoSuchAlgorithmException e) {
178             throw new SecurityException("In order to proceed with activation please run Magnolia CMS using Java version with MD5 support.", e);
179         }
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     @Deprecated
225     protected String repositoryName;
226 
227     protected String workspaceName;
228 
229     protected String parent;
230 
231     protected Rule contentFilterRule;
232 
233     protected User user;
234 
235     private Calendar contentVersionDate;
236 
237     private MagnoliaConfigurationProperties properties;
238 
239     private ResourceCollector resourceCollector;
240 
241     /**
242      * @param user
243      * @param repositoryName repository ID
244      * @param workspaceName workspace ID
245      * @param rule content filter rule
246      * @see info.magnolia.cms.exchange.Syndicator#init(info.magnolia.cms.security.User, String, String,
247      * info.magnolia.cms.util.Rule)
248      */
249     @Override
250     public void init(User user, String repositoryName, String workspaceName, Rule rule) {
251         this.user = user;
252         this.contentFilterRule = rule;
253         this.repositoryName = repositoryName;
254         this.workspaceName = workspaceName;
255     }
256 
257     /**
258      * This will activate specifies page (sub pages) to all configured subscribers.
259      *
260      * @param parent parent under which this page will be activated
261      * @param content to be activated
262      * @throws javax.jcr.RepositoryException
263      * @throws info.magnolia.cms.exchange.ExchangeException
264      */
265     @Override
266     public void activate(String parent, Content content) throws ExchangeException, RepositoryException {
267         this.activate(parent, content, null);
268     }
269 
270     /**
271      * This will activate specified node to all configured subscribers.
272      *
273      * @param parent parent under which this page will be activated
274      * @param content to be activated
275      * @param orderBefore List of UUID to be used by the implementation to order this node after activation
276      * @throws javax.jcr.RepositoryException
277      * @throws info.magnolia.cms.exchange.ExchangeException
278      *
279      */
280     @Override
281     public void activate(String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
282         this.activate(null, parent, content, orderBefore);
283     }
284 
285     /**
286      * This will activate specifies page (sub pages) to the specified subscriber.
287      *
288      * @param subscriber
289      * @param parent parent under which this page will be activated
290      * @param content to be activated
291      * @throws javax.jcr.RepositoryException
292      * @throws info.magnolia.cms.exchange.ExchangeException
293      */
294     @Override
295     public void activate(Subscriber subscriber, String parent, Content content) throws ExchangeException, RepositoryException {
296         this.activate(subscriber, parent, content, null);
297     }
298 
299     /**
300      * This will activate specifies node to the specified subscriber.
301      *
302      * @param subscriber
303      * @param parent      parent under which this page will be activated
304      * @param content     to be activated
305      * @param orderBefore List of UUID to be used by the subscriber to order this node after activation
306      * @throws javax.jcr.RepositoryException
307      * @throws info.magnolia.cms.exchange.ExchangeException
308      */
309     @Override
310     public void activate(Subscriber subscriber, String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
311         this.parent = parent;
312         String path = content.getHandle();
313 
314         if (content instanceof ContentVersion) {
315             contentVersionDate = ((ContentVersion)content).getCreated();
316         }
317 
318         ActivationContent activationContent = null;
319         try {
320             activationContent = resourceCollector.collect(content, orderBefore, parent, workspaceName, repositoryName, contentFilterRule);
321             if (null == subscriber) {
322                 this.activate(activationContent, path);
323             } else {
324                 this.activate(subscriber, activationContent, path);
325             }
326             if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
327                 final HierarchyManager hm = content.getHierarchyManager();
328                 final Session session = content.getJCRNode().getSession();
329                 String uuid = content.getUUID();
330                 if (StringUtils.isNotBlank(uuid)) {
331                     if (content instanceof ContentVersion) {
332                         // replace versioned content with the real node
333                         content = hm.getContentByUUID(uuid);
334                     }
335                     Content parentContent = content.getParent();
336                     content.delete();
337                     parentContent.save();
338                 } else {
339                     log.warn("Content {}:{} was already removed.", new String[] {content.getWorkspace().getName(), path});
340                 }
341             } else {
342                 this.updateActivationDetails(path);
343             }
344             log.info("Exchange: activation succeeded [{}]", path);
345         } catch (Exception e) {
346             if (log.isDebugEnabled()) {
347                 log.error("Exchange: activation failed for path:" + ((path != null) ? path : "[null]"), e);
348                 long timestamp = System.currentTimeMillis();
349                 log.warn("moving files from failed activation to *.failed" + timestamp );
350                 Iterator<File> keys = activationContent.getFiles().values().iterator();
351                 while (keys.hasNext()) {
352                     File f = keys.next();
353                     f.renameTo(new File(f.getAbsolutePath()+".failed" + timestamp));
354                 }
355                 activationContent.getFiles().clear();
356 
357             }
358             throw new ExchangeException(e);
359         } finally {
360             log.debug("Cleaning temporary files");
361             cleanTemporaryStore(activationContent);
362         }
363     }
364 
365     /**
366      * @throws ExchangeException
367      */
368     public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
369 
370 
371     /**
372      * 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.
373      */
374     public String activate(Subscriber subscriber, ActivationContent activationContent, String nodePath) throws ExchangeException {
375         // 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) )
376         log.debug("activate");
377         if (null == subscriber) {
378             throw new ExchangeException("Null Subscriber");
379         }
380 
381         String parentPath = null;
382 
383         // concurrency: from path and repo name are same for all subscribers
384         Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
385         if (null != subscription) {
386             // its subscribed since we found the matching subscription
387             parentPath = this.getMappedPath(this.parent, subscription);
388             activationContent.setProperty(PARENT_PATH, parentPath);
389         } else {
390             log.debug("Exchange : subscriber [{}] is not subscribed to {}", subscriber.getName(), nodePath);
391             return "not subscribed";
392         }
393         log.debug("Exchange : sending activation request to {} with user {}", subscriber.getName(), this.user.getName());
394 
395         URLConnection urlConnection = null;
396         String versionName = null;
397         try {
398             urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
399             versionName = transportActivatedData(activationContent, urlConnection, null);
400 
401             String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
402 
403             if (StringUtils.equals(status, ACTIVATION_HANDSHAKE)) {
404                 String handshakeKey = urlConnection.getHeaderField(ACTIVATION_AUTH);
405                 // receive all pending data
406                 urlConnection.getContent();
407 
408                 // transport the data again
409                 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
410                 // and get the version & status again
411                 versionName = transportActivatedData(activationContent, urlConnection, handshakeKey);
412                 status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
413             }
414 
415             // check if the activation failed
416             if (StringUtils.equals(status, ACTIVATION_FAILED)) {
417                 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
418                 throw new ExchangeException("Message received from subscriber: " + message);
419             }
420             urlConnection.getContent();
421             log.debug("Exchange : activation request sent to {}", subscriber.getName());
422         }
423         catch (ExchangeException e) {
424             throw e;
425         }
426         catch (IOException e) {
427             log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
428             String url = (urlConnection == null ? null : urlConnection.getURL().toString());
429             url = SecurityUtil.stripPasswordFromUrl(url);
430             // hide pwd if present
431             throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
432         }
433         catch (Exception e) {
434             throw new ExchangeException(e);
435         }
436         return versionName;
437     }
438 
439     private String transportActivatedData(ActivationContent activationContent, URLConnection urlConnection, String handshakeKey) throws ExchangeException {
440         String versionName;
441         this.addActivationHeaders(urlConnection, activationContent, handshakeKey);
442 
443         Transporter.transport((HttpURLConnection) urlConnection, activationContent);
444 
445         versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
446         return versionName;
447     }
448 
449     /**
450      * Cleans up temporary file store after activation.
451      */
452     protected void cleanTemporaryStore(ActivationContent activationContent) {
453         if (activationContent == null) {
454             log.debug("Clean temporary store - nothing to do");
455             return;
456         }
457         if (log.isDebugEnabled()) {
458             log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
459             return;
460         }
461 
462         Iterator<String> keys = activationContent.getFiles().keySet().iterator();
463         while (keys.hasNext()) {
464             String key = keys.next();
465             log.debug("Removing temporary file {}", key);
466             activationContent.getFile(key).delete();
467         }
468     }
469 
470     public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
471         final Content node = getHierarchyManager().getContent(path);
472         deactivate(node);
473     }
474 
475     /**
476      * @param node to deactivate
477      * @throws RepositoryException
478      * @throws ExchangeException
479      */
480     @Override
481     public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
482         String nodeUUID = node.getUUID();
483         String path = node.getHandle();
484         this.doDeactivate(nodeUUID, path);
485         updateDeactivationDetails(nodeUUID);
486     }
487 
488     /**
489      * @param node , to deactivate
490      * @param subscriber
491      * @throws RepositoryException
492      * @throws ExchangeException
493      */
494     @Override
495     public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
496         String nodeUUID = node.getUUID();
497         String path = node.getHandle();
498         this.doDeactivate(subscriber, nodeUUID, path);
499         updateDeactivationDetails(nodeUUID);
500     }
501 
502     /**
503      * @throws ExchangeException
504      */
505     public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
506 
507     /**
508      * Deactivate content from specified subscriber.
509      * @param subscriber
510      * @throws ExchangeException
511      */
512     public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
513 
514     /**
515      * Return URI set for deactivation.
516      * @param subscriberInfo
517      */
518     protected String getDeactivationURL(Subscriber subscriberInfo) {
519         return getActivationURL(subscriberInfo);
520     }
521 
522     /**
523      * Adds header fields describing deactivation request.
524      * 
525      * @param connection
526      * @param handshakeKey
527      *            optional key to encrypt public key before sending it over
528      */
529     protected void addDeactivationHeaders(URLConnection connection, String nodeUUID, String handshakeKey) {
530         connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
531         connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
532         // TODO: how can this ever be null?? We don't send path along anywhere, so there's no way to delete anything w/o uuid, which means we pbly do not support deactivation of content w/o UUID!!!
533         String md5 = "";
534         if (nodeUUID != null) {
535             connection.addRequestProperty(NODE_UUID, nodeUUID);
536             // send md5 of uuid ... it would be silly to send clear text along the encrypted message
537             md5 = SecurityUtil.byteArrayToHex(md.digest(nodeUUID.getBytes()));
538         }
539         // send md5 of uuid ... it would be silly to send clear text along the encrypted message
540         String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
541 
542         // optional
543         addHandshakeInfo(connection, handshakeKey);
544 
545         connection.setRequestProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
546         connection.addRequestProperty(ACTION, DEACTIVATE);
547     }
548 
549     protected void addHandshakeInfo(URLConnection connection, String handshakeKey) {
550         if (handshakeKey != null) {
551             connection.setRequestProperty(ACTIVATION_AUTH_KEY, SecurityUtil.encrypt(SecurityUtil.getPublicKey(), handshakeKey));
552         }
553     }
554 
555     /**
556      * Retrieves URL subscriber is listening on for (de)activation requests.
557      */
558     protected String getActivationURL(Subscriber subscriberInfo) {
559         final String url = subscriberInfo.getURL();
560         if (!url.endsWith("/")) {
561             return url + "/" + DEFAULT_HANDLER;
562         }
563         return url + DEFAULT_HANDLER;
564     }
565 
566     /**
567      * Adds headers fields describing activation request.
568      * 
569      * @param handshakeKey
570      *            Optional key previously received from subscriber, used to encrypt activation public key for delivery to said subscriber. Or null when such key is not known or present.
571      */
572     protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent, String handshakeKey) {
573 
574         String md5 = activationContent.getproperty(RESOURCE_MAPPING_MD_ATTRIBUTE);
575         String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
576         activationContent.setProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
577         Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
578         while (headerKeys.hasNext()) {
579             String key = headerKeys.next();
580             if (RESOURCE_MAPPING_MD_ATTRIBUTE.equals(key)) {
581                 // do not send md5 in plain string
582                 continue;
583             }
584             String value = activationContent.getproperty(key);
585             if(SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
586                 try {
587                     value = URLEncoder.encode(value, "UTF-8");
588                 }
589                 catch (UnsupportedEncodingException e) {
590                     // do nothing
591                 }
592             }
593             connection.setRequestProperty(key, value);
594         }
595         addHandshakeInfo(connection, handshakeKey);
596     }
597 
598     /**
599      * Updates current content activation meta data with the time stamp and user details of the activation.
600      */
601     protected void updateActivationDetails(String path) throws RepositoryException {
602         // page activated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
603         Content page = getSystemHierarchyManager().getContent(path);
604         updateMetaData(page, ACTIVATE);
605         page.save();
606         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path );
607     }
608 
609     /**
610      * Updates current content activation meta data with the timestamp and user details of the deactivation.
611      */
612     protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
613         // page deactivated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
614         Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
615         updateMetaData(page, DEACTIVATE);
616         page.save();
617         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle() );
618     }
619 
620 
621     private HierarchyManager getHierarchyManager() {
622         return MgnlContext.getHierarchyManager(this.workspaceName);
623     }
624 
625     private HierarchyManager getSystemHierarchyManager() {
626         return MgnlContext.getSystemContext().getHierarchyManager(this.workspaceName);
627     }
628 
629     /**
630      * @param node
631      * @param type (activate / deactivate)
632      */
633     protected void updateMetaData(Content node, String type) throws AccessDeniedException {
634         // update the passed node
635         MetaData md = node.getMetaData();
636         if (type.equals(ACTIVATE)) {
637             md.setActivated();
638         }
639         else {
640             md.setUnActivated();
641         }
642         md.setActivatorId(this.user.getName());
643         md.setLastActivationActionDate();
644 
645         if(type.equals(ACTIVATE)){
646             if(md.getModificationDate() != null && md.getModificationDate().after(contentVersionDate)){
647                 try {
648                     Thread.sleep(1);
649                 } catch (InterruptedException e) {
650                     e.printStackTrace();
651                 }
652                 md.setModificationDate();
653             }
654         }
655 
656         Iterator<Content> children;
657         if (type.equals(ACTIVATE)) {
658             // use syndicator rule based filter
659             children = node.getChildren(new RuleBasedContentFilter(contentFilterRule)).iterator();
660         }
661         else {
662             // all children
663             children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
664         }
665 
666         while (children.hasNext()) {
667             Content child = children.next();
668             this.updateMetaData(child, type);
669         }
670 
671 
672     }
673     /**
674      * Gets target path to which the current path is mapped in given subscription. Provided path should be without trailing slash.
675      */
676     protected String getMappedPath(String path, Subscription subscription) {
677         String toURI = subscription.getToURI();
678         if (null != toURI) {
679             String fromURI = subscription.getFromURI();
680             // remove trailing slash if any
681             fromURI = StringUtils.removeEnd(fromURI, "/");
682             toURI = StringUtils.removeEnd(toURI, "/");
683             // apply path transformation if any
684             path = path.replaceFirst(fromURI, toURI);
685             if (path.equals("")) {
686                 path = "/";
687             }
688         }
689         return path;
690     }
691 
692     protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
693 
694         //String handle = getActivationURL(subscriber);
695 
696         try {
697             URL url = new URL(urlString);
698             URLConnection urlConnection = url.openConnection();
699             urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
700             urlConnection.setReadTimeout(subscriber.getReadTimeout());
701 
702             return urlConnection;
703         } catch (MalformedURLException e) {
704             throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + SecurityUtil.stripPasswordFromUrl(urlString) + "]");
705         } catch (IOException e) {
706             throw new ExchangeException("Not able to send the activation request [" + SecurityUtil.stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
707         } catch (Exception e) {
708             throw new ExchangeException(e);
709         }
710     }
711 
712     @Inject
713     public void setResouceCollector(ResourceCollector resourceCollector) {
714         this.resourceCollector = resourceCollector;
715     }
716 }