View Javadoc

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