View Javadoc

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