001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.jxpath.xml;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URL;
023import java.util.HashMap;
024
025import org.apache.commons.jxpath.Container;
026import org.apache.commons.jxpath.JXPathException;
027import org.apache.commons.jxpath.util.ClassLoaderUtil;
028
029/**
030 * An XML document container reads and parses XML only when it is accessed. JXPath traverses Containers transparently - you use the same paths to access objects
031 * in containers as you do to access those objects directly. You can create XMLDocumentContainers for various XML documents that may or may not be accessed by
032 * XPaths. If they are, they will be automatically read, parsed and traversed. If they are not - they won't be read at all.
033 */
034public class DocumentContainer extends XMLParser2 implements Container {
035
036    /** DOM constant */
037    public static final String MODEL_DOM = "DOM";
038    /** JDOM constant */
039    public static final String MODEL_JDOM = "JDOM";
040    private static final long serialVersionUID = -8713290334113427066L;
041    private static HashMap<String, String> parserClasses = new HashMap<>();
042
043    static {
044        parserClasses.put(MODEL_DOM, "org.apache.commons.jxpath.xml.DOMParser");
045        parserClasses.put(MODEL_JDOM, "org.apache.commons.jxpath.xml.JDOMParser");
046    }
047
048    private static HashMap<String, XMLParser> parsers = new HashMap<>();
049
050    /**
051     * Maps a model type to a parser.
052     *
053     * @param model input model type
054     * @return XMLParser
055     */
056    private static XMLParser getParser(final String model) {
057        return parsers.computeIfAbsent(model, k -> {
058            final String className = parserClasses.get(model);
059            if (className == null) {
060                throw new JXPathException("Unsupported XML model: " + model);
061            }
062            try {
063                return ClassLoaderUtil.<XMLParser>getClass(className, true).getConstructor().newInstance();
064            } catch (final Exception ex) {
065                throw new JXPathException("Cannot allocate XMLParser: " + className, ex);
066            }
067        });
068    }
069
070    /**
071     * Add a class of a custom XML parser. Parsers for the models "DOM" and "JDOM" are pre-registered.
072     *
073     * @param model           model name
074     * @param parserClassName parser class name
075     */
076    public static void registerXMLParser(final String model, final String parserClassName) {
077        parserClasses.put(model, parserClassName);
078    }
079
080    /**
081     * Add an XML parser. Parsers for the models "DOM" and "JDOM" are pre-registered.
082     *
083     * @param model  model name
084     * @param parser parser
085     */
086    public static void registerXMLParser(final String model, final XMLParser parser) {
087        parsers.put(model, parser);
088    }
089
090    /**
091     * Parsed XML.
092     */
093    private Object document;
094
095    /**
096     * XML source URL.
097     */
098    private final URL xmlUrl;
099
100    /**
101     * XML model: DOM, JDOM.
102     */
103    private final String model;
104
105    /**
106     * Use this constructor if the desired model is DOM.
107     *
108     * @param xmlURL is a URL for an XML file. Use getClass().getResource(resourceName) to load XML from a resource file.
109     */
110    public DocumentContainer(final URL xmlURL) {
111        this(xmlURL, MODEL_DOM);
112    }
113
114    /**
115     * Constructs a new DocumentContainer.
116     *
117     * @param xmlUrl is a URL for an XML file. Use getClass().getResource (resourceName) to load XML from a resource file.
118     *
119     * @param model  is one of the MODEL_* constants defined in this class. It determines which parser should be used to load the XML.
120     */
121    public DocumentContainer(final URL xmlUrl, final String model) {
122        this.xmlUrl = xmlUrl;
123        if (xmlUrl == null) {
124            throw new JXPathException("XML URL is null");
125        }
126        this.model = model;
127    }
128
129    /**
130     * Reads XML, caches it internally and returns the Document.
131     *
132     * @return Object
133     */
134    @Override
135    public Object getValue() {
136        if (document == null) {
137            try {
138                InputStream stream = null;
139                try {
140                    if (xmlUrl != null) {
141                        stream = xmlUrl.openStream();
142                    }
143                    document = parseXML(stream);
144                } finally {
145                    if (stream != null) {
146                        stream.close();
147                    }
148                }
149            } catch (final IOException ex) {
150                throw new JXPathException("Cannot read XML from: " + xmlUrl.toString(), ex);
151            }
152        }
153        return document;
154    }
155
156    /**
157     * Parses XML using the parser for the specified model.
158     *
159     * @param stream InputStream
160     * @return Object
161     */
162    @Override
163    public Object parseXML(final InputStream stream) {
164        final XMLParser parser = getParser(model);
165        if (parser instanceof XMLParser2) {
166            final XMLParser2 parser2 = (XMLParser2) parser;
167            parser2.setValidating(isValidating());
168            parser2.setNamespaceAware(isNamespaceAware());
169            parser2.setIgnoringElementContentWhitespace(isIgnoringElementContentWhitespace());
170            parser2.setExpandEntityReferences(isExpandEntityReferences());
171            parser2.setIgnoringComments(isIgnoringComments());
172            parser2.setCoalescing(isCoalescing());
173        }
174        return parser.parseXML(stream);
175    }
176
177    /**
178     * Throws an UnsupportedOperationException.
179     *
180     * @param value value (not) to set
181     */
182    @Override
183    public void setValue(final Object value) {
184        throw new UnsupportedOperationException();
185    }
186}