001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.xml.containerpage;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsPropertyDefinition;
033import org.opencms.file.CmsResource;
034import org.opencms.file.history.I_CmsHistoryResource;
035import org.opencms.i18n.CmsEncoder;
036import org.opencms.loader.CmsLoaderException;
037import org.opencms.main.CmsException;
038import org.opencms.main.OpenCms;
039import org.opencms.xml.CmsXmlContentDefinition;
040import org.opencms.xml.CmsXmlEntityResolver;
041import org.opencms.xml.CmsXmlException;
042import org.opencms.xml.CmsXmlUtils;
043import org.opencms.xml.content.Messages;
044
045import java.io.UnsupportedEncodingException;
046import java.util.Locale;
047
048import javax.servlet.ServletRequest;
049
050import org.dom4j.Document;
051import org.dom4j.DocumentHelper;
052import org.xml.sax.EntityResolver;
053
054/**
055 * Provides factory methods to unmarshal (read) an group container object.<p>
056 *
057 * @since 8.0.0
058 */
059public final class CmsXmlGroupContainerFactory {
060
061    /**
062     * No instances of this class should be created.<p>
063     */
064    private CmsXmlGroupContainerFactory() {
065
066        // noop
067    }
068
069    /**
070     * Create a new instance of an group container based on the given default content,
071     * that will have all language nodes of the default content and ensures the presence of the given locale.<p>
072     *
073     * The given encoding is used when marshalling the XML again later.<p>
074     *
075     * @param cms the current users OpenCms content
076     * @param locale the locale to generate the default content for
077     * @param modelUri the absolute path to the group container file acting as model
078     *
079     * @throws CmsException in case the model file is not found or not valid
080     *
081     * @return the created group container
082     */
083    public static CmsXmlGroupContainer createDocument(CmsObject cms, Locale locale, String modelUri)
084    throws CmsException {
085
086        // create the XML content
087        CmsXmlGroupContainer content = new CmsXmlGroupContainer(cms, locale, modelUri);
088        // call prepare for use content handler and return the result
089        return (CmsXmlGroupContainer)content.getHandler().prepareForUse(cms, content);
090    }
091
092    /**
093     * Create a new instance of a group container based on the given content definition,
094     * that will have one language node for the given locale all initialized with default values.<p>
095     *
096     * The given encoding is used when marshalling the XML again later.<p>
097     *
098     * @param cms the current users OpenCms content
099     * @param locale the locale to generate the default content for
100     * @param encoding the encoding to use when marshalling the XML content later
101     * @param contentDefinition the content definition to create the content for
102     *
103     * @return the created group container
104     */
105    public static CmsXmlGroupContainer createDocument(
106        CmsObject cms,
107        Locale locale,
108        String encoding,
109        CmsXmlContentDefinition contentDefinition) {
110
111        // create the XML content
112        CmsXmlGroupContainer content = new CmsXmlGroupContainer(cms, locale, encoding, contentDefinition);
113        // call prepare for use content handler and return the result
114        return (CmsXmlGroupContainer)content.getHandler().prepareForUse(cms, content);
115    }
116
117    /**
118     * Factory method to unmarshal (generate) a group container instance from a byte array
119     * that contains XML data.<p>
120     *
121     * When unmarshalling, the encoding is read directly from the XML header of the byte array.
122     * The given encoding is used only when marshalling the XML again later.<p>
123     *
124     * <b>Warning:</b><br/>
125     * This method does not support requested historic versions, it always loads the
126     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
127     * for history support.<p>
128     *
129     * @param cms the cms context
130     * @param xmlData the XML data in a byte array
131     * @param encoding the encoding to use when marshalling the XML content later
132     * @param resolver the XML entitiy resolver to use
133     *
134     * @return a group container instance unmarshalled from the byte array
135     *
136     * @throws CmsXmlException if something goes wrong
137     */
138    public static CmsXmlGroupContainer unmarshal(
139        CmsObject cms,
140        byte[] xmlData,
141        String encoding,
142        EntityResolver resolver) throws CmsXmlException {
143
144        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
145    }
146
147    /**
148     * Factory method to unmarshal (read) a group container instance from a OpenCms VFS file
149     * that contains XML data.<p>
150     *
151     * <b>Warning:</b><br/>
152     * This method does not support requested historic versions, it always loads the
153     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
154     * for history support.<p>
155     *
156     * @param cms the current cms object
157     * @param file the file with the XML data to unmarshal
158     *
159     * @return a group container instance unmarshalled from the provided file
160     *
161     * @throws CmsXmlException if something goes wrong
162     */
163    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsFile file) throws CmsXmlException {
164
165        return unmarshal(cms, file, true);
166    }
167
168    /**
169     * Factory method to unmarshal (read) a group container instance from a OpenCms VFS file
170     * that contains XML data, using wither the encoding set
171     * in the XML file header, or the encoding set in the VFS file property.<p>
172     *
173     * If you are not sure about the implications of the encoding issues,
174     * use {@link #unmarshal(CmsObject, CmsFile)} instead.<p>
175     *
176     * <b>Warning:</b><br/>
177     * This method does not support requested historic versions, it always loads the
178     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
179     * for history support.<p>
180     *
181     * @param cms the current cms object
182     * @param file the file with the XML data to unmarshal
183     * @param keepEncoding if <code>true</code>, the encoding specified in the XML header is used,
184     *    otherwise the encoding from the VFS file property is used
185     *
186     * @return a group container instance unmarshalled from the provided file
187     *
188     * @throws CmsXmlException if something goes wrong
189     */
190    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsFile file, boolean keepEncoding)
191    throws CmsXmlException {
192
193        // check the cache
194        CmsXmlGroupContainer content = getCache(cms, file, keepEncoding);
195        if (content != null) {
196            return content;
197        }
198
199        // not found in cache, read as normally
200        byte[] contentBytes = file.getContents();
201        String filename = cms.getSitePath(file);
202
203        String encoding = null;
204        try {
205            encoding = cms.readPropertyObject(
206                filename,
207                CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
208                true).getValue();
209        } catch (CmsException e) {
210            // encoding will be null
211        }
212        if (encoding == null) {
213            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
214        } else {
215            encoding = CmsEncoder.lookupEncoding(encoding, null);
216            if (encoding == null) {
217                throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
218            }
219        }
220
221        if (contentBytes.length > 0) {
222            // content is initialized
223            if (keepEncoding) {
224                // use the encoding from the content
225                content = unmarshal(cms, contentBytes, encoding, new CmsXmlEntityResolver(cms));
226            } else {
227                // use the encoding from the file property
228                // this usually only triggered by a save operation
229                try {
230                    String contentStr = new String(contentBytes, encoding);
231                    content = unmarshal(cms, contentStr, encoding, new CmsXmlEntityResolver(cms));
232                } catch (UnsupportedEncodingException e) {
233                    // this will not happen since the encoding has already been validated
234                    throw new CmsXmlException(
235                        Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
236                }
237            }
238        } else {
239            // content is empty
240            content = new CmsXmlGroupContainer(
241                cms,
242                DocumentHelper.createDocument(),
243                encoding,
244                new CmsXmlEntityResolver(cms));
245        }
246
247        // set the file
248        content.setFile(file);
249        // call prepare for use content handler and return the result
250        CmsXmlGroupContainer xmlGroupContainer = (CmsXmlGroupContainer)content.getHandler().prepareForUse(cms, content);
251
252        // set the cache
253        setCache(cms, xmlGroupContainer, keepEncoding);
254
255        return xmlGroupContainer;
256    }
257
258    /**
259     * Factory method to unmarshal (read) a group container instance from a OpenCms VFS resource
260     * that contains XML data.<p>
261     *
262     * <b>Warning:</b><br/>
263     * This method does not support requested historic versions, it always loads the
264     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
265     * for history support.<p>
266     *
267     * @param cms the current cms object
268     * @param resource the resource with the XML data to unmarshal
269     *
270     * @return a group container instance unmarshalled from the provided resource
271     *
272     * @throws CmsException if something goes wrong
273     */
274    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsResource resource) throws CmsException {
275
276        // check the cache
277        CmsXmlGroupContainer content = getCache(cms, resource, true);
278        if (content != null) {
279            return content;
280        }
281
282        content = unmarshal(cms, cms.readFile(resource), true);
283
284        // set the cache
285        setCache(cms, content, true);
286
287        return content;
288    }
289
290    /**
291     * Factory method to unmarshal (read) a group container instance from
292     * a resource, using the request attributes as cache.<p>
293     *
294     * @param cms the current OpenCms context object
295     * @param resource the resource to unmarshal
296     * @param req the current request
297     *
298     * @return the unmarshaled xml content
299     *
300     * @throws CmsException in something goes wrong
301     * @throws CmsLoaderException if no loader for the given <code>resource</code> type ({@link CmsResource#getTypeId()}) is available
302     * @throws CmsXmlException if the given <code>resource</code> is not of type group container
303     */
304    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsResource resource, ServletRequest req)
305    throws CmsXmlException, CmsLoaderException, CmsException {
306
307        String rootPath = resource.getRootPath();
308
309        // try to get the requested content from the current request attribute
310        // this is also necessary for historic versions that have been loaded
311        CmsXmlGroupContainer content = (CmsXmlGroupContainer)req.getAttribute(rootPath);
312
313        if (content == null) {
314            // unmarshal XML structure from the file content
315            content = unmarshal(cms, resource);
316            // store the content as request attribute for future read requests
317            req.setAttribute(rootPath, content);
318        }
319
320        // return the result
321        return content;
322    }
323
324    /**
325     * Factory method to unmarshal (generate) a group container instance from a XML document.<p>
326     *
327     * The given encoding is used when marshalling the XML again later.<p>
328     *
329     * <b>Warning:</b><br/>
330     * This method does not support requested historic versions, it always loads the
331     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
332     * for history support.<p>
333     *
334     * @param cms the cms context, if <code>null</code> no link validation is performed
335     * @param document the XML document to generate the group container from
336     * @param encoding the encoding to use when marshalling the group container later
337     * @param resolver the XML entity resolver to use
338     *
339     * @return a group container instance unmarshalled from the String
340     */
341    public static CmsXmlGroupContainer unmarshal(
342        CmsObject cms,
343        Document document,
344        String encoding,
345        EntityResolver resolver) {
346
347        CmsXmlGroupContainer content = new CmsXmlGroupContainer(cms, document, encoding, resolver);
348        // call prepare for use content handler and return the result
349        return (CmsXmlGroupContainer)content.getHandler().prepareForUse(cms, content);
350    }
351
352    /**
353     * Factory method to unmarshal (generate) a group container instance from a String
354     * that contains XML data.<p>
355     *
356     * The given encoding is used when marshalling the XML again later.<p>
357     *
358     * <b>Warning:</b><br/>
359     * This method does not support requested historic versions, it always loads the
360     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
361     * for history support.<p>
362     *
363     * @param cms the cms context, if <code>null</code> no link validation is performed
364     * @param xmlData the XML data in a String
365     * @param encoding the encoding to use when marshalling the group container later
366     * @param resolver the XML entity resolver to use
367     *
368     * @return a group container instance unmarshalled from the String
369     *
370     * @throws CmsXmlException if something goes wrong
371     */
372    public static CmsXmlGroupContainer unmarshal(
373        CmsObject cms,
374        String xmlData,
375        String encoding,
376        EntityResolver resolver) throws CmsXmlException {
377
378        // create the XML content object from the provided String
379        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
380    }
381
382    /**
383     * Gets the ADE cache from the ADE manager.<p>
384     *
385     * @return the ADE cache
386     */
387    private static CmsADECache getCache() {
388
389        return OpenCms.getADEManager().getCache();
390    }
391
392    /**
393     * Returns the cached group container.<p>
394     *
395     * @param cms the cms context
396     * @param resource the group container resource
397     * @param keepEncoding if to keep the encoding while unmarshalling
398     *
399     * @return the cached group container, or <code>null</code> if not found
400     */
401    private static CmsXmlGroupContainer getCache(CmsObject cms, CmsResource resource, boolean keepEncoding) {
402
403        if (resource instanceof I_CmsHistoryResource) {
404            return null;
405        }
406        return getCache().getCacheGroupContainer(
407            getCache().getCacheKey(resource.getStructureId(), keepEncoding),
408            cms.getRequestContext().getCurrentProject().isOnlineProject());
409    }
410
411    /**
412     * Stores the given group container in the cache.<p>
413     *
414     * @param cms the cms context
415     * @param xmlGroupContainer the group container to cache
416     * @param keepEncoding if the encoding was kept while unmarshalling
417     */
418    private static void setCache(CmsObject cms, CmsXmlGroupContainer xmlGroupContainer, boolean keepEncoding) {
419
420        if (xmlGroupContainer.getFile() instanceof I_CmsHistoryResource) {
421            return;
422        }
423        boolean online = cms.getRequestContext().getCurrentProject().isOnlineProject();
424        getCache().setCacheGroupContainer(
425            getCache().getCacheKey(xmlGroupContainer.getFile().getStructureId(), keepEncoding),
426            xmlGroupContainer,
427            online);
428    }
429
430}