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