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.beans.runtime.Document;
37  import info.magnolia.cms.beans.runtime.MultipartForm;
38  import info.magnolia.cms.core.Access;
39  import info.magnolia.cms.core.Content;
40  import info.magnolia.cms.core.HierarchyManager;
41  import info.magnolia.cms.core.ItemType;
42  import info.magnolia.cms.core.NodeData;
43  import info.magnolia.cms.core.SystemProperty;
44  import info.magnolia.cms.exchange.ExchangeException;
45  import info.magnolia.cms.filters.AbstractMgnlFilter;
46  import info.magnolia.cms.security.AccessDeniedException;
47  import info.magnolia.cms.security.Permission;
48  import info.magnolia.cms.util.ContentUtil;
49  import info.magnolia.cms.util.Rule;
50  import info.magnolia.cms.util.RuleBasedContentFilter;
51  import info.magnolia.context.MgnlContext;
52  
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.security.InvalidParameterException;
56  import java.util.Iterator;
57  import java.util.List;
58  import java.util.zip.GZIPInputStream;
59  
60  import javax.jcr.ImportUUIDBehavior;
61  import javax.jcr.ItemNotFoundException;
62  import javax.jcr.Node;
63  import javax.jcr.PathNotFoundException;
64  import javax.jcr.Property;
65  import javax.jcr.PropertyType;
66  import javax.jcr.RepositoryException;
67  import javax.jcr.UnsupportedRepositoryOperationException;
68  import javax.jcr.lock.LockException;
69  import javax.servlet.FilterChain;
70  import javax.servlet.ServletException;
71  import javax.servlet.http.HttpServletRequest;
72  import javax.servlet.http.HttpServletResponse;
73  import javax.servlet.http.HttpSession;
74  
75  import org.apache.commons.codec.binary.Base64;
76  import org.apache.commons.io.IOUtils;
77  import org.apache.commons.lang.StringUtils;
78  import org.jdom.Element;
79  import org.jdom.JDOMException;
80  import org.jdom.input.SAXBuilder;
81  import org.safehaus.uuid.UUIDGenerator;
82  import org.slf4j.Logger;
83  import org.slf4j.LoggerFactory;
84  
85  /**
86   * This filter receives activation requests from another instance and applies them.
87   *
88   * @author Sameer Charles
89   * $Id$
90   */
91  public class ReceiveFilter extends AbstractMgnlFilter {
92  
93      private static final Logger log = LoggerFactory.getLogger(ReceiveFilter.class);
94  
95      /**
96       * @deprecated since 3.5. This is the attribute name that was used in 3.0, so we keep to be able to activate from 3.0.
97       */
98      @Deprecated
99      private static final String SIBLING_UUID_3_0 = "UUID";
100 
101     public static final String SYSTEM_REPO = "mgnlSystem";
102 
103     public static final String ROOT_LOCK_NAME = "rootLock";
104 
105     private int unlockRetries = 10;
106 
107     private int retryWait = 2;
108 
109     public int getUnlockRetries() {
110         return unlockRetries;
111     }
112 
113     public void setUnlockRetries(int unlockRetries) {
114         this.unlockRetries = unlockRetries;
115     }
116 
117     public long getRetryWait() {
118         return retryWait;
119     }
120 
121     public void setRetryWait(int retryWait) {
122         this.retryWait = retryWait;
123     }
124 
125     @Override
126     public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
127         String statusMessage = "";
128         String status = "";
129         String result = null;
130 
131         try {
132             final String utf8AuthorStatus = request.getHeader(BaseSyndicatorImpl.UTF8_STATUS);
133             // null check first to make sure we do not break activation from older versions w/o this flag
134             if (utf8AuthorStatus != null && (Boolean.parseBoolean(utf8AuthorStatus) != SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED))) {
135                 throw new UnsupportedOperationException("Activation between instances with different UTF-8 setting is not supported.");
136             }
137             final String action = request.getHeader(BaseSyndicatorImpl.ACTION);
138             if (action == null) {
139                 throw new InvalidParameterException("Activation action must be set for each activation request.");
140             }
141             applyLock(request);
142         } catch (ExchangeException e) {
143             // can't obtain a lock ... this is (should be) a normal threading situation that we are
144             // just reporting back to user.
145             log.debug(e.getMessage(), e);
146             // we can only rely on the exception's actual message to give something back to the user
147             // here.
148             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
149             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
150             setResponseHeaders(response, statusMessage, status, result);
151             return;
152         } catch (Throwable e) {
153             log.error(e.getMessage(), e);
154             // we can only rely on the exception's actual message to give something back to the user here.
155             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
156             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
157             setResponseHeaders(response, statusMessage, status, result);
158             return;
159         }
160         try {
161             result = receive(request);
162             status = BaseSyndicatorImpl.ACTIVATION_SUCCESSFUL;
163         }
164         catch (OutOfMemoryError e) {
165             Runtime rt = Runtime.getRuntime();
166             log.error("---------\nOutOfMemoryError caught during activation. Total memory = " //$NON-NLS-1$
167                     + rt.totalMemory()
168                     + ", free memory = " //$NON-NLS-1$
169                     + rt.freeMemory()
170                     + "\n---------"); //$NON-NLS-1$
171             statusMessage = e.getMessage();
172             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
173         }
174         catch (PathNotFoundException e) {
175             // this should not happen. PNFE should be already caught and wrapped in ExchangeEx
176             log.error(e.getMessage(), e);
177             statusMessage = "Parent not found (not yet activated): " + e.getMessage();
178             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
179         } catch (ExchangeException e) {
180             log.debug(e.getMessage(), e);
181             statusMessage = e.getMessage();
182             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
183         } catch (Throwable e) {
184             log.error(e.getMessage(), e);
185             // we can only rely on the exception's actual message to give something back to the user here.
186             statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
187             status = BaseSyndicatorImpl.ACTIVATION_FAILED;
188         }
189         finally {
190             cleanUp(request, status);
191             setResponseHeaders(response, statusMessage, status, result);
192         }
193     }
194 
195     protected void setResponseHeaders(HttpServletResponse response, String statusMessage, String status, String result) {
196         response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_STATUS, status);
197         response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_MESSAGE, statusMessage);
198     }
199 
200     /**
201      * handle activate or deactivate request.
202      * @param request
203      * @throws Exception if fails to update
204      */
205     protected synchronized String receive(HttpServletRequest request) throws Exception {
206         String action = request.getHeader(BaseSyndicatorImpl.ACTION);
207         log.debug("action: " + action);
208         String authorization = getUser(request);
209         String webapp = getWebappName();
210 
211         if (action.equalsIgnoreCase(BaseSyndicatorImpl.ACTIVATE)) {
212             String name = update(request);
213             // Everything went well
214             log.info("User {} successfuly activated {} on {}.", new Object[]{authorization, name, webapp});
215         }
216         else if (action.equalsIgnoreCase(BaseSyndicatorImpl.DEACTIVATE)) {
217             String name = remove(request);
218             // Everything went well
219             log.info("User {} succeessfuly deactivated {} on {}.", new Object[] {authorization, name, webapp});
220         }
221         else {
222             throw new UnsupportedOperationException("Method not supported : " + action);
223         }
224         return null;
225     }
226 
227     protected String getWebappName() {
228         return SystemProperty.getProperty(SystemProperty.MAGNOLIA_WEBAPP);
229     }
230 
231     protected String getUser(HttpServletRequest request) {
232         // get the user who authorized this request.
233         String user = request.getHeader(BaseSyndicatorImpl.AUTHORIZATION);
234         if (StringUtils.isEmpty(user)) {
235             user = request.getParameter(BaseSyndicatorImpl.AUTH_USER);
236         } else {
237             user = new String(Base64.decodeBase64(user.substring(6).getBytes())); //Basic uname:pwd
238             user = user.substring(0, user.indexOf(":"));
239         }
240         return user;
241     }
242 
243     /**
244      * handle update (activate) request.
245      * @param request
246      * @throws Exception if fails to update
247      */
248     protected synchronized String update(HttpServletRequest request) throws Exception {
249         MultipartForm data = MgnlContext.getPostedForm();
250         if (null != data) {
251             String newParentPath = this.getParentPath(request);
252             String resourceFileName = request.getHeader(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE);
253             HierarchyManager hm = getHierarchyManager(request);
254             Element rootElement = getImportedContentRoot(data, resourceFileName);
255             Element topContentElement = rootElement.getChild(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT);
256             Content content = null;
257             try {
258                 String uuid = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE);
259                 content = hm.getContentByUUID(uuid);
260                 // move content to new location if necessary.
261                 newParentPath = handleMovedContent(newParentPath, hm, topContentElement, content);
262                 handleChildren(request, content);
263                 this.importOnExisting(topContentElement, data, hm, content);
264             }
265             catch (ItemNotFoundException e) {
266                 // new content
267                 importFresh(topContentElement, data, hm, newParentPath);
268             }
269 
270             return orderImportedNode(newParentPath, hm, rootElement, topContentElement);
271         }
272         return null;
273     }
274 
275     protected Element getImportedContentRoot(MultipartForm data, String resourceFileName) throws JDOMException, IOException {
276         Document resourceDocument = data.getDocument(resourceFileName);
277         SAXBuilder builder = new SAXBuilder();
278         InputStream documentInputStream = resourceDocument.getStream();
279         org.jdom.Document jdomDocument = builder.build(documentInputStream);
280         IOUtils.closeQuietly(documentInputStream);
281         return jdomDocument.getRootElement();
282     }
283 
284     protected void handleChildren(HttpServletRequest request, Content content) {
285         String ruleString = request.getHeader(BaseSyndicatorImpl.CONTENT_FILTER_RULE);
286         Rule rule = new Rule(ruleString, ",");
287         RuleBasedContentFilter filter = new RuleBasedContentFilter(rule);
288         // remove all child nodes
289         this.removeChildren(content, filter);
290     }
291 
292     protected String handleMovedContent(String newParentPath,
293             HierarchyManager hm, Element topContentElement, Content content)
294                     throws RepositoryException {
295         String currentParentPath = content.getHandle();
296         currentParentPath = currentParentPath.substring(0, currentParentPath.lastIndexOf('/'));
297         String newName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
298         if (!newParentPath.endsWith("/")) {
299             newParentPath += "/";
300         }
301         if (!currentParentPath.endsWith("/")) {
302             currentParentPath += "/";
303         }
304         if (!newParentPath.equals(currentParentPath) || !content.getName().equals(newName)) {
305             log.info("Moving content from {} to {} due to activation request.", new Object[] { content.getHandle(), newParentPath  + newName});
306             hm.moveTo(content.getHandle(), newParentPath + newName);
307         }
308         return newParentPath;
309     }
310 
311     protected String orderImportedNode(String newParentPath, HierarchyManager hm, Element rootElement, Element topContentElement) throws RepositoryException {
312         String name;
313         // order imported node
314         name = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
315         Content parent = hm.getContent(newParentPath);
316         List siblings = rootElement.getChild(BaseSyndicatorImpl.SIBLINGS_ROOT_ELEMENT).getChildren(BaseSyndicatorImpl.SIBLINGS_ELEMENT);
317         Iterator siblingsIterator = siblings.iterator();
318         while (siblingsIterator.hasNext()) {
319             Element sibling = (Element) siblingsIterator.next();
320             // check for existence and order
321             String siblingUUID = sibling.getAttributeValue(BaseSyndicatorImpl.SIBLING_UUID);
322             try {
323                 // be compatible with 3.0 (MAGNOLIA-2016)
324                 if (StringUtils.isEmpty(siblingUUID)) {
325                     log.debug("Activating from a Magnolia 3.0 instance");
326                     siblingUUID = sibling.getAttributeValue(SIBLING_UUID_3_0);
327                 }
328                 Content beforeContent = hm.getContentByUUID(siblingUUID);
329                 log.debug("Ordering {} before {}", name, beforeContent.getName());
330                 order(parent, name, beforeContent.getName());
331                 break;
332             } catch (AccessDeniedException e) {
333                 log.info("User doesn't have enough rights to force order of nodes on {}", siblingUUID);
334             } catch (ItemNotFoundException e) {
335                 // ignore
336             } catch (RepositoryException re) {
337                 log.warn("Failed to order node");
338                 log.debug("Failed to order node", re);
339             }
340         }
341 
342         // ensure the no sibling nodes are at the end ... since move is not activated immediately it is sometimes necessary to preserve right order
343         if (siblings.isEmpty()) {
344             order(parent, name, null);
345         }
346         return name;
347     }
348 
349 
350     protected void order(Content parent, String name, String orderBefore) throws RepositoryException {
351         try {
352             parent.orderBefore(name, orderBefore);
353         } catch (UnsupportedRepositoryOperationException e) {
354             // since not all types support ordering we should not enforce it, but only log the error
355             log.warn("Failed to order unorderable content {} at {} due to {}", new Object[] {name, parent.getHandle(), e.getMessage()});
356         }
357         parent.save();
358     }
359 
360     /**
361      * Copy all properties from source to destination (by cleaning the old properties).
362      * @param source the content node to be copied
363      * @param destination the destination node
364      */
365     protected synchronized void copyProperties(Content source, Content destination) throws RepositoryException {
366         // first remove all existing properties at the destination
367         // will be different with incremental activation
368         Iterator nodeDataIterator = destination.getNodeDataCollection().iterator();
369         while (nodeDataIterator.hasNext()) {
370             NodeData nodeData = (NodeData) nodeDataIterator.next();
371             // Ignore binary types, since these are sub nodes and already taken care of while
372             // importing sub resources
373             if (nodeData.getType() != PropertyType.BINARY) {
374                 nodeData.delete();
375             }
376         }
377 
378         // copy all properties
379         Node destinationNode = destination.getJCRNode();
380         nodeDataIterator = source.getNodeDataCollection().iterator();
381         while (nodeDataIterator.hasNext()) {
382             NodeData nodeData = (NodeData) nodeDataIterator.next();
383             Property property = nodeData.getJCRProperty();
384             if (property.getDefinition().isMultiple()) {
385                 if (destination.isGranted(Permission.WRITE)) {
386                     destinationNode.setProperty(nodeData.getName(), property.getValues());
387                 }
388                 else {
389                     throw new AccessDeniedException("User not allowed to " + Permission.PERMISSION_NAME_WRITE + " at [" + nodeData.getHandle() + "]"); //$NON-NLS-1$ $NON-NLS-2$ $NON-NLS-3$
390                 }
391             }
392             else {
393                 destination.createNodeData(nodeData.getName(), nodeData.getValue());
394             }
395         }
396     }
397 
398     /**
399      * remove children.
400      * @param content whose children to be deleted
401      * @param filter content filter
402      */
403     protected synchronized void removeChildren(Content content, Content.ContentFilter filter) {
404         Iterator children = content.getChildren(filter).iterator();
405         // remove sub nodes using the same filter used by the sender to collect
406         // this will make sure there is no existing nodes of the same type
407         while (children.hasNext()) {
408             Content child = (Content) children.next();
409             try {
410                 child.delete();
411             }
412             catch (Exception e) {
413                 log.error("Failed to remove " + child.getHandle() + " | " + e.getMessage());
414             }
415         }
416     }
417 
418     /**
419      * import on non existing tree.
420      * @param topContentElement
421      * @param data
422      * @param hierarchyManager
423      * @param parentPath
424      * @throws ExchangeException
425      * @throws RepositoryException
426      */
427     protected synchronized void importFresh(Element topContentElement, MultipartForm data, HierarchyManager hierarchyManager, String parentPath) throws ExchangeException, RepositoryException {
428         // content might still exists under different uuid if it was auto generated. Check the path first, if exists, then remove it before activating new content into same path
429         // TODO: handle same name siblings!
430         String path = parentPath + (parentPath.endsWith("/") ? "" : "/") + topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
431         if (hierarchyManager.isExist(path)) {
432             log.warn("Replacing {} due to name collision (but different UUIDs.). This operation could not be rolled back in case of failure and you need to reactivate the page manually.", path);
433             hierarchyManager.delete(path);
434         }
435         try {
436             importResource(data, topContentElement, hierarchyManager, parentPath);
437             hierarchyManager.save();
438         } catch (PathNotFoundException e) {
439             final String  message = "Parent content " + parentPath + " is not yet activated or you do not have write access to it. Please activate the parent content before activating children and ensure you have appropriate rights"; // .. on XXX will be appended to the error message by syndicator on the author instance
440             // this is not a system error so there should not be a need to log the exception all the time.
441             log.debug(message, e);
442             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
443             throw new ExchangeException(message);
444         } catch (Exception e) {
445             final String message = "Activation failed | " + e.getMessage();
446             log.error("Exception caught", e);
447             hierarchyManager.refresh(false); // revert all transient changes made in this session till now.
448             throw new ExchangeException(message);
449         }
450     }
451 
452     /**
453      * import on existing content, making sure that content which is not sent stays as is.
454      * @param topContentElement
455      * @param data
456      * @param hierarchyManager
457      * @param existingContent
458      * @throws ExchangeException
459      * @throws RepositoryException
460      */
461     protected synchronized void importOnExisting(Element topContentElement, MultipartForm data,
462             final HierarchyManager hierarchyManager, Content existingContent) throws ExchangeException, RepositoryException {
463         final Iterator<Content> fileListIterator = topContentElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
464         final String uuid = UUIDGenerator.getInstance().generateTimeBasedUUID().toString();
465         final String handle = existingContent.getHandle();
466         // Can't execute in system context here just get hm from SC and use it for temp node handling
467         final HierarchyManager systemHM = MgnlContext.getSystemContext().getHierarchyManager(SYSTEM_REPO);
468         try {
469             while (fileListIterator.hasNext()) {
470                 Element fileElement = (Element) fileListIterator.next();
471                 importResource(data, fileElement, hierarchyManager, handle);
472             }
473             // use temporary node in mgnlSystem workspace to extract the top level node and copy its properties
474             Content activationTmp = ContentUtil.getOrCreateContent(systemHM.getRoot(), "activation-tmp", ItemType.FOLDER, true);
475             final Content transientNode = activationTmp.createContent(uuid, ItemType.CONTENTNODE.toString());
476             systemHM.save();
477             final String transientStoreHandle = transientNode.getHandle();
478             // import properties into transientStore
479             final String fileName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
480             final GZIPInputStream inputStream = new GZIPInputStream(data.getDocument(fileName).getStream());
481             // need to import in system context
482             // and yes we generate new uuids - content is stored here just totransfer it's props. We
483             // already have backup copy in same workspace w/ same uuid
484             // and using other strategy would destroy such copy
485             systemHM.getWorkspace().getSession().importXML(transientStoreHandle, inputStream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
486             IOUtils.closeQuietly(inputStream);
487             // copy properties from transient store to existing content
488             Content tmpContent = transientNode.getContent(topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE));
489             copyProperties(tmpContent, existingContent);
490             systemHM.delete(transientStoreHandle);
491             hierarchyManager.save();
492             systemHM.save();
493         } catch (Exception e) {
494             // revert all transient changes made in this session till now.
495             hierarchyManager.refresh(false);
496             systemHM.refresh(false);
497 
498             log.error("Exception caught", e);
499             throw new ExchangeException("Activation failed : " + e.getMessage());
500         }
501     }
502 
503     /**
504      * import documents.
505      * @param data as sent
506      * @param resourceElement parent file element
507      * @param hm
508      * @param parentPath Path to the node parent
509      * @throws Exception
510      */
511     protected synchronized void importResource(MultipartForm data, Element resourceElement, HierarchyManager hm, String parentPath) throws Exception {
512 
513         // throws an exception in case you don't have the permission
514         Access.isGranted(hm.getAccessManager(), parentPath, Permission.WRITE);
515 
516         final String name = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
517         final String fileName = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
518         // do actual import
519         final GZIPInputStream inputStream = new GZIPInputStream(data.getDocument(fileName).getStream());
520         log.debug("Importing {} into parent path {}", new Object[] {name, parentPath});
521         hm.getWorkspace().getSession().importXML(parentPath, inputStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
522         IOUtils.closeQuietly(inputStream);
523         Iterator fileListIterator = resourceElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
524         // parent path
525         try {
526             parentPath = hm.getContentByUUID(resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE)).getHandle();
527         } catch (ItemNotFoundException e) {
528             // non referencable content like meta data ...
529             // FYI: if we ever have non referencable same name sibling content the trouble will be here with child content being mixed
530             parentPath = StringUtils.removeEnd(parentPath, "/") + "/" + name;
531         }
532         while (fileListIterator.hasNext()) {
533             Element fileElement = (Element) fileListIterator.next();
534             importResource(data, fileElement, hm, parentPath);
535         }
536     }
537 
538     /**
539      * Deletes (de-activate) the content specified by the request.
540      * @param request
541      * @throws Exception if fails to update
542      */
543     protected synchronized String remove(HttpServletRequest request) throws Exception {
544         HierarchyManager hm = getHierarchyManager(request);
545         String handle = null;
546         try {
547             Content node = this.getNode(request);
548             handle = node.getHandle();
549             hm.delete(handle);
550             hm.save();
551         } catch (ItemNotFoundException e) {
552             log.debug("Unable to delete node", e);
553         }
554         return handle;
555     }
556 
557     /**
558      * get hierarchy manager.
559      * @param request
560      * @throws ExchangeException
561      */
562     protected HierarchyManager getHierarchyManager(HttpServletRequest request) throws ExchangeException {
563         String repositoryName = request.getHeader(BaseSyndicatorImpl.REPOSITORY_NAME);
564         String workspaceName = request.getHeader(BaseSyndicatorImpl.WORKSPACE_NAME);
565 
566         if (StringUtils.isEmpty(repositoryName) || StringUtils.isEmpty(workspaceName)) {
567             throw new ExchangeException("Repository or workspace name not sent, unable to activate. Repository: " + repositoryName + ", workspace: " + workspaceName) ;
568         }
569 
570         return MgnlContext.getHierarchyManager(repositoryName, workspaceName);
571     }
572 
573     /**
574      * cleans temporary store and removes any locks set.
575      *
576      * @param request
577      * @param status
578      */
579     protected void cleanUp(HttpServletRequest request, String status) {
580         if (!BaseSyndicatorImpl.DEACTIVATE.equalsIgnoreCase(request.getHeader(BaseSyndicatorImpl.ACTION))) {
581             MultipartForm data = MgnlContext.getPostedForm();
582             if (null != data) {
583                 Iterator keys = data.getDocuments().keySet().iterator();
584                 while (keys.hasNext()) {
585                     String key = (String) keys.next();
586                     data.getDocument(key).delete();
587                 }
588             }
589             releaseLock(request);
590         }
591 
592         // TODO : why is this here ? as far as I can tell, http sessions are never created when reaching this
593         try {
594             HttpSession httpSession = request.getSession(false);
595             if (httpSession != null) {
596                 httpSession.invalidate();
597             }
598         } catch (Throwable t) {
599             // its only a test so just dump
600             log.error("failed to invalidate session", t);
601         }
602     }
603 
604     protected void releaseLock(HttpServletRequest request) {
605         try {
606             final String parentPath = getParentPath(request);
607             Content content = null;
608             // unlock parent
609             if (StringUtils.isEmpty(parentPath) || this.getHierarchyManager(request).isExist(parentPath)) {
610                 try {
611                     content = this.getNode(request);
612                 } catch (ItemNotFoundException e) {
613                     // ignore - commit of deactivation
614                 }
615                 unlock(content);
616             }
617 
618             // unlock the node itself
619             Content item = null;
620             try {
621                 final String uuid = this.getUUID(request);
622                 if (StringUtils.isNotBlank(uuid)) {
623                     item = this.getHierarchyManager(request).getContentByUUID(uuid);
624                 }
625             } catch (ItemNotFoundException e) {
626                 // ignore - commit of deactivation
627             } catch (PathNotFoundException e) {
628                 // ignore - commit of deactivation
629             }
630             unlock(item);
631 
632             // unlock root (if locked)
633             Content rootLock = getRootLockOrNull(content);
634             unlock(rootLock);
635         } catch (LockException le) {
636             // either repository does not support locking OR this node never locked
637             log.warn(le.getMessage());
638         } catch (RepositoryException re) {
639             // should never come here
640             log.warn("Exception caught", re);
641         } catch (ExchangeException e) {
642             // should never come here
643             log.warn("Exception caught", e);
644         }
645     }
646 
647     private void unlock(Content content) throws RepositoryException, LockException {
648         if (content != null && content.isLocked()) {
649             content.unlock();
650             logLockStatus(content, false);
651         }
652     }
653 
654     /**
655      * apply lock.
656      * @param request
657      */
658     protected void applyLock(HttpServletRequest request) throws ExchangeException {
659         Content node = null;
660         try{
661             node = getHierarchyManager(request).getContentByUUID(getUUID(request));
662         } catch (ItemNotFoundException e) {
663         // ignore node just ain't exist yet
664         } catch (RepositoryException e) {
665             throw new ExchangeException(e.getMessage(), e);
666         }
667         try {
668             Content parent = waitForLock(request);
669             lock(node, parent);
670         } catch (RepositoryException re) {
671             // will blow fully at later stage
672             log.warn("Exception caught", re);
673         }
674     }
675 
676     /**
677      * Will wait for predefined amount of time and attempt predefined number of times to obtain unlocked content.
678      *
679      * @param request
680      * @return unlocked content specified by the request or null in case such content doesnt exist
681      * @throws ExchangeException
682      * @throws RepositoryException
683      */
684     protected Content waitForLock(HttpServletRequest request) throws ExchangeException, RepositoryException {
685         int retries = getUnlockRetries();
686         long retryWait = getRetryWait() * 1000;
687         Content parentOnActivationNodeOtherwise = null;
688         Content rootLock = null;
689         Content activatedNode;
690         try {
691             activatedNode = this.getHierarchyManager(request).getContentByUUID(this.getUUID(request));
692         } catch (RepositoryException e) {
693             // first time activation
694             activatedNode = null;
695         }
696         try {
697             parentOnActivationNodeOtherwise = this.getNode(request);
698             rootLock = getRootLockOrNull(parentOnActivationNodeOtherwise);
699 
700             while (isLocked(parentOnActivationNodeOtherwise, rootLock, activatedNode) && retries > 0) {
701                 log.info("Content " + parentOnActivationNodeOtherwise.getHandle() + " is locked. Will retry " + retries + " more times.");
702                 try {
703                     Thread.sleep(retryWait);
704                 } catch (InterruptedException e) {
705                     // Restore the interrupted status
706                     Thread.currentThread().interrupt();
707                 }
708                 retries--;
709                 parentOnActivationNodeOtherwise = this.getNode(request);
710                 rootLock = getRootLockOrNull(parentOnActivationNodeOtherwise);
711             }
712 
713             if (isLocked(parentOnActivationNodeOtherwise, rootLock, activatedNode)) {
714                 throw new ExchangeException("Content " + parentOnActivationNodeOtherwise.getHandle() + " is locked while activating " + request.getHeader(BaseSyndicatorImpl.NODE_UUID)
715                         + ". This most likely means that content have been at the same time activated by some other user. Please try again and if problem persists contact administrator.");
716             }
717         } catch (ItemNotFoundException e) {
718             // - when deleting new piece of content on the author and mgnl tries to deactivate it on public automatically
719             log.debug("Attempt to lock non existing content {} during (de)activation.", getUUID(request));
720         } catch (PathNotFoundException e) {
721             // - when attempting to activate the content for which parent content have not been yet activated
722             log.debug("Attempt to lock non existing content {}:{} during (de)activation.",getHierarchyManager(request).getName(), getParentPath(request));
723         }
724         return parentOnActivationNodeOtherwise;
725     }
726 
727     private boolean isLocked(Content parentContent, Content rootLock, Content activatedNode) throws RepositoryException {
728         return (rootLock != null && rootLock.isLocked()) || parentContent.isLocked() || (activatedNode != null && activatedNode.isLocked());
729     }
730 
731     private Content getRootLockOrNull(Content content) throws ExchangeException {
732         try {
733             if (content != null && "/".equals(content.getHandle())) {
734                 return ContentUtil.getOrCreateContent(getBackupHierarchyManager().getRoot(), content.getHierarchyManager().getName() + "-" + ROOT_LOCK_NAME, ItemType.CONTENT, true);
735             }
736         } catch (RepositoryException e) {
737             throw new ExchangeException("Failed to obtain root lock.", e);
738         }
739         return null;
740     }
741 
742     protected HierarchyManager getBackupHierarchyManager() {
743         return MgnlContext.getSystemContext().getHierarchyManager(SYSTEM_REPO);
744     }
745 
746     /**
747      * Returns parent node of the activated node in case of activation or the node itself in case of
748      * deactivation.
749      */
750     protected Content getNode(HttpServletRequest request) throws ExchangeException, RepositoryException {
751         if (request.getHeader(BaseSyndicatorImpl.PARENT_PATH) != null) {
752             String parentPath = this.getParentPath(request);
753             log.debug("parent path:" + parentPath);
754             return this.getHierarchyManager(request).getContent(parentPath);
755         } else if (!StringUtils.isEmpty(getUUID(request))){
756             final String uuid = getUUID(request);
757             log.debug("node uuid:" + uuid);
758             return this.getHierarchyManager(request).getContentByUUID(uuid);
759         }
760         // 3.0 protocol
761         else {
762             log.debug("path: {}", request.getHeader(BaseSyndicatorImpl.PATH));
763             return this.getHierarchyManager(request).getContent(request.getHeader(BaseSyndicatorImpl.PATH));
764         }
765     }
766 
767     protected String getParentPath(HttpServletRequest request) {
768         String parentPath = request.getHeader(BaseSyndicatorImpl.PARENT_PATH);
769         if (StringUtils.isNotEmpty(parentPath)) {
770             return parentPath;
771         }
772         return "";
773     }
774 
775     protected String getUUID(HttpServletRequest request) {
776         final String uuid = request.getHeader(BaseSyndicatorImpl.NODE_UUID);
777         if (StringUtils.isNotEmpty(uuid)) {
778             return uuid;
779         }
780         return "";
781     }
782 
783     /**
784      * Method handling issuing of the lock. Magnolia needs to lock not only the updated node, but
785      * also the parent node in case activated node is just being moved within same hierarchy or in
786      * case of deactivation. Locking of parent is not always possible (e.g. root node is not
787      * lockable). This method treats all special cases and can be overridden in case even more
788      * specialized handling is needed.
789      *
790      * @param node Activated node.
791      * @param parent Parent of activated node.
792      */
793     protected void lock(Content node, Content parent) throws ExchangeException, RepositoryException {
794         try {
795             if (parent != null) {
796                 if (parent.getHandle().equals("/")) {
797                     // root is not lockable so we lock this lock instead
798                     Content rootLock = getRootLockOrNull(parent);
799                     rootLock.lock(false, true);
800                     logLockStatus(rootLock, true);
801                     // and we also lock the node itself if it exists
802                     if (node != null) {
803                         node.lock(true, true);
804                         logLockStatus(node, true);
805                     }
806                 } else {
807                     parent.lock(true, true);
808                     logLockStatus(parent, true);
809                 }
810             }
811         } catch (LockException le) {
812             // either repository does not support locking OR this node never locked
813             log.error(le.getMessage(), le);
814         }
815 
816     }
817 
818     private void logLockStatus(Content content, boolean isLock) throws RepositoryException {
819         if (log.isDebugEnabled()) {
820             // this log obtains too much data to processed all the time when not enabled
821             log.debug("{} {} {}locked {}:{}", new Object[] { content.getWorkspace().getSession(), isLock ^ content.isLocked() ? "DIDN'T" : "DID", isLock ? "" : "un", content.getWorkspace().getName(), content.getHandle() });
822         }
823     }
824 
825 
826 }