001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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 org.dom4j.Document;
049import org.dom4j.DocumentHelper;
050import org.xml.sax.EntityResolver;
051
052import jakarta.servlet.ServletRequest;
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)
143    throws CmsXmlException {
144
145        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
146    }
147
148    /**
149     * Factory method to unmarshal (read) a group container instance from a OpenCms VFS file
150     * that contains XML data.<p>
151     *
152     * <b>Warning:</b><br/>
153     * This method does not support requested historic versions, it always loads the
154     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
155     * for history support.<p>
156     *
157     * @param cms the current cms object
158     * @param file the file with the XML data to unmarshal
159     *
160     * @return a group container instance unmarshalled from the provided file
161     *
162     * @throws CmsXmlException if something goes wrong
163     */
164    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsFile file) throws CmsXmlException {
165
166        return unmarshal(cms, file, true);
167    }
168
169    /**
170     * Factory method to unmarshal (read) a group container instance from a OpenCms VFS file
171     * that contains XML data, using wither the encoding set
172     * in the XML file header, or the encoding set in the VFS file property.<p>
173     *
174     * If you are not sure about the implications of the encoding issues,
175     * use {@link #unmarshal(CmsObject, CmsFile)} instead.<p>
176     *
177     * <b>Warning:</b><br/>
178     * This method does not support requested historic versions, it always loads the
179     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
180     * for history support.<p>
181     *
182     * @param cms the current cms object
183     * @param file the file with the XML data to unmarshal
184     * @param keepEncoding if <code>true</code>, the encoding specified in the XML header is used,
185     *    otherwise the encoding from the VFS file property is used
186     *
187     * @return a group container instance unmarshalled from the provided file
188     *
189     * @throws CmsXmlException if something goes wrong
190     */
191    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsFile file, boolean keepEncoding)
192    throws CmsXmlException {
193
194        // check the cache
195        CmsXmlGroupContainer content = getCache(cms, file, keepEncoding);
196        if (content != null) {
197            return content;
198        }
199
200        // not found in cache, read as normally
201        byte[] contentBytes = file.getContents();
202        String filename = cms.getSitePath(file);
203
204        String encoding = null;
205        try {
206            encoding = cms.readPropertyObject(
207                filename,
208                CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
209                true).getValue();
210        } catch (CmsException e) {
211            // encoding will be null
212        }
213        if (encoding == null) {
214            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
215        } else {
216            encoding = CmsEncoder.lookupEncoding(encoding, null);
217            if (encoding == null) {
218                throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
219            }
220        }
221
222        if (contentBytes.length > 0) {
223            // content is initialized
224            if (keepEncoding) {
225                // use the encoding from the content
226                content = unmarshal(cms, contentBytes, encoding, new CmsXmlEntityResolver(cms));
227            } else {
228                // use the encoding from the file property
229                // this usually only triggered by a save operation
230                try {
231                    String contentStr = new String(contentBytes, encoding);
232                    content = unmarshal(cms, contentStr, encoding, new CmsXmlEntityResolver(cms));
233                } catch (UnsupportedEncodingException e) {
234                    // this will not happen since the encoding has already been validated
235                    throw new CmsXmlException(
236                        Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
237                }
238            }
239        } else {
240            // content is empty
241            content = new CmsXmlGroupContainer(
242                cms,
243                DocumentHelper.createDocument(),
244                encoding,
245                new CmsXmlEntityResolver(cms));
246        }
247
248        // set the file
249        content.setFile(file);
250        // call prepare for use content handler and return the result
251        CmsXmlGroupContainer xmlGroupContainer = (CmsXmlGroupContainer)content.getHandler().prepareForUse(cms, content);
252
253        // set the cache
254        setCache(cms, xmlGroupContainer, keepEncoding);
255
256        return xmlGroupContainer;
257    }
258
259    /**
260     * Factory method to unmarshal (read) a group container instance from a OpenCms VFS resource
261     * that contains XML data.<p>
262     *
263     * <b>Warning:</b><br/>
264     * This method does not support requested historic versions, it always loads the
265     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
266     * for history support.<p>
267     *
268     * @param cms the current cms object
269     * @param resource the resource with the XML data to unmarshal
270     *
271     * @return a group container instance unmarshalled from the provided resource
272     *
273     * @throws CmsException if something goes wrong
274     */
275    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsResource resource) throws CmsException {
276
277        // check the cache
278        CmsXmlGroupContainer content = getCache(cms, resource, true);
279        if (content != null) {
280            return content;
281        }
282
283        content = unmarshal(cms, cms.readFile(resource), true);
284
285        // set the cache
286        setCache(cms, content, true);
287
288        return content;
289    }
290
291    /**
292     * Factory method to unmarshal (read) a group container instance from
293     * a resource, using the request attributes as cache.<p>
294     *
295     * @param cms the current OpenCms context object
296     * @param resource the resource to unmarshal
297     * @param req the current request
298     *
299     * @return the unmarshaled xml content
300     *
301     * @throws CmsException in something goes wrong
302     * @throws CmsLoaderException if no loader for the given <code>resource</code> type ({@link CmsResource#getTypeId()}) is available
303     * @throws CmsXmlException if the given <code>resource</code> is not of type group container
304     */
305    public static CmsXmlGroupContainer unmarshal(CmsObject cms, CmsResource resource, ServletRequest req)
306    throws CmsXmlException, CmsLoaderException, CmsException {
307
308        String rootPath = resource.getRootPath();
309
310        // try to get the requested content from the current request attribute
311        // this is also necessary for historic versions that have been loaded
312        CmsXmlGroupContainer content = (CmsXmlGroupContainer)req.getAttribute(rootPath);
313
314        if (content == null) {
315            // unmarshal XML structure from the file content
316            content = unmarshal(cms, resource);
317            // store the content as request attribute for future read requests
318            req.setAttribute(rootPath, content);
319        }
320
321        // return the result
322        return content;
323    }
324
325    /**
326     * Factory method to unmarshal (generate) a group container instance from a XML document.<p>
327     *
328     * The given encoding is used when marshalling the XML again later.<p>
329     *
330     * <b>Warning:</b><br/>
331     * This method does not support requested historic versions, it always loads the
332     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
333     * for history support.<p>
334     *
335     * @param cms the cms context, if <code>null</code> no link validation is performed
336     * @param document the XML document to generate the group container from
337     * @param encoding the encoding to use when marshalling the group container later
338     * @param resolver the XML entity resolver to use
339     *
340     * @return a group container instance unmarshalled from the String
341     */
342    public static CmsXmlGroupContainer unmarshal(
343        CmsObject cms,
344        Document document,
345        String encoding,
346        EntityResolver resolver) {
347
348        CmsXmlGroupContainer content = new CmsXmlGroupContainer(cms, document, encoding, resolver);
349        // call prepare for use content handler and return the result
350        return (CmsXmlGroupContainer)content.getHandler().prepareForUse(cms, content);
351    }
352
353    /**
354     * Factory method to unmarshal (generate) a group container instance from a String
355     * that contains XML data.<p>
356     *
357     * The given encoding is used when marshalling the XML again later.<p>
358     *
359     * <b>Warning:</b><br/>
360     * This method does not support requested historic versions, it always loads the
361     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
362     * for history support.<p>
363     *
364     * @param cms the cms context, if <code>null</code> no link validation is performed
365     * @param xmlData the XML data in a String
366     * @param encoding the encoding to use when marshalling the group container later
367     * @param resolver the XML entity resolver to use
368     *
369     * @return a group container instance unmarshalled from the String
370     *
371     * @throws CmsXmlException if something goes wrong
372     */
373    public static CmsXmlGroupContainer unmarshal(
374        CmsObject cms,
375        String xmlData,
376        String encoding,
377        EntityResolver resolver)
378    throws CmsXmlException {
379
380        // create the XML content object from the provided String
381        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
382    }
383
384    /**
385     * Gets the ADE cache from the ADE manager.<p>
386     *
387     * @return the ADE cache
388     */
389    private static CmsADECache getCache() {
390
391        return OpenCms.getADEManager().getCache();
392    }
393
394    /**
395     * Returns the cached group container.<p>
396     *
397     * @param cms the cms context
398     * @param resource the group container resource
399     * @param keepEncoding if to keep the encoding while unmarshalling
400     *
401     * @return the cached group container, or <code>null</code> if not found
402     */
403    private static CmsXmlGroupContainer getCache(CmsObject cms, CmsResource resource, boolean keepEncoding) {
404
405        if (resource instanceof I_CmsHistoryResource) {
406            return null;
407        }
408        return getCache().getCacheGroupContainer(
409            getCache().getCacheKey(resource.getStructureId(), keepEncoding),
410            cms.getRequestContext().getCurrentProject().isOnlineProject());
411    }
412
413    /**
414     * Stores the given group container in the cache.<p>
415     *
416     * @param cms the cms context
417     * @param xmlGroupContainer the group container to cache
418     * @param keepEncoding if the encoding was kept while unmarshalling
419     */
420    private static void setCache(CmsObject cms, CmsXmlGroupContainer xmlGroupContainer, boolean keepEncoding) {
421
422        if (xmlGroupContainer.getFile() instanceof I_CmsHistoryResource) {
423            return;
424        }
425        boolean online = cms.getRequestContext().getCurrentProject().isOnlineProject();
426        getCache().setCacheGroupContainer(
427            getCache().getCacheKey(xmlGroupContainer.getFile().getStructureId(), keepEncoding),
428            xmlGroupContainer,
429            online);
430    }
431
432}