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.repository.RepositoryConstants;
63  
64  import java.io.File;
65  import java.io.FileInputStream;
66  import java.io.FileOutputStream;
67  import java.io.IOException;
68  import java.io.OutputStream;
69  import java.security.DigestOutputStream;
70  import java.security.MessageDigest;
71  import java.security.NoSuchAlgorithmException;
72  import java.util.Iterator;
73  import java.util.List;
74  import java.util.zip.GZIPOutputStream;
75  
76  import javax.jcr.RepositoryException;
77  import javax.jcr.Session;
78  
79  import org.apache.commons.io.IOUtils;
80  import org.apache.xml.serialize.OutputFormat;
81  import org.apache.xml.serialize.XMLSerializer;
82  import org.jdom.Document;
83  import org.jdom.Element;
84  import org.jdom.output.XMLOutputter;
85  import org.slf4j.Logger;
86  import org.slf4j.LoggerFactory;
87  import org.xml.sax.InputSource;
88  import org.xml.sax.SAXException;
89  import org.xml.sax.XMLReader;
90  import org.xml.sax.helpers.XMLReaderFactory;
91  
92  /**
93   * Class responsible for collecting all the resources for activation of content.
94   * 
95   * @version $Id$
96   * 
97   */
98  public class ResourceCollector {
99  
100     private final MessageDigest md;
101 
102     public ResourceCollector() throws ExchangeException {
103         try {
104             md = MessageDigest.getInstance("MD5");
105         } catch (NoSuchAlgorithmException e) {
106             throw new ExchangeException("In order to proceed with activation please run Magnolia CMS using Java version with MD5 support.", e);
107         }
108     }
109 
110     private static final Logger log = LoggerFactory.getLogger(ResourceCollector.class);
111 
112     /**
113      * Collects all information about activated content and its children (those that are set to be activated with the parent by filter rules).
114      * 
115      * @param contentFilterRule
116      * @param repositoryName
117      * @param workspaceName
118      * @param parent
119      * 
120      * @throws Exception
121      */
122     protected ActivationContent collect(Content node, List<String> orderBefore, String parent, String workspaceName, String repositoryName, Rule contentFilterRule) throws Exception {
123 
124         // just to be sure, there should be no reason for md to not be reset (reset is called automatically after digest call)
125         md.reset();
126 
127         Content.ContentFilter contentFilter = new RuleBasedContentFilter(contentFilterRule);
128 
129         // make sure resource file is unique
130         File resourceFile = File.createTempFile("resources", ".xml", Path.getTempDirectory());
131 
132         ActivationContent activationContent = new ActivationContent();
133         // add global properties true for this path/hierarchy
134         activationContent.addProperty(PARENT_PATH, parent);
135         activationContent.addProperty(WORKSPACE_NAME, workspaceName);
136         activationContent.addProperty(REPOSITORY_NAME, repositoryName);
137         activationContent.addProperty(RESOURCE_MAPPING_FILE, resourceFile.getName());// "resources.xml");
138         activationContent.addProperty(ACTION, ACTIVATE);
139         activationContent.addProperty(CONTENT_FILTER_RULE, contentFilterRule.toString());
140         activationContent.addProperty(NODE_UUID, node.getUUID());
141         activationContent.addProperty(UTF8_STATUS, SystemProperty.getProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED));
142 
143         Document document = new Document();
144         Element root = new Element(RESOURCE_MAPPING_ROOT_ELEMENT);
145         document.setRootElement(root);
146         // collect exact order of this node within its same nodeType siblings
147         addOrderingInfo(root, orderBefore);
148 
149         this.addResources(root, node.getWorkspace().getSession(), node, contentFilter, activationContent);
150         XMLOutputter outputter = new XMLOutputter();
151 
152         outputter.output(document, new DigestOutputStream(new FileOutputStream(resourceFile), md));
153         // add resource file to the list
154         activationContent.addFile(resourceFile.getName(), resourceFile);
155         // add signature of the resource file itself
156         activationContent.addProperty(RESOURCE_MAPPING_MD_ATTRIBUTE, SecurityUtil.byteArrayToHex(md.digest()));
157 
158         // add deletion info
159         activationContent.addProperty(ItemType.DELETED_NODE_MIXIN, "" + node.hasMixin(ItemType.DELETED_NODE_MIXIN));
160 
161         return activationContent;
162     }
163 
164     /**
165      * Adds ordering information to the resource mapping file.
166      * 
167      * @param root
168      *            element of the resource file under which ordering info must be added
169      * @param orderBefore
170      */
171     protected void addOrderingInfo(Element root, List<String> orderBefore) {
172         // do not use magnolia Content class since these objects are only meant for a single use to read UUID
173         Element siblingRoot = new Element(SIBLINGS_ROOT_ELEMENT);
174         root.addContent(siblingRoot);
175         if (orderBefore == null) {
176             return;
177         }
178         Iterator<String> siblings = orderBefore.iterator();
179         while (siblings.hasNext()) {
180             String uuid = siblings.next();
181             Element e = new Element(SIBLINGS_ELEMENT);
182             e.setAttribute(SIBLING_UUID, uuid);
183             siblingRoot.addContent(e);
184         }
185     }
186 
187     protected void addResources(Element resourceElement, Session session, final Content content, Content.ContentFilter filter, ActivationContent activationContent) throws IOException, RepositoryException, SAXException, Exception {
188         final String workspaceName = content.getWorkspace().getName();
189         log.debug("Preparing content {}:{} for publishing.", new String[] { workspaceName, content.getHandle() });
190         final String uuid = content.getUUID();
191 
192         File file = File.createTempFile("exchange_" + uuid, ".xml.gz", Path.getTempDirectory());
193         OutputStream outputStream = new DigestOutputStream(new GZIPOutputStream(new FileOutputStream(file)), md);
194 
195         // 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
196         if (content.isNodeType("nt:frozenNode") || workspaceName.equals(RepositoryConstants.VERSION_STORE)) {
197             XMLReader elementfilter = new FrozenElementFilter(XMLReaderFactory
198                     .createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName()));
199             ((FrozenElementFilter) elementfilter).setNodeName(content.getName());
200             /**
201              * nt:file node type has mandatory sub nodes
202              */
203             boolean noRecurse = !content.isNodeType(ItemType.NT_FILE);
204             exportAndParse(session, content, elementfilter, outputStream, noRecurse);
205         } else {
206             /**
207              * nt:file node type has mandatory sub nodes
208              */
209             if (content.isNodeType(ItemType.NT_FILE)) {
210                 session.exportSystemView(content.getJCRNode().getPath(), outputStream, false, false);
211             } else {
212                 session.exportSystemView(content.getJCRNode().getPath(), outputStream, false, true);
213             }
214         }
215 
216         IOUtils.closeQuietly(outputStream);
217         // add file entry in mapping.xml
218         Element element = new Element(RESOURCE_MAPPING_FILE_ELEMENT);
219         element.setAttribute(RESOURCE_MAPPING_NAME_ATTRIBUTE, content.getName());
220         element.setAttribute(RESOURCE_MAPPING_UUID_ATTRIBUTE, uuid);
221         element.setAttribute(RESOURCE_MAPPING_ID_ATTRIBUTE, file.getName());
222         element.setAttribute(RESOURCE_MAPPING_MD_ATTRIBUTE, SecurityUtil.byteArrayToHex(md.digest()));
223         resourceElement.addContent(element);
224         // add this file element as resource in activation content
225         activationContent.addFile(file.getName(), file);
226 
227         Iterator<Content> children = content.getChildren(filter).iterator();
228         while (children.hasNext()) {
229             Content child = children.next();
230             this.addResources(element, session, child, filter, activationContent);
231         }
232     }
233 
234     /**
235      * Exports frozen resource. We can't export such resource directly, but need to filter out versioning information from the export.
236      */
237     protected void exportAndParse(Session session, Content content, XMLReader elementfilter, OutputStream os, boolean noRecurse) throws Exception {
238         File tempFile = File.createTempFile("Frozen_" + content.getName(), ".xml");
239         OutputStream tmpFileOutStream = null;
240         FileInputStream tmpFileInStream = null;
241         try {
242             tmpFileOutStream = new FileOutputStream(tempFile);
243             // has to get path via JCR node since if "content" is of type ContentVersion, getHandle() call would have returned path to the base
244             session.exportSystemView(content.getJCRNode().getPath(), tmpFileOutStream, false, noRecurse);
245             tmpFileOutStream.flush();
246             tmpFileOutStream.close();
247 
248             OutputFormat outputFormat = new OutputFormat();
249             outputFormat.setPreserveSpace(false);
250 
251             tmpFileInStream = new FileInputStream(tempFile);
252             elementfilter.setContentHandler(new XMLSerializer(os, outputFormat));
253             elementfilter.parse(new InputSource(tmpFileInStream));
254             tmpFileInStream.close();
255         } catch (Throwable t) {
256             log.error("Failed to parse XML using FrozenElementFilter", t);
257             throw new Exception(t);
258         } finally {
259             IOUtils.closeQuietly(tmpFileInStream);
260             IOUtils.closeQuietly(tmpFileOutStream);
261             tempFile.delete();
262         }
263     }
264 
265     // @Inject
266     // public void setProperties(MagnoliaConfigurationProperties props) {
267     // this.properties = props;
268     // }
269 }