View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 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  import info.magnolia.module.exchangesimple.monitor.ActivationMonitor;
56  import info.magnolia.objectfactory.Components;
57  
58  import java.io.File;
59  import java.io.IOException;
60  import java.io.UnsupportedEncodingException;
61  import java.net.HttpURLConnection;
62  import java.net.MalformedURLException;
63  import java.net.URL;
64  import java.net.URLConnection;
65  import java.net.URLEncoder;
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.io.IOUtils;
74  import org.apache.commons.lang.StringUtils;
75  import org.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  import EDU.oswego.cs.dl.util.concurrent.Sync;
79  
80  import com.google.inject.Inject;
81  
82  /**
83   * Default implementation of {@link Syndicator}. Activates all the content to a subscriber configured on the server.
84   * 
85   * @author Sameer Charles
86   * $Id: $
87   */
88  public abstract class BaseSyndicatorImpl implements Syndicator {
89      private static final Logger log = LoggerFactory.getLogger(BaseSyndicatorImpl.class);
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     protected ActivationMonitor activationMonitor;
175 
176     public BaseSyndicatorImpl() {
177         this(Components.getComponent(ActivationMonitor.class));
178     }
179 
180     @Inject
181     public BaseSyndicatorImpl(ActivationMonitor activationMonitor) {
182         this.activationMonitor = activationMonitor;
183     }
184 
185     /**
186      * Runs a given job in the thread pool.
187      * 
188      * @param job the job to run
189      * @throws ExchangeException if the job could not be put in the pool
190      */
191     protected static void executeInPool(Runnable job) throws ExchangeException {
192         try {
193             ThreadPool.getInstance().execute(job);
194         } catch (InterruptedException e) {
195             // this is kind of a problem, we could not add the job to the pool
196             // retrying might or might not work now that the interruption
197             // status is cleared but there is not much we can do so throwing
198             // an ExchangeException seems like the least bad choice
199             String message = "could not execute job in pool";
200             log.error(message, e);
201             throw new ExchangeException(message, e);
202         }
203     }
204 
205     /**
206      * Acquires a {@link Sync} ignoring any interruptions. Should any
207      * interruption occur the interruption status will be set. Might
208      * potentially block/wait forever.
209      * 
210      * @see Sync#acquire()
211      * @param latch the latch on which to wait
212      */
213     protected static void acquireIgnoringInterruption(Sync latch) {
214         try {
215             latch.acquire();
216         } catch (InterruptedException e) {
217             // waken up externally - ignore try again
218             acquireIgnoringInterruption(latch);
219             // be a good citizen and set back the interruption status
220             Thread.currentThread().interrupt();
221         }
222     }
223 
224     /**
225      * @deprecated since 4.5 - should no longer be needed when operating with logical workspace names
226      */
227     @Deprecated
228     protected String repositoryName;
229 
230     protected String workspaceName;
231 
232     protected String parent;
233 
234     protected Rule contentFilterRule;
235 
236     protected User user;
237 
238     private Calendar contentVersionDate;
239 
240     private MagnoliaConfigurationProperties properties;
241 
242     private ResourceCollector resourceCollector;
243 
244     /**
245      * @param user
246      * @param repositoryName repository ID
247      * @param workspaceName workspace ID
248      * @param rule content filter rule
249      * @see info.magnolia.cms.exchange.Syndicator#init(info.magnolia.cms.security.User, String, String, info.magnolia.cms.util.Rule)
250      */
251     @Override
252     public void init(User user, String repositoryName, String workspaceName, Rule rule) {
253         this.user = user;
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     @Override
282     public void activate(String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
283         this.activate(null, parent, content, orderBefore);
284     }
285 
286     /**
287      * This will activate specifies page (sub pages) to the specified subscriber.
288      * 
289      * @param subscriber
290      * @param parent parent under which this page will be activated
291      * @param content to be activated
292      * @throws javax.jcr.RepositoryException
293      * @throws info.magnolia.cms.exchange.ExchangeException
294      */
295     @Override
296     public void activate(Subscriber subscriber, String parent, Content content) throws ExchangeException, RepositoryException {
297         this.activate(subscriber, parent, content, null);
298     }
299 
300     /**
301      * This will activate specifies node to the specified subscriber.
302      * 
303      * @param subscriber
304      * @param parent parent under which this page will be activated
305      * @param content to be activated
306      * @param orderBefore List of UUID to be used by the subscriber to order this node after activation
307      * @throws javax.jcr.RepositoryException
308      * @throws info.magnolia.cms.exchange.ExchangeException
309      */
310     @Override
311     public void activate(Subscriber subscriber, String parent, Content content, List<String> orderBefore) throws ExchangeException, RepositoryException {
312         this.parent = parent;
313         String path = content.getHandle();
314 
315         if (content instanceof ContentVersion) {
316             contentVersionDate = ((ContentVersion) content).getCreated();
317         }
318 
319         ActivationContent activationContent = null;
320         try {
321             activationContent = resourceCollector.collect(content, orderBefore, parent, workspaceName, repositoryName, contentFilterRule);
322             if (null == subscriber) {
323                 this.activate(activationContent, path);
324             } else {
325                 this.activate(subscriber, activationContent, path);
326             }
327             if (Boolean.parseBoolean(activationContent.getproperty(ItemType.DELETED_NODE_MIXIN))) {
328                 final HierarchyManager hm = content.getHierarchyManager();
329                 final Session session = content.getJCRNode().getSession();
330                 String uuid = content.getUUID();
331                 if (StringUtils.isNotBlank(uuid)) {
332                     if (content instanceof ContentVersion) {
333                         // replace versioned content with the real node
334                         content = hm.getContentByUUID(uuid);
335                     }
336                     Content parentContent = content.getParent();
337                     content.delete();
338                     parentContent.save();
339                 } else {
340                     log.warn("Content {}:{} was already removed.", new String[] { content.getWorkspace().getName(), path });
341                 }
342             } else {
343                 this.updateActivationDetails(path);
344             }
345             log.info("Exchange: activation succeeded [{}]", path);
346         } catch (Exception e) {
347             if (log.isDebugEnabled()) {
348                 log.error("Exchange: activation failed for path:" + (path != null ? path : "[null]"), e);
349                 long timestamp = System.currentTimeMillis();
350                 log.warn("moving files from failed activation to *.failed" + timestamp);
351                 Iterator<File> keys = activationContent.getFiles().values().iterator();
352                 while (keys.hasNext()) {
353                     File f = keys.next();
354                     f.renameTo(new File(f.getAbsolutePath() + ".failed" + timestamp));
355                 }
356                 activationContent.getFiles().clear();
357 
358             }
359             throw new ExchangeException(e);
360         } finally {
361             log.debug("Cleaning temporary files");
362             cleanTemporaryStore(activationContent);
363         }
364     }
365 
366     /**
367      * @throws ExchangeException
368      */
369     public abstract void activate(ActivationContent activationContent, String nodePath) throws ExchangeException;
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         // Start measuring
381         long start = System.currentTimeMillis();
382 
383         boolean success = true;
384         for (File f : activationContent.getFiles().values()) {
385             activationMonitor.addSizeOfActivatedContent(f.length());
386         }
387 
388         String parentPath = null;
389 
390         // concurrency: from path and repo name are same for all subscribers
391         Subscription subscription = subscriber.getMatchedSubscription(nodePath, this.repositoryName);
392         if (null != subscription) {
393             // its subscribed since we found the matching subscription
394             parentPath = this.getMappedPath(this.parent, subscription);
395             activationContent.setProperty(PARENT_PATH, parentPath);
396         } else {
397             log.debug("Exchange : subscriber [{}] is not subscribed to {}", subscriber.getName(), nodePath);
398             return "not subscribed";
399         }
400         log.debug("Exchange : sending activation request to {} with user {}", subscriber.getName(), this.user.getName());
401 
402         URLConnection urlConnection = null;
403         String versionName = null;
404         try {
405             urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
406             versionName = transportActivatedData(activationContent, urlConnection, null);
407 
408             String status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
409 
410             if (StringUtils.equals(status, ACTIVATION_HANDSHAKE)) {
411                 String handshakeKey = urlConnection.getHeaderField(ACTIVATION_AUTH);
412                 // receive all pending data
413                 urlConnection.getContent();
414 
415                 // transport the data again
416                 urlConnection = prepareConnection(subscriber, getActivationURL(subscriber));
417                 // and get the version & status again
418                 versionName = transportActivatedData(activationContent, urlConnection, handshakeKey);
419                 status = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_STATUS);
420             }
421 
422             // check if the activation failed
423             if (StringUtils.equals(status, ACTIVATION_FAILED)) {
424                 String message = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_MESSAGE);
425                 throw new ExchangeException("Message received from subscriber: " + message);
426             }
427             urlConnection.getContent();
428             log.debug("Exchange : activation request sent to {}", subscriber.getName());
429         } catch (ExchangeException e) {
430             activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
431             success = false;
432             throw e;
433         } catch (IOException e) {
434             log.debug("Failed to transport following activated content {" + StringUtils.join(activationContent.getProperties().keySet().iterator(), ',') + "} due to " + e.getMessage(), e);
435             String url = urlConnection == null ? null : urlConnection.getURL().toString();
436             url = SecurityUtil.stripPasswordFromUrl(url);
437             activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
438             success = false;
439             // hide pwd if present
440             throw new ExchangeException("Not able to send the activation request [" + url + "]: " + e.getMessage(), e);
441         } catch (Exception e) {
442             activationMonitor.logError(nodePath, user.getName(), workspaceName, subscriber.getName(), e, false);
443             success = false;
444             throw new ExchangeException(e);
445         } finally {
446             releaseConnection(urlConnection);
447             long end = System.currentTimeMillis();
448             activationMonitor.addActivationTime(end - start);
449             activationMonitor.logActivation(nodePath, user.getName(), workspaceName, subscriber.getName(), false, success);
450         }
451         return versionName;
452     }
453 
454     protected void releaseConnection(URLConnection urlConnection) {
455         if (urlConnection == null) {
456             return;
457         }
458         // close all in case jvm is not smart enough to find out
459         try {
460             IOUtils.closeQuietly(urlConnection.getInputStream());
461         } catch (IOException e) {
462             // this is most likely happening only when stream is closed already or not available. Show it only in debug mode.
463             if (log.isDebugEnabled()) {
464                 log.error("Failed to release input stream of activation connection with " + e.getMessage(), e);
465             }
466         }
467         if (urlConnection instanceof HttpURLConnection) {
468             ((HttpURLConnection) urlConnection).disconnect();
469         }
470     }
471     private String transportActivatedData(ActivationContent activationContent, URLConnection urlConnection, String handshakeKey) throws ExchangeException {
472         String versionName;
473         this.addActivationHeaders(urlConnection, activationContent, handshakeKey);
474 
475         Transporter.transport((HttpURLConnection) urlConnection, activationContent);
476 
477         versionName = urlConnection.getHeaderField(ACTIVATION_ATTRIBUTE_VERSION);
478         return versionName;
479     }
480 
481     /**
482      * Cleans up temporary file store after activation.
483      */
484     protected void cleanTemporaryStore(ActivationContent activationContent) {
485         if (activationContent == null) {
486             log.debug("Clean temporary store - nothing to do");
487             return;
488         }
489         if (log.isDebugEnabled()) {
490             log.debug("Debugging is enabled. Keeping temporary files in store for debugging purposes. Clean the store manually once done with debugging.");
491             return;
492         }
493 
494         Iterator<String> keys = activationContent.getFiles().keySet().iterator();
495         while (keys.hasNext()) {
496             String key = keys.next();
497             log.debug("Removing temporary file {}", key);
498             activationContent.getFile(key).delete();
499         }
500     }
501 
502     public synchronized void deactivate(String path) throws ExchangeException, RepositoryException {
503         final Content node = getHierarchyManager().getContent(path);
504         deactivate(node);
505     }
506 
507     /**
508      * @param node to deactivate
509      * @throws RepositoryException
510      * @throws ExchangeException
511      */
512     @Override
513     public synchronized void deactivate(Content node) throws ExchangeException, RepositoryException {
514         String nodeUUID = node.getUUID();
515         String path = node.getHandle();
516         this.doDeactivate(nodeUUID, path);
517         updateDeactivationDetails(nodeUUID);
518     }
519 
520     /**
521      * @param node , to deactivate
522      * @param subscriber
523      * @throws RepositoryException
524      * @throws ExchangeException
525      */
526     @Override
527     public synchronized void deactivate(Subscriber subscriber, Content node) throws ExchangeException, RepositoryException {
528         String nodeUUID = node.getUUID();
529         String path = node.getHandle();
530         this.doDeactivate(subscriber, nodeUUID, path);
531         updateDeactivationDetails(nodeUUID);
532     }
533 
534     /**
535      * @throws ExchangeException
536      */
537     public abstract void doDeactivate(String nodeUUID, String nodePath) throws ExchangeException;
538 
539     /**
540      * Deactivate content from specified subscriber.
541      * 
542      * @param subscriber
543      * @throws ExchangeException
544      */
545     public abstract String doDeactivate(Subscriber subscriber, String nodeUUID, String nodePath) throws ExchangeException;
546 
547     /**
548      * Return URI set for deactivation.
549      * 
550      * @param subscriberInfo
551      */
552     protected String getDeactivationURL(Subscriber subscriberInfo) {
553         return getActivationURL(subscriberInfo);
554     }
555 
556     /**
557      * Adds header fields describing deactivation request.
558      * 
559      * @param connection
560      * @param handshakeKey
561      * optional key to encrypt public key before sending it over
562      */
563     protected void addDeactivationHeaders(URLConnection connection, String nodeUUID, String handshakeKey) {
564         connection.addRequestProperty(REPOSITORY_NAME, this.repositoryName);
565         connection.addRequestProperty(WORKSPACE_NAME, this.workspaceName);
566         // 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!!!
567         String md5 = "";
568         if (nodeUUID != null) {
569             connection.addRequestProperty(NODE_UUID, nodeUUID);
570             // send md5 of uuid ... it would be silly to send clear text along the encrypted message
571             md5 = SecurityUtil.getMD5Hex(nodeUUID);
572         }
573         // send md5 of uuid ... it would be silly to send clear text along the encrypted message
574         String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
575 
576         // optional
577         addHandshakeInfo(connection, handshakeKey);
578 
579         connection.setRequestProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
580         connection.addRequestProperty(ACTION, DEACTIVATE);
581     }
582 
583     protected void addHandshakeInfo(URLConnection connection, String handshakeKey) {
584         if (handshakeKey != null) {
585             connection.setRequestProperty(ACTIVATION_AUTH_KEY, SecurityUtil.encrypt(SecurityUtil.getPublicKey(), handshakeKey));
586         }
587     }
588 
589     /**
590      * Retrieves URL subscriber is listening on for (de)activation requests.
591      */
592     protected String getActivationURL(Subscriber subscriberInfo) {
593         final String url = subscriberInfo.getURL();
594         if (!url.endsWith("/")) {
595             return url + "/" + DEFAULT_HANDLER;
596         }
597         return url + DEFAULT_HANDLER;
598     }
599 
600     /**
601      * Adds headers fields describing activation request.
602      * 
603      * @param handshakeKey
604      * 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.
605      */
606     protected void addActivationHeaders(URLConnection connection, ActivationContent activationContent, String handshakeKey) {
607 
608         String md5 = activationContent.getproperty(RESOURCE_MAPPING_MD_ATTRIBUTE);
609         String pass = System.currentTimeMillis() + ";" + this.user.getName() + ";" + md5;
610         activationContent.setProperty(ACTIVATION_AUTH, SecurityUtil.encrypt(pass));
611         Iterator<String> headerKeys = activationContent.getProperties().keySet().iterator();
612         while (headerKeys.hasNext()) {
613             String key = headerKeys.next();
614             if (RESOURCE_MAPPING_MD_ATTRIBUTE.equals(key)) {
615                 // do not send md5 in plain string
616                 continue;
617             }
618             String value = activationContent.getproperty(key);
619             if (SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED)) {
620                 try {
621                     value = URLEncoder.encode(value, "UTF-8");
622                 } catch (UnsupportedEncodingException e) {
623                     // do nothing
624                 }
625             }
626             connection.setRequestProperty(key, value);
627         }
628         addHandshakeInfo(connection, handshakeKey);
629     }
630 
631     /**
632      * Updates current content activation meta data with the time stamp and user details of the activation.
633      */
634     protected void updateActivationDetails(String path) throws RepositoryException {
635         // page activated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
636         Content page = getSystemHierarchyManager().getContent(path);
637         updateMetaData(page, ACTIVATE);
638         page.save();
639         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_ACTIVATE, this.workspaceName, page.getItemType(), path);
640     }
641 
642     /**
643      * Updates current content activation meta data with the timestamp and user details of the deactivation.
644      */
645     protected void updateDeactivationDetails(String nodeUUID) throws RepositoryException {
646         // page deactivated already use system context to ensure meta data is activated even if activating user has no rights to the activated page children
647         Content page = getSystemHierarchyManager().getContentByUUID(nodeUUID);
648         updateMetaData(page, DEACTIVATE);
649         page.save();
650         AuditLoggingUtil.log(AuditLoggingUtil.ACTION_DEACTIVATE, this.workspaceName, page.getItemType(), page.getHandle());
651     }
652 
653     private HierarchyManager getHierarchyManager() {
654         return MgnlContext.getHierarchyManager(this.workspaceName);
655     }
656 
657     private HierarchyManager getSystemHierarchyManager() {
658         return MgnlContext.getSystemContext().getHierarchyManager(this.workspaceName);
659     }
660 
661     /**
662      * @param node
663      * @param type (activate / deactivate)
664      */
665     protected void updateMetaData(Content node, String type) throws AccessDeniedException {
666         // update the passed node
667         MetaData md = node.getMetaData();
668         if (type.equals(ACTIVATE)) {
669             md.setActivated();
670         }
671         else {
672             md.setUnActivated();
673         }
674         md.setActivatorId(this.user.getName());
675         md.setLastActivationActionDate();
676 
677         if (type.equals(ACTIVATE)) {
678             if (md.getModificationDate() != null && md.getModificationDate().after(contentVersionDate)) {
679                 try {
680                     Thread.sleep(1);
681                 } catch (InterruptedException e) {
682                     e.printStackTrace();
683                 }
684                 md.setModificationDate();
685             }
686         }
687 
688         Iterator<Content> children;
689         if (type.equals(ACTIVATE)) {
690             // use syndicator rule based filter
691             children = node.getChildren(new RuleBasedContentFilter(contentFilterRule)).iterator();
692         }
693         else {
694             // all children
695             children = node.getChildren(ContentUtil.EXCLUDE_META_DATA_CONTENT_FILTER).iterator();
696         }
697 
698         while (children.hasNext()) {
699             Content child = children.next();
700             this.updateMetaData(child, type);
701         }
702 
703     }
704 
705     /**
706      * Gets target path to which the current path is mapped in given subscription. Provided path should be without trailing slash.
707      */
708     protected String getMappedPath(String path, Subscription subscription) {
709         String toURI = subscription.getToURI();
710         if (null != toURI) {
711             String fromURI = subscription.getFromURI();
712             // remove trailing slash if any
713             fromURI = StringUtils.removeEnd(fromURI, "/");
714             toURI = StringUtils.removeEnd(toURI, "/");
715             // apply path transformation if any
716             path = path.replaceFirst(fromURI, toURI);
717             if (path.equals("")) {
718                 path = "/";
719             }
720         }
721         return path;
722     }
723 
724     protected URLConnection prepareConnection(Subscriber subscriber, String urlString) throws ExchangeException {
725 
726         // String handle = getActivationURL(subscriber);
727 
728         try {
729             URL url = new URL(urlString);
730             URLConnection urlConnection = url.openConnection();
731             urlConnection.setConnectTimeout(subscriber.getConnectTimeout());
732             urlConnection.setReadTimeout(subscriber.getReadTimeout());
733 
734             return urlConnection;
735         } catch (MalformedURLException e) {
736             throw new ExchangeException("Incorrect URL for subscriber " + subscriber + "[" + SecurityUtil.stripPasswordFromUrl(urlString) + "]");
737         } catch (IOException e) {
738             throw new ExchangeException("Not able to send the activation request [" + SecurityUtil.stripPasswordFromUrl(urlString) + "]: " + e.getMessage());
739         } catch (Exception e) {
740             throw new ExchangeException(e);
741         }
742     }
743 
744     @Inject
745     public void setResouceCollector(ResourceCollector resourceCollector) {
746         this.resourceCollector = resourceCollector;
747     }
748 }