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 GmbH & Co. KG, 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.file.types.CmsResourceTypeXmlContainerPage;
036import org.opencms.i18n.CmsEncoder;
037import org.opencms.loader.CmsLoaderException;
038import org.opencms.main.CmsException;
039import org.opencms.main.OpenCms;
040import org.opencms.xml.CmsXmlContentDefinition;
041import org.opencms.xml.CmsXmlEntityResolver;
042import org.opencms.xml.CmsXmlException;
043import org.opencms.xml.CmsXmlUtils;
044import org.opencms.xml.content.Messages;
045
046import java.io.UnsupportedEncodingException;
047import java.util.Locale;
048
049import javax.servlet.ServletRequest;
050
051import org.dom4j.Document;
052import org.dom4j.DocumentHelper;
053import org.xml.sax.EntityResolver;
054
055/**
056 * Provides factory methods to unmarshal (read) an container page object.<p>
057 *
058 * @since 7.5.2
059 */
060public final class CmsXmlContainerPageFactory {
061
062    /**
063     * No instances of this class should be created.<p>
064     */
065    private CmsXmlContainerPageFactory() {
066
067        // noop
068    }
069
070    /**
071     * Create a new instance of an container page based on the given default content,
072     * that will have all language nodes of the default content and ensures the presence of the given locale.<p>
073     *
074     * The given encoding is used when marshalling the XML again later.<p>
075     *
076     * @param cms the current users OpenCms content
077     * @param locale the locale to generate the default content for
078     * @param modelUri the absolute path to the container page file acting as model
079     *
080     * @throws CmsException in case the model file is not found or not valid
081     *
082     * @return the created container page
083     */
084    public static CmsXmlContainerPage createDocument(CmsObject cms, Locale locale, String modelUri)
085    throws CmsException {
086
087        // create the XML content
088        CmsXmlContainerPage content = new CmsXmlContainerPage(cms, locale, modelUri);
089        // call prepare for use content handler and return the result
090        return (CmsXmlContainerPage)content.getHandler().prepareForUse(cms, content);
091    }
092
093    /**
094     * Create a new instance of a container page based on the given content definition,
095     * that will have one language node for the given locale all initialized with default values.<p>
096     *
097     * The given encoding is used when marshalling the XML again later.<p>
098     *
099     * @param cms the current users OpenCms content
100     * @param locale the locale to generate the default content for
101     * @param encoding the encoding to use when marshalling the XML content later
102     * @param contentDefinition the content definition to create the content for
103     *
104     * @return the created container page
105     */
106    public static CmsXmlContainerPage createDocument(
107        CmsObject cms,
108        Locale locale,
109        String encoding,
110        CmsXmlContentDefinition contentDefinition) {
111
112        // create the XML content
113        CmsXmlContainerPage content = new CmsXmlContainerPage(cms, locale, encoding, contentDefinition);
114        // call prepare for use content handler and return the result
115        return (CmsXmlContainerPage)content.getHandler().prepareForUse(cms, content);
116    }
117
118    /**
119     * Factory method to unmarshal (generate) a container page instance from a byte array
120     * that contains XML data.<p>
121     *
122     * When unmarshalling, the encoding is read directly from the XML header of the byte array.
123     * The given encoding is used only when marshalling the XML again later.<p>
124     *
125     * <b>Warning:</b><br/>
126     * This method does not support requested historic versions, it always loads the
127     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
128     * for history support.<p>
129     *
130     * @param cms the cms context
131     * @param xmlData the XML data in a byte array
132     * @param encoding the encoding to use when marshalling the XML content later
133     * @param resolver the XML entitiy resolver to use
134     *
135     * @return a container page instance unmarshalled from the byte array
136     *
137     * @throws CmsXmlException if something goes wrong
138     */
139    public static CmsXmlContainerPage unmarshal(CmsObject cms, byte[] xmlData, String encoding, EntityResolver resolver)
140    throws CmsXmlException {
141
142        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
143    }
144
145    /**
146     * Factory method to unmarshal (read) a container page instance from a OpenCms VFS file
147     * that contains XML data.<p>
148     *
149     * <b>Warning:</b><br/>
150     * This method does not support requested historic versions, it always loads the
151     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
152     * for history support.<p>
153     *
154     * @param cms the current cms object
155     * @param file the file with the XML data to unmarshal
156     *
157     * @return a container page instance unmarshalled from the provided file
158     *
159     * @throws CmsXmlException if something goes wrong
160     */
161    public static CmsXmlContainerPage unmarshal(CmsObject cms, CmsFile file) throws CmsXmlException {
162
163        return unmarshal(cms, file, true);
164    }
165
166    /**
167     * Factory method to unmarshal (read) a container page instance from a OpenCms VFS file
168     * that contains XML data, using wither the encoding set
169     * in the XML file header, or the encoding set in the VFS file property.<p>
170     *
171     * If you are not sure about the implications of the encoding issues,
172     * use {@link #unmarshal(CmsObject, CmsFile)} instead.<p>
173     *
174     * <b>Warning:</b><br/>
175     * This method does not support requested historic versions, it always loads the
176     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
177     * for history support.<p>
178     *
179     * @param cms the current cms object
180     * @param file the file with the XML data to unmarshal
181     * @param keepEncoding if <code>true</code>, the encoding specified in the XML header is used,
182     *    otherwise the encoding from the VFS file property is used
183     *
184     * @return a container page instance unmarshalled from the provided file
185     *
186     * @throws CmsXmlException if something goes wrong
187     */
188    public static CmsXmlContainerPage unmarshal(CmsObject cms, CmsFile file, boolean keepEncoding)
189    throws CmsXmlException {
190
191        return unmarshal(cms, file, keepEncoding, false);
192    }
193
194    /**
195     * Factory method to unmarshal (read) a container page instance from a OpenCms VFS file
196     * that contains XML data, using wither the encoding set
197     * in the XML file header, or the encoding set in the VFS file property.<p>
198     *
199     * If you are not sure about the implications of the encoding issues,
200     * use {@link #unmarshal(CmsObject, CmsFile)} instead.<p>
201     *
202     * <b>Warning:</b><br/>
203     * This method does not support requested historic versions, it always loads the
204     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
205     * for history support.<p>
206     *
207     * @param cms the current cms object
208     * @param file the file with the XML data to unmarshal
209     * @param keepEncoding if <code>true</code>, the encoding specified in the XML header is used,
210     *    otherwise the encoding from the VFS file property is used
211     * @param noCache <code>true</code> to avoid cached results
212     *
213     * @return a container page instance unmarshalled from the provided file
214     *
215     * @throws CmsXmlException if something goes wrong
216     */
217    public static CmsXmlContainerPage unmarshal(CmsObject cms, CmsFile file, boolean keepEncoding, boolean noCache)
218    throws CmsXmlException {
219
220        // check the cache
221        CmsXmlContainerPage content = null;
222        if (!noCache) {
223            content = getCache(cms, file, keepEncoding);
224            if (content != null) {
225                return content;
226            }
227        }
228
229        // not found in cache, read as normally
230        byte[] contentBytes = file.getContents();
231        String filename = cms.getSitePath(file);
232
233        String encoding = null;
234        try {
235            encoding = cms.readPropertyObject(
236                filename,
237                CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
238                true).getValue();
239        } catch (CmsException e) {
240            // encoding will be null
241        }
242        if (encoding == null) {
243            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
244        } else {
245            encoding = CmsEncoder.lookupEncoding(encoding, null);
246            if (encoding == null) {
247                throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
248            }
249        }
250
251        if (contentBytes.length > 0) {
252            // content is initialized
253            if (keepEncoding) {
254                // use the encoding from the content
255                content = unmarshal(cms, contentBytes, encoding, new CmsXmlEntityResolver(cms));
256            } else {
257                // use the encoding from the file property
258                // this usually only triggered by a save operation
259                try {
260                    String contentStr = new String(contentBytes, encoding);
261                    content = unmarshal(cms, contentStr, encoding, new CmsXmlEntityResolver(cms));
262                } catch (UnsupportedEncodingException e) {
263                    // this will not happen since the encoding has already been validated
264                    throw new CmsXmlException(
265                        Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
266                }
267            }
268        } else {
269            // content is empty
270            content = new CmsXmlContainerPage(
271                cms,
272                DocumentHelper.createDocument(),
273                encoding,
274                new CmsXmlEntityResolver(cms));
275        }
276
277        // set the file
278        content.setFile(file);
279
280        // Need to call initDocument again because it needs the file set to fix the nested formatter settings
281        content.initDocument(cms);
282
283        // call prepare for use content handler and return the result
284        CmsXmlContainerPage xmlCntPage = (CmsXmlContainerPage)content.getHandler().prepareForUse(cms, content);
285
286        // set the cache
287        if (!noCache) {
288            setCache(cms, xmlCntPage, keepEncoding);
289        }
290
291        return xmlCntPage;
292    }
293
294    /**
295     * Factory method to unmarshal (read) a container page instance from a OpenCms VFS resource
296     * that contains XML data.<p>
297     *
298     * <b>Warning:</b><br/>
299     * This method does not support requested historic versions, it always loads the
300     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
301     * for history support.<p>
302     *
303     * @param cms the current cms object
304     * @param resource the resource with the XML data to unmarshal
305     *
306     * @return a container page instance unmarshalled from the provided resource
307     *
308     * @throws CmsException if something goes wrong
309     */
310    public static CmsXmlContainerPage unmarshal(CmsObject cms, CmsResource resource) throws CmsException {
311
312        // check the cache
313        CmsXmlContainerPage content = getCache(cms, resource, true);
314        if (content != null) {
315            return content;
316        }
317
318        content = unmarshal(cms, cms.readFile(resource), true, true);
319
320        // set the cache
321        setCache(cms, content, true);
322
323        return content;
324    }
325
326    /**
327     * Factory method to unmarshal (read) a container page instance from
328     * a resource, using the request attributes as cache.<p>
329     *
330     * @param cms the current OpenCms context object
331     * @param resource the resource to unmarshal
332     * @param req the current request
333     *
334     * @return the unmarshaled xml content, or null if the given resource was not of type {@link org.opencms.file.types.CmsResourceTypeXmlContainerPage}
335     *
336     * @throws CmsException in something goes wrong
337     * @throws CmsLoaderException if no loader for the given <code>resource</code> type ({@link CmsResource#getTypeId()}) is available
338     * @throws CmsXmlException if the given <code>resource</code> is not of type container page
339     */
340    public static CmsXmlContainerPage unmarshal(CmsObject cms, CmsResource resource, ServletRequest req)
341    throws CmsXmlException, CmsLoaderException, CmsException {
342
343        String rootPath = resource.getRootPath();
344
345        if (!CmsResourceTypeXmlContainerPage.isContainerPage(resource)) {
346            // sanity check: resource must be of type XML content
347            throw new CmsXmlException(
348                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_TYPE_1, resource.getRootPath()));
349        }
350
351        // try to get the requested content from the current request attribute
352        // this is also necessary for historic versions that have been loaded
353        CmsXmlContainerPage content = (CmsXmlContainerPage)req.getAttribute(rootPath);
354
355        if (content == null) {
356            // unmarshal XML structure from the file content
357            content = unmarshal(cms, resource);
358            // store the content as request attribute for future read requests
359            req.setAttribute(rootPath, content);
360        }
361
362        // return the result
363        return content;
364    }
365
366    /**
367     * Factory method to unmarshal (generate) a container page instance from a XML document.<p>
368     *
369     * The given encoding is used when marshalling the XML again later.<p>
370     *
371     * <b>Warning:</b><br/>
372     * This method does not support requested historic versions, it always loads the
373     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
374     * for history support.<p>
375     *
376     * @param cms the cms context, if <code>null</code> no link validation is performed
377     * @param document the XML document to generate the container page from
378     * @param encoding the encoding to use when marshalling the container page later
379     * @param resolver the XML entity resolver to use
380     *
381     * @return a container page instance unmarshalled from the String
382     */
383    public static CmsXmlContainerPage unmarshal(
384        CmsObject cms,
385        Document document,
386        String encoding,
387        EntityResolver resolver) {
388
389        CmsXmlContainerPage content = new CmsXmlContainerPage(cms, document, encoding, resolver);
390        // call prepare for use content handler and return the result
391        return (CmsXmlContainerPage)content.getHandler().prepareForUse(cms, content);
392    }
393
394    /**
395     * Factory method to unmarshal (generate) a container page instance from a String
396     * that contains XML data.<p>
397     *
398     * The given encoding is used when marshalling the XML again later.<p>
399     *
400     * <b>Warning:</b><br/>
401     * This method does not support requested historic versions, it always loads the
402     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
403     * for history support.<p>
404     *
405     * @param cms the cms context, if <code>null</code> no link validation is performed
406     * @param xmlData the XML data in a String
407     * @param encoding the encoding to use when marshalling the container page later
408     * @param resolver the XML entity resolver to use
409     *
410     * @return a container page instance unmarshalled from the String
411     *
412     * @throws CmsXmlException if something goes wrong
413     */
414    public static CmsXmlContainerPage unmarshal(CmsObject cms, String xmlData, String encoding, EntityResolver resolver)
415    throws CmsXmlException {
416
417        // create the XML content object from the provided String
418        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
419    }
420
421    /**
422     * Gets the ADE cache from the ADE manager.<p>
423     *
424     * @return the ADE cache
425     */
426    private static CmsADECache getCache() {
427
428        return OpenCms.getADEManager().getCache();
429    }
430
431    /**
432     * Returns the cached container page.<p>
433     *
434     * @param cms the cms context
435     * @param resource the container page resource
436     * @param keepEncoding if to keep the encoding while unmarshalling
437     *
438     * @return the cached container page, or <code>null</code> if not found
439     */
440    private static CmsXmlContainerPage getCache(CmsObject cms, CmsResource resource, boolean keepEncoding) {
441
442        if (resource instanceof I_CmsHistoryResource) {
443            return null;
444        }
445        return getCache().getCacheContainerPage(
446            getCache().getCacheKey(resource.getStructureId(), keepEncoding),
447            cms.getRequestContext().getCurrentProject().isOnlineProject());
448    }
449
450    /**
451     * Stores the given container page in the cache.<p>
452     *
453     * @param cms the cms context
454     * @param xmlCntPage the container page to cache
455     * @param keepEncoding if the encoding was kept while unmarshalling
456     */
457    private static void setCache(CmsObject cms, CmsXmlContainerPage xmlCntPage, boolean keepEncoding) {
458
459        if (xmlCntPage.getFile() instanceof I_CmsHistoryResource) {
460            return;
461        }
462        if (xmlCntPage.hasInvalidatedBrokenLinks()) {
463            // not caching container pages with broken links - they may be 'broken' for permission reasons,
464            // and the cache does not take the current user into account
465            return;
466        }
467        boolean online = cms.getRequestContext().getCurrentProject().isOnlineProject();
468        getCache().setCacheContainerPage(
469            getCache().getCacheKey(xmlCntPage.getFile().getStructureId(), keepEncoding),
470            xmlCntPage,
471            online);
472    }
473}