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.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.ByteArrayOutputStream;
66  import java.io.File;
67  import java.io.FileInputStream;
68  import java.io.FileOutputStream;
69  import java.io.IOException;
70  import java.io.OutputStream;
71  import java.util.Iterator;
72  import java.util.List;
73  import java.util.zip.GZIPOutputStream;
74  
75  import javax.jcr.Node;
76  import javax.jcr.RepositoryException;
77  import javax.jcr.Session;
78  
79  import org.apache.commons.io.IOUtils;
80  import org.apache.commons.io.output.TeeOutputStream;
81  import org.apache.xml.serialize.OutputFormat;
82  import org.apache.xml.serialize.XMLSerializer;
83  import org.jdom.Document;
84  import org.jdom.Element;
85  import org.jdom.output.XMLOutputter;
86  import org.slf4j.Logger;
87  import org.slf4j.LoggerFactory;
88  import org.xml.sax.InputSource;
89  import org.xml.sax.SAXException;
90  import org.xml.sax.XMLReader;
91  import org.xml.sax.helpers.XMLReaderFactory;
92  
93  /**
94   * Class responsible for collecting all the resources for activation of content.
95   * 
96   * @version $Id$
97   * 
98   */
99  public class ResourceCollector {
100 
101     private final ByteArrayOutputStream md5;
102 
103     public ResourceCollector() throws ExchangeException {
104         md5 = new ByteArrayOutputStream();
105     }
106 
107     private static final Logger log = LoggerFactory.getLogger(ResourceCollector.class);
108 
109     /**
110      * Collects all information about activated content and its children (those that are set to be activated with the parent by filter rules).
111      * 
112      * @param contentFilterRule
113      * @param repositoryName
114      * @param workspaceName
115      * @param parent
116      * 
117      * @throws Exception
118      */
119     protected ActivationContent collect(Content node, List<String> orderBefore, String parent, String workspaceName, String repositoryName, Rule contentFilterRule) throws Exception {
120         md5.reset();
121 
122         Content.ContentFilter contentFilter = new RuleBasedContentFilter(contentFilterRule);
123 
124         // make sure resource file is unique
125         File resourceFile = File.createTempFile("resources", ".xml", Path.getTempDirectory());
126 
127         ActivationContent activationContent = new ActivationContent();
128         // add global properties true for this path/hierarchy
129         activationContent.addProperty(PARENT_PATH, parent);
130         activationContent.addProperty(WORKSPACE_NAME, workspaceName);
131         activationContent.addProperty(REPOSITORY_NAME, repositoryName);
132         activationContent.addProperty(RESOURCE_MAPPING_FILE, resourceFile.getName());// "resources.xml");
133         activationContent.addProperty(ACTION, ACTIVATE);
134         activationContent.addProperty(CONTENT_FILTER_RULE, contentFilterRule.toString());
135         activationContent.addProperty(NODE_UUID, node.getUUID());
136         activationContent.addProperty(UTF8_STATUS, SystemProperty.getProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED));
137 
138         Document document = new Document();
139         Element root = new Element(RESOURCE_MAPPING_ROOT_ELEMENT);
140         document.setRootElement(root);
141         // collect exact order of this node within its same nodeType siblings
142         addOrderingInfo(root, orderBefore);
143 
144         this.addResources(root, node.getWorkspace().getSession(), node, contentFilter, activationContent);
145         XMLOutputter outputter = new XMLOutputter();        
146         
147 
148         outputter.output(document, new TeeOutputStream(new FileOutputStream(resourceFile), md5));
149         // add resource file to the list
150         activationContent.addFile(resourceFile.getName(), resourceFile);
151         // add signature of the resource file itself
152         activationContent.addProperty(RESOURCE_MAPPING_MD_ATTRIBUTE, SecurityUtil.getMD5Hex(md5.toByteArray()));
153         md5.reset();
154 
155         // add deletion info
156         activationContent.addProperty(ItemType.DELETED_NODE_MIXIN, "" + node.hasMixin(ItemType.DELETED_NODE_MIXIN));
157 
158         return activationContent;
159     }
160 
161     /**
162      * Adds ordering information to the resource mapping file.
163      * 
164      * @param root
165      *            element of the resource file under which ordering info must be added
166      * @param orderBefore
167      */
168     protected void addOrderingInfo(Element root, List<String> orderBefore) {
169         // do not use magnolia Content class since these objects are only meant for a single use to read UUID
170         Element siblingRoot = new Element(SIBLINGS_ROOT_ELEMENT);
171         root.addContent(siblingRoot);
172         if (orderBefore == null) {
173             return;
174         }
175         Iterator<String> siblings = orderBefore.iterator();
176         while (siblings.hasNext()) {
177             String uuid = siblings.next();
178             Element e = new Element(SIBLINGS_ELEMENT);
179             e.setAttribute(SIBLING_UUID, uuid);
180             siblingRoot.addContent(e);
181         }
182     }
183 
184     protected void addResources(Element resourceElement, Session session, final Content content, Content.ContentFilter filter, ActivationContent activationContent) throws IOException, RepositoryException, SAXException, Exception {
185         final String workspaceName = content.getWorkspace().getName();
186         
187         log.debug("Preparing content {}:{} for publishing.", new String[] { workspaceName, content.getHandle() });
188         final String uuid = content.getUUID();
189 
190         File file = File.createTempFile("exchange_" + uuid, ".xml.gz", Path.getTempDirectory());
191         
192         OutputStream outputStream = new TeeOutputStream(new GZIPOutputStream(new FileOutputStream(file)), md5);
193 
194         // 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
195         if (content.isNodeType("nt:frozenNode") || workspaceName.equals(RepositoryConstants.VERSION_STORE)) {
196             XMLReader elementfilter = new FrozenElementFilter(XMLReaderFactory
197                     .createXMLReader(org.apache.xerces.parsers.SAXParser.class.getName()));
198             ((FrozenElementFilter) elementfilter).setNodeName(content.getName());
199             /**
200              * nt:file node type has mandatory sub nodes
201              */
202             boolean noRecurse = !content.isNodeType(ItemType.NT_FILE);
203             exportAndParse(session, content, elementfilter, outputStream, noRecurse);
204         } else {
205             /**
206              * nt:file node type has mandatory sub nodes
207              */
208             if (content.isNodeType(ItemType.NT_FILE)) {
209                 session.exportSystemView(content.getJCRNode().getPath(), outputStream, false, false);
210             } else {
211                 session.exportSystemView(content.getJCRNode().getPath(), outputStream, false, true);
212             }
213         }
214 
215         IOUtils.closeQuietly(outputStream);
216         // add file entry in mapping.xml
217         Element element = new Element(RESOURCE_MAPPING_FILE_ELEMENT);
218         element.setAttribute(RESOURCE_MAPPING_NAME_ATTRIBUTE, content.getName());
219         element.setAttribute(RESOURCE_MAPPING_UUID_ATTRIBUTE, uuid);
220         element.setAttribute(RESOURCE_MAPPING_ID_ATTRIBUTE, file.getName());
221         
222         element.setAttribute(RESOURCE_MAPPING_MD_ATTRIBUTE, SecurityUtil.getMD5Hex(md5.toByteArray()));
223         md5.reset();
224         
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 }