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