View Javadoc

1   /**
2    * This file Copyright (c) 2011 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.exchangesimple;
35  
36  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.ACTION;
37  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.ACTIVATE;
38  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.CONTENT_FILTER_RULE;
39  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.NODE_UUID;
40  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.PARENT_PATH;
41  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.REPOSITORY_NAME;
42  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_FILE;
43  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT;
44  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE;
45  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_MD_ATTRIBUTE;
46  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE;
47  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_ROOT_ELEMENT;
48  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE;
49  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.SIBLINGS_ELEMENT;
50  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.SIBLINGS_ROOT_ELEMENT;
51  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.SIBLING_UUID;
52  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.UTF8_STATUS;
53  import static info.magnolia.module.exchangesimple.BaseSyndicatorImpl.WORKSPACE_NAME;
54  import info.magnolia.cms.core.Content;
55  import info.magnolia.cms.core.ItemType;
56  import info.magnolia.cms.core.Path;
57  import info.magnolia.cms.core.SystemProperty;
58  import info.magnolia.cms.exchange.ExchangeException;
59  import info.magnolia.cms.security.SecurityUtil;
60  import info.magnolia.cms.util.Rule;
61  import info.magnolia.cms.util.RuleBasedContentFilter;
62  import info.magnolia.jcr.wrapper.DelegateNodeWrapper;
63  import info.magnolia.repository.RepositoryConstants;
64  
65  import java.io.File;
66  import java.io.FileInputStream;
67  import java.io.FileOutputStream;
68  import java.io.IOException;
69  import java.io.OutputStream;
70  import java.security.DigestOutputStream;
71  import java.security.MessageDigest;
72  import java.security.NoSuchAlgorithmException;
73  import java.util.Iterator;
74  import java.util.List;
75  import java.util.zip.GZIPOutputStream;
76  
77  import javax.jcr.Node;
78  import javax.jcr.RepositoryException;
79  import javax.jcr.Session;
80  
81  import org.apache.commons.io.IOUtils;
82  import org.apache.xml.serialize.OutputFormat;
83  import org.apache.xml.serialize.XMLSerializer;
84  import org.jdom.Document;
85  import org.jdom.Element;
86  import org.jdom.output.XMLOutputter;
87  import org.slf4j.Logger;
88  import org.slf4j.LoggerFactory;
89  import org.xml.sax.InputSource;
90  import org.xml.sax.SAXException;
91  import org.xml.sax.XMLReader;
92  import org.xml.sax.helpers.XMLReaderFactory;
93  
94  /**
95   * Class responsible for collecting all the resources for activation of content.
96   * 
97   * @version $Id$
98   * 
99   */
100 public class ResourceCollector {
101 
102     private final MessageDigest md;
103 
104     public ResourceCollector() throws ExchangeException {
105         try {
106             md = MessageDigest.getInstance("MD5");
107         } catch (NoSuchAlgorithmException e) {
108             throw new ExchangeException("In order to proceed with activation please run Magnolia CMS using Java version with MD5 support.", e);
109         }
110     }
111 
112     private static final Logger log = LoggerFactory.getLogger(ResourceCollector.class);
113 
114     /**
115      * Collects all information about activated content and its children (those that are set to be activated with the parent by filter rules).
116      * 
117      * @param contentFilterRule
118      * @param repositoryName
119      * @param workspaceName
120      * @param parent
121      * 
122      * @throws Exception
123      */
124     protected ActivationContent collect(Content node, List<String> orderBefore, String parent, String workspaceName, String repositoryName, Rule contentFilterRule) throws Exception {
125 
126         // just to be sure, there should be no reason for md to not be reset (reset is called automatically after digest call)
127         md.reset();
128 
129         Content.ContentFilter contentFilter = new RuleBasedContentFilter(contentFilterRule);
130 
131         // make sure resource file is unique
132         File resourceFile = File.createTempFile("resources", ".xml", Path.getTempDirectory());
133 
134         ActivationContent activationContent = new ActivationContent();
135         // add global properties true for this path/hierarchy
136         activationContent.addProperty(PARENT_PATH, parent);
137         activationContent.addProperty(WORKSPACE_NAME, workspaceName);
138         activationContent.addProperty(REPOSITORY_NAME, repositoryName);
139         activationContent.addProperty(RESOURCE_MAPPING_FILE, resourceFile.getName());// "resources.xml");
140         activationContent.addProperty(ACTION, ACTIVATE);
141         activationContent.addProperty(CONTENT_FILTER_RULE, contentFilterRule.toString());
142         activationContent.addProperty(NODE_UUID, node.getUUID());
143         activationContent.addProperty(UTF8_STATUS, SystemProperty.getProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED));
144 
145         Document document = new Document();
146         Element root = new Element(RESOURCE_MAPPING_ROOT_ELEMENT);
147         document.setRootElement(root);
148         // collect exact order of this node within its same nodeType siblings
149         addOrderingInfo(root, orderBefore);
150 
151         this.addResources(root, node.getWorkspace().getSession(), node, contentFilter, activationContent);
152         XMLOutputter outputter = new XMLOutputter();
153 
154         outputter.output(document, new DigestOutputStream(new FileOutputStream(resourceFile), md));
155         // add resource file to the list
156         activationContent.addFile(resourceFile.getName(), resourceFile);
157         // add signature of the resource file itself
158         activationContent.addProperty(RESOURCE_MAPPING_MD_ATTRIBUTE, SecurityUtil.byteArrayToHex(md.digest()));
159 
160         // add deletion info
161         activationContent.addProperty(ItemType.DELETED_NODE_MIXIN, "" + node.hasMixin(ItemType.DELETED_NODE_MIXIN));
162 
163         return activationContent;
164     }
165 
166     /**
167      * Adds ordering information to the resource mapping file.
168      * 
169      * @param root
170      *            element of the resource file under which ordering info must be added
171      * @param orderBefore
172      */
173     protected void addOrderingInfo(Element root, List<String> orderBefore) {
174         // do not use magnolia Content class since these objects are only meant for a single use to read UUID
175         Element siblingRoot = new Element(SIBLINGS_ROOT_ELEMENT);
176         root.addContent(siblingRoot);
177         if (orderBefore == null) {
178             return;
179         }
180         Iterator<String> siblings = orderBefore.iterator();
181         while (siblings.hasNext()) {
182             String uuid = siblings.next();
183             Element e = new Element(SIBLINGS_ELEMENT);
184             e.setAttribute(SIBLING_UUID, uuid);
185             siblingRoot.addContent(e);
186         }
187     }
188 
189     protected void addResources(Element resourceElement, Session session, final Content content, Content.ContentFilter filter, ActivationContent activationContent) throws IOException, RepositoryException, SAXException, Exception {
190         final String workspaceName = content.getWorkspace().getName();
191         log.debug("Preparing content {}:{} for publishing.", new String[] { workspaceName, content.getHandle() });
192         final String uuid = content.getUUID();
193 
194         File file = File.createTempFile("exchange_" + uuid, ".xml.gz", Path.getTempDirectory());
195         OutputStream outputStream = new DigestOutputStream(new GZIPOutputStream(new FileOutputStream(file)), md);
196 
197         // TODO: remove the second check. It should not be necessary. The only safe way to identify the versioned node is by looking at its type since the type is mandated by spec. and the frozen nodes is what the filter below removes anyway
198         if (content.isNodeType("nt:frozenNode") || workspaceName.equals(RepositoryConstants.VERSION_STORE)) {
199             XMLReader elementfilter = new FrozenElementFilter(XMLReaderFactory
200                     .createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName()));
201             ((FrozenElementFilter) elementfilter).setNodeName(content.getName());
202             /**
203              * nt:file node type has mandatory sub nodes
204              */
205             boolean noRecurse = !content.isNodeType(ItemType.NT_FILE);
206             exportAndParse(session, content, elementfilter, outputStream, noRecurse);
207         } else {
208             /**
209              * nt:file node type has mandatory sub nodes
210              */
211             if (content.isNodeType(ItemType.NT_FILE)) {
212                 session.exportSystemView(content.getJCRNode().getPath(), outputStream, false, false);
213             } else {
214                 session.exportSystemView(content.getJCRNode().getPath(), outputStream, false, true);
215             }
216         }
217 
218         IOUtils.closeQuietly(outputStream);
219         // add file entry in mapping.xml
220         Element element = new Element(RESOURCE_MAPPING_FILE_ELEMENT);
221         element.setAttribute(RESOURCE_MAPPING_NAME_ATTRIBUTE, content.getName());
222         element.setAttribute(RESOURCE_MAPPING_UUID_ATTRIBUTE, uuid);
223         element.setAttribute(RESOURCE_MAPPING_ID_ATTRIBUTE, file.getName());
224         element.setAttribute(RESOURCE_MAPPING_MD_ATTRIBUTE, SecurityUtil.byteArrayToHex(md.digest()));
225         resourceElement.addContent(element);
226         // add this file element as resource in activation content
227         activationContent.addFile(file.getName(), file);
228 
229         Iterator<Content> children = content.getChildren(filter).iterator();
230         while (children.hasNext()) {
231             Content child = children.next();
232             this.addResources(element, session, child, filter, activationContent);
233         }
234     }
235 
236     /**
237      * Exports frozen resource. We can't export such resource directly, but need to filter out versioning information from the export.
238      */
239     protected void exportAndParse(Session session, Content content, XMLReader elementfilter, OutputStream os, boolean noRecurse) throws Exception {
240         File tempFile = File.createTempFile("Frozen_" + content.getName(), ".xml");
241         OutputStream tmpFileOutStream = null;
242         FileInputStream tmpFileInStream = null;
243         try {
244             tmpFileOutStream = new FileOutputStream(tempFile);
245             // has to get path via unwrapped JCR node since all else would have returned path to the base
246             Node node = content.getJCRNode();
247             if (node instanceof DelegateNodeWrapper) {
248                 node = ((DelegateNodeWrapper) node).getWrappedNode();
249             }
250             session.exportSystemView(node.getPath(), tmpFileOutStream, false, noRecurse);
251             tmpFileOutStream.flush();
252             tmpFileOutStream.close();
253 
254             OutputFormat outputFormat = new OutputFormat();
255             outputFormat.setPreserveSpace(false);
256 
257             tmpFileInStream = new FileInputStream(tempFile);
258             elementfilter.setContentHandler(new XMLSerializer(os, outputFormat));
259             elementfilter.parse(new InputSource(tmpFileInStream));
260             tmpFileInStream.close();
261         } catch (Throwable t) {
262             log.error("Failed to parse XML using FrozenElementFilter", t);
263             throw new Exception(t);
264         } finally {
265             IOUtils.closeQuietly(tmpFileInStream);
266             IOUtils.closeQuietly(tmpFileOutStream);
267             tempFile.delete();
268         }
269     }
270 
271     // @Inject
272     // public void setProperties(MagnoliaConfigurationProperties props) {
273     // this.properties = props;
274     // }
275 }