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.content;
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.types.CmsResourceTypeXmlAdeConfiguration;
035import org.opencms.file.types.CmsResourceTypeXmlContent;
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;
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 XML content object.<p>
056 *
057 * @since 6.0.0
058 */
059public final class CmsXmlContentFactory {
060
061    /**
062     * No instances of this class should be created.<p>
063     */
064    private CmsXmlContentFactory() {
065
066        // noop
067    }
068
069    /**
070     * Creates a new XML content based on a resource type.<p>
071     *
072     * @param cms the current OpenCms context
073     * @param locale the locale to generate the default content for
074     * @param resourceType the resource type for which the document should be created
075     *
076     * @return the created XML content
077     *
078     * @throws CmsXmlException if something goes wrong
079     */
080    public static CmsXmlContent createDocument(CmsObject cms, Locale locale, CmsResourceTypeXmlContent resourceType)
081    throws CmsXmlException {
082
083        String schema = resourceType.getSchema();
084        CmsXmlContentDefinition contentDefinition = CmsXmlContentDefinition.unmarshal(cms, schema);
085        CmsXmlContent xmlContent = CmsXmlContentFactory.createDocument(
086            cms,
087            locale,
088            OpenCms.getSystemInfo().getDefaultEncoding(),
089            contentDefinition);
090        return xmlContent;
091    }
092
093    /**
094     * Create a new instance of an XML content based on the given default content,
095     * hat will have all language nodes of the default content and ensures the presence of the given locale.<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 modelUri the absolute path to the XML content file acting as model
102     *
103     * @throws CmsException in case the model file is not found or not valid
104     *
105     * @return the created XML content
106     */
107    public static CmsXmlContent createDocument(CmsObject cms, Locale locale, String modelUri) throws CmsException {
108
109        // create the XML content
110        CmsXmlContent content = new CmsXmlContent(cms, locale, modelUri);
111        // call prepare for use content handler and return the result
112        return content.getContentDefinition().getContentHandler().prepareForUse(cms, content);
113    }
114
115    /**
116     * Create a new instance of an XML content based on the given content definiton,
117     * that will have one language node for the given locale all initialized with default values.<p>
118     *
119     * The given encoding is used when marshalling the XML again later.<p>
120     *
121     * @param cms the current users OpenCms content
122     * @param locale the locale to generate the default content for
123     * @param encoding the encoding to use when marshalling the XML content later
124     * @param contentDefinition the content definiton to create the content for
125     *
126     * @return the created XML content
127     */
128    public static CmsXmlContent createDocument(
129        CmsObject cms,
130        Locale locale,
131        String encoding,
132        CmsXmlContentDefinition contentDefinition) {
133
134        // create the XML content
135        CmsXmlContent content = new CmsXmlContent(cms, locale, encoding, contentDefinition);
136        // call prepare for use content handler and return the result
137        return content.getHandler().prepareForUse(cms, content);
138    }
139
140    /**
141     * Factory method to unmarshal (generate) a XML content instance from a byte array
142     * that contains XML data.<p>
143     *
144     * When unmarshalling, the encoding is read directly from the XML header of the byte array.
145     * The given encoding is used only when marshalling the XML again later.<p>
146     *
147     * <b>Warning:</b><br/>
148     * This method does not support requested historic versions, it always loads the
149     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
150     * for history support.<p>
151     *
152     * @param cms the cms context
153     * @param xmlData the XML data in a byte array
154     * @param encoding the encoding to use when marshalling the XML content later
155     * @param resolver the XML entitiy resolver to use
156     *
157     * @return a XML content instance unmarshalled from the byte array
158     *
159     * @throws CmsXmlException if something goes wrong
160     */
161    public static CmsXmlContent unmarshal(CmsObject cms, byte[] xmlData, String encoding, EntityResolver resolver)
162    throws CmsXmlException {
163
164        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
165    }
166
167    /**
168     * Factory method to unmarshal (read) a XML content instance from a OpenCms VFS file
169     * that contains XML data.<p>
170     *
171     * <b>Warning:</b><br/>
172     * This method does not support requested historic versions, it always loads the
173     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
174     * for history support.<p>
175     *
176     * @param cms the current cms object
177     * @param file the file with the XML data to unmarshal
178     *
179     * @return a XML page instance unmarshalled from the provided file
180     *
181     * @throws CmsXmlException if something goes wrong
182     */
183    public static CmsXmlContent unmarshal(CmsObject cms, CmsFile file) throws CmsXmlException {
184
185        return unmarshal(cms, file, true);
186    }
187
188    /**
189     * Factory method to unmarshal (read) a XML content instance from a OpenCms VFS file
190     * that contains XML data, using wither the encoding set
191     * in the XML file header, or the encoding set in the VFS file property.<p>
192     *
193     * If you are not sure about the implications of the encoding issues,
194     * use {@link #unmarshal(CmsObject, CmsFile)} instead.<p>
195     *
196     * <b>Warning:</b><br/>
197     * This method does not support requested historic versions, it always loads the
198     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
199     * for history support.<p>
200     *
201     * @param cms the current cms object
202     * @param file the file with the XML data to unmarshal
203     * @param keepEncoding if true, the encoding spefified in the XML header is used,
204     *    otherwise the encoding from the VFS file property is used
205     *
206     * @return a XML content instance unmarshalled from the provided file
207     *
208     * @throws CmsXmlException if something goes wrong
209     */
210    public static CmsXmlContent unmarshal(CmsObject cms, CmsFile file, boolean keepEncoding) throws CmsXmlException {
211
212        byte[] contentBytes = file.getContents();
213        String filename = cms.getSitePath(file);
214
215        String encoding = null;
216        if (OpenCms.getResourceManager().hasResourceType(file.getTypeId())) {
217            if (OpenCms.getResourceManager().getResourceType(file) instanceof CmsResourceTypeXmlAdeConfiguration) {
218                encoding = "UTF-8";
219            }
220        }
221        if (encoding == null) {
222            try {
223                encoding = cms.readPropertyObject(
224                    file,
225                    CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
226                    true).getValue();
227            } catch (@SuppressWarnings("unused") CmsException e) {
228                // encoding will be null
229            }
230        }
231        if (encoding == null) {
232            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
233        } else {
234            encoding = CmsEncoder.lookupEncoding(encoding, null);
235            if (encoding == null) {
236                throw new CmsXmlException(Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename));
237            }
238        }
239
240        CmsXmlContent content;
241        if (contentBytes.length > 0) {
242            // content is initialized
243            if (keepEncoding) {
244                // use the encoding from the content
245                content = unmarshal(cms, contentBytes, encoding, new CmsXmlEntityResolver(cms));
246            } else {
247                // use the encoding from the file property
248                // this usually only triggered by a save operation
249                try {
250                    String contentStr = new String(contentBytes, encoding);
251                    content = unmarshal(cms, contentStr, encoding, new CmsXmlEntityResolver(cms));
252                } catch (UnsupportedEncodingException e) {
253                    // this will not happen since the encodig has already been validated
254                    throw new CmsXmlException(
255                        Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, filename),
256                        e);
257                }
258            }
259        } else {
260            // content is empty
261            content = new CmsXmlContent(cms, DocumentHelper.createDocument(), encoding, new CmsXmlEntityResolver(cms));
262        }
263
264        // set the file
265        content.setFile(file);
266        // call prepare for use content handler and return the result
267        return content.getHandler().prepareForUse(cms, content);
268    }
269
270    /**
271     * Factory method to unmarshal (read) a XML content instance from
272     * a resource, using the request attributes as cache.<p>
273     *
274     * @param cms the current OpenCms context object
275     * @param resource the resource to unmarshal
276     * @param req the current request
277     *
278     * @return the unmarshaled xml content, or null if the given resource was not of type {@link org.opencms.file.types.CmsResourceTypeXmlContent}
279     *
280     * @throws CmsException in something goes wrong
281     * @throws CmsLoaderException if no loader for the given <code>resource</code> type ({@link CmsResource#getTypeId()}) is available
282     * @throws CmsXmlException if the given <code>resource</code> is not of type xml content
283     */
284    public static CmsXmlContent unmarshal(CmsObject cms, CmsResource resource, ServletRequest req)
285    throws CmsXmlException, CmsLoaderException, CmsException {
286
287        String rootPath = resource.getRootPath();
288
289        if (!CmsResourceTypeXmlContent.isXmlContent(resource)) {
290            // sanity check: resource must be of type XML content
291            throw new CmsXmlException(
292                Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_TYPE_1, cms.getSitePath(resource)));
293        }
294
295        // try to get the requested content from the current request attribute
296        // this is also necessary for historic versions that have been loaded
297        CmsXmlContent content = (CmsXmlContent)req.getAttribute(rootPath);
298
299        if (content == null) {
300            // unmarshal XML structure from the file content
301            CmsFile file = resource instanceof CmsFile ? (CmsFile)resource : cms.readFile(resource);
302            content = unmarshal(cms, file);
303            // store the content as request attribute for future read requests
304            req.setAttribute(rootPath, content);
305        }
306
307        // return the result
308        return content;
309    }
310
311    /**
312     * Factory method to unmarshal (generate) a XML content instance from a XML document.<p>
313     *
314     * The given encoding is used when marshalling the XML again later.<p>
315     *
316     * <b>Warning:</b><br/>
317     * This method does not support requested historic versions, it always loads the
318     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
319     * for history support.<p>
320     *
321     * @param cms the cms context, if <code>null</code> no link validation is performed
322     * @param document the XML document to generate the XML content from
323     * @param encoding the encoding to use when marshalling the XML content later
324     * @param resolver the XML entitiy resolver to use
325     *
326     * @return a XML content instance unmarshalled from the String
327     */
328    public static CmsXmlContent unmarshal(CmsObject cms, Document document, String encoding, EntityResolver resolver) {
329
330        CmsXmlContent content = new CmsXmlContent(cms, document, encoding, resolver);
331        // call prepare for use content handler and return the result
332        return content.getHandler().prepareForUse(cms, content);
333    }
334
335    /**
336     * Factory method to unmarshal (generate) a XML content instance from a String
337     * that contains XML data.<p>
338     *
339     * The given encoding is used when marshalling the XML again later.<p>
340     *
341     * <b>Warning:</b><br/>
342     * This method does not support requested historic versions, it always loads the
343     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
344     * for history support.<p>
345     *
346     * @param cms the cms context, if <code>null</code> no link validation is performed
347     * @param xmlData the XML data in a String
348     * @param encoding the encoding to use when marshalling the XML content later
349     * @param resolver the XML entitiy resolver to use
350     *
351     * @return a XML content instance unmarshalled from the String
352     *
353     * @throws CmsXmlException if something goes wrong
354     */
355    public static CmsXmlContent unmarshal(CmsObject cms, String xmlData, String encoding, EntityResolver resolver)
356    throws CmsXmlException {
357
358        // create the XML content object from the provided String
359        return unmarshal(cms, CmsXmlUtils.unmarshalHelper(xmlData, resolver), encoding, resolver);
360    }
361
362    /**
363     * Factory method to unmarshal (generate) a XML content instance from a String
364     * that contains XML data.<p>
365     *
366     * The given encoding is used when marshalling the XML again later.<p>
367     *
368     * Since no {@link CmsObject} is available, no link validation is performed!<p>
369     *
370     * <b>Warning:</b><br/>
371     * This method does not support requested historic versions, it always loads the
372     * most recent version. Use <code>{@link #unmarshal(CmsObject, CmsResource, ServletRequest)}</code>
373     * for history support.<p>
374     *
375     * @param xmlData the XML data in a String
376     * @param encoding the encoding to use when marshalling the XML content later
377     * @param resolver the XML entity resolver to use
378     *
379     * @return a XML content instance unmarshalled from the String
380     *
381     * @throws CmsXmlException if something goes wrong
382     */
383    public static CmsXmlContent unmarshal(String xmlData, String encoding, EntityResolver resolver)
384    throws CmsXmlException {
385
386        return unmarshal(null, xmlData, encoding, resolver);
387    }
388}