blob: 011980b9f8e70c6aaa0063ca35f368557f0c5d00 [file] [log] [blame]
/*
* Copyright (c) 2011, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Rick Barkhouse - 2.1 - Initial implementation
package org.eclipse.persistence.jaxb.dynamic;
import java.io.InputStream;
import java.util.Map;
import jakarta.xml.bind.JAXBException;
import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.JAXBContextProperties;
import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext.MetadataContextInput;
import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext.SchemaContextInput;
import org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext.SessionsXmlContextInput;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;
/**
* <p>
* DynamicJAXBContextFactory allows the user to create a DynamicJAXBContext without having
* realized Java classes available on the classpath. During context creation, the user's
* metadata will be analyzed, and in-memory classes will be generated.
* </p>
*
* <p>
* Objects that are returned by EclipseLink unmarshal methods will be subclasses of DynamicEntity.
* DynamicEntities offer a simple get(propertyName) / set(propertyName, propertyValue) API to
* manipulate their data.
* </p>
*
* <p>
* Example:
* </p>
*
* <p><code>
* ClassLoader classLoader = Thread.currentThread().getContextClassLoader();<br>
* InputStream iStream = classLoader.getResourceAsStream("resource/MySchema.xsd");<br><br>
*
* Map&lt;String, Object&gt; properties = new HashMap&lt;String, Object&gt;();<br>
* properties.put(DynamicJAXBContextFactory.XML_SCHEMA_KEY, iStream);<br><br>
*
* DynamicJAXBContext jaxbContext = (DynamicJAXBContext) JAXBContext.newInstance("org.example", classLoader, properties);<br><br>
*
* DynamicEntity employee = jaxbContext.newDynamicEntity("org.example.Employee");<br>
* employee.set("firstName", "Bob");<br>
* employee.set("lastName", "Barker");<br>
* jaxbContext.createMarshaller().(employee, System.out);
* </code></p>
*
* @see jakarta.xml.bind.JAXBContext
* @see org.eclipse.persistence.jaxb.dynamic.DynamicJAXBContext
* @see org.eclipse.persistence.dynamic.DynamicEntity
* @see org.eclipse.persistence.dynamic.DynamicType
*
* @author rbarkhouse
* @since EclipseLink 2.1
*/
public class DynamicJAXBContextFactory {
public static final String XML_SCHEMA_KEY = "xml-schema";
public static final String ENTITY_RESOLVER_KEY = "entity-resolver";
public static final String EXTERNAL_BINDINGS_KEY = "external-bindings";
public static final String SCHEMAMETADATA_CLASS_NAME = "org.eclipse.persistence.jaxb.dynamic.metadata.SchemaMetadata";
/**
* Create a <code>DynamicJAXBContext</code>, using either an XML Schema, EclipseLink OXM file,
* or EclipseLink <code>sessions.xml</code> as the metadata source. This creation method will be
* called if the user calls the <code>newInstance()</code> method on <code>jakarta.xml.bind.JAXBContext</code>,
* and has specified <code>jakarta.xml.bind.JAXBContextFactory=org.eclipse.persistence.jaxb.DynamicJAXBContextFactory</code> in their
* <code>jaxb.properties</code> file.<p>
*
* <b>-- Context Creation From XML Schema --</b><p>
*
* The <code>properties</code> map must contain the following key/value pairs:
* <dl>
* <dt>DynamicJAXBContextFactory.XML_SCHEMA_KEY
* <dd>Either a <code>org.w3c.dom.Node</code>, <code>javax.xml.transform.Source</code>, or <code>java.io.InputStream</code> pointing to the XML Schema
* <dt>DynamicJAXBContextFactory.ENTITY_RESOLVER_KEY
* <dd>An <code>org.xml.sax.EntityResolver</code>, used to resolve schema imports. Can be null.
* </dl>
*
* <i>Example:</i>
* <pre>
* ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
* InputStream iStream = classLoader.getResourceAsStream("resource/MySchema.xsd");
*
* Map&lt;String, Object&gt; properties = new HashMap&lt;String, Object&gt;();
* properties.put(DynamicJAXBContextFactory.XML_SCHEMA_KEY, iStream);
*
* DynamicJAXBContext jaxbContext = (DynamicJAXBContext) JAXBContext.newInstance("org.example", classLoader, properties);
* DynamicEntity emp = jaxbContext.newDynamicEntity("org.example.Employee");
* ...
* </pre>
*
* <b>Context Creation From EclipseLink OXM:</b><p>
*
* The <code>properties</code> map must contain the key <b>JAXBContextProperties.OXM_METADATA_SOURCE</b>, which can have
* several possible values:
*
* <ul>
* <li>One of the following, pointing to your OXM file: <code>java.io.File</code>, <code>java.io.InputStream</code>, <code>java.io.Reader</code>, <code>java.net.URL</code>,<br>
* <code>javax.xml.stream.XMLEventReader</code>, <code>javax.xml.stream.XMLStreamReader</code>, <code>javax.xml.transform.Source</code>,<br>
* <code>org.w3c.dom.Node</code>, or <code>org.xml.sax.InputSource</code>.
* <li>A <code>List</code> of objects from the set above.
* <li>A <code>Map&lt;String, Object&gt;</code>, where <code>String</code> is a package name, and <code>Object</code> is the pointer to the OXM file, from the set<br>
* of possibilities above. If using this option, a <code>package-name</code> element is not required in the <code>xml-bindings</code>
* element of your OXM file.
* </ul>
*
* <i>Example:</i>
* <pre>
* ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
* InputStream iStream = classLoader.getResourceAsStream("resource/eclipselink-oxm.xml");
*
* Map&lt;String, Object&gt; properties = new HashMap&lt;String, Object&gt;();
* properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, iStream);
*
* DynamicJAXBContext jaxbContext = (DynamicJAXBContext) JAXBContext.newInstance("org.example", classLoader, properties);
* DynamicEntity emp = jaxbContext.newDynamicEntity("org.example.Employee");
* ...
* </pre>
*
* <b>Context Creation From EclipseLink sessions.xml:</b><p>
*
* The <code>sessionNames</code> parameter is a colon-delimited list of session names within the
* <code>sessions.xml</code> file. <code>Descriptors</code> in this session's <code>Project</code> must not
* have <code>javaClass</code> set, but must have <code>javaClassName</code> set.<p>
*
* <i>Example:</i>
* <pre>
* ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
*
* DynamicJAXBContext jaxbContext = (DynamicJAXBContext) JAXBContext.newInstance("org.example", classLoader, null);
* DynamicEntity emp = jaxbContext.newDynamicEntity("org.example.Employee");
* ...
* </pre>
*
* @param contextPath
* A colon-delimited <code>String</code> specifying the packages containing <code>jaxb.properties</code>. If bootstrapping
* from EclipseLink <code>sessions.xml</code>, this will also be the name(s) of your sessions.
* @param classLoader
* The application's current class loader, which will be used to first lookup
* classes to see if they exist before new <code>DynamicTypes</code> are generated. Can be
* <code>null</code>, in which case <code>Thread.currentThread().getContextClassLoader()</code> will be used.
* @param properties
* Map of properties to use when creating a new <code>DynamicJAXBContext</code>. Can be null if bootstrapping from sessions.xml.
*
* @return
* A new instance of <code>DynamicJAXBContext</code>.
*
* @throws JAXBException
* if an error was encountered while creating the <code>DynamicJAXBContext</code>.
*/
public static DynamicJAXBContext createContext(String contextPath, ClassLoader classLoader, Map<String, Object> properties) throws JAXBException {
Object schema = null;
EntityResolver resolver = null;
Object bindings = null;
if (properties != null) {
schema = properties.get(XML_SCHEMA_KEY);
resolver = (EntityResolver) properties.get(ENTITY_RESOLVER_KEY);
if ((bindings = properties.get(JAXBContextProperties.OXM_METADATA_SOURCE)) == null) {
// try looking up the 'old' metadata source key
bindings = properties.get(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY);
}
}
// First try looking for an XSD
if (schema != null) {
if (schema instanceof Node) {
return createContextFromXSD((Node) schema, resolver, classLoader, properties);
}
if (schema instanceof InputStream) {
return createContextFromXSD((InputStream) schema, resolver, classLoader, properties);
}
if (schema instanceof Source) {
return createContextFromXSD((Source) schema, resolver, classLoader, properties);
}
}
// Next, check for OXM
if (bindings != null) {
return createContextFromOXM(classLoader, properties);
}
// Lastly, try sessions.xml
if (contextPath != null) {
return new DynamicJAXBContext(new SessionsXmlContextInput(contextPath, properties, classLoader));
} else {
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.nullSessionName());
}
}
/**
* Unsupported Operation. DynamicJAXBConexts can not be created from concrete classes. Use the standard
* JAXBContext to create a context from existing Classes.
*
* @see org.eclipse.persistence.jaxb.JAXBContext
*/
public static DynamicJAXBContext createContext(Class<?>[] classes, Map<String, Object> properties) throws JAXBException {
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.cannotCreateDynamicContextFromClasses());
}
/**
* Create a <code>DynamicJAXBContext</code>, using XML Schema as the metadata source.
*
* @param schemaDOM
* <code>org.w3c.dom.Node</code> representing the XML Schema.
* @param resolver
* An <code>org.xml.sax.EntityResolver</code>, used to resolve schema imports. Can be null.
* @param classLoader
* The application's current class loader, which will be used to first lookup
* classes to see if they exist before new <code>DynamicTypes</code> are generated. Can be
* <code>null</code>, in which case <code>Thread.currentThread().getContextClassLoader()</code> will be used.
* @param properties
* Map of properties to use when creating a new <code>DynamicJAXBContext</code>. Can be null.
*
* @return
* A new instance of <code>DynamicJAXBContext</code>.
*
* @throws JAXBException
* if an error was encountered while creating the <code>DynamicJAXBContext</code>.
*/
public static DynamicJAXBContext createContextFromXSD(Node schemaDOM, EntityResolver resolver, ClassLoader classLoader, Map<String, Object> properties) throws JAXBException {
if (schemaDOM == null) {
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.nullNode());
}
if (resolver != null) {
// If schema and resolver are both specified, this indicates a schema import
// This is not supported when boostrapping from a Node.
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.xsdImportNotSource());
}
DynamicJAXBContext ctx = new DynamicJAXBContext(new SchemaContextInput(schemaDOM, null, properties, classLoader));
fixDateTimeConversion(ctx);
return ctx;
}
/**
* Create a <code>DynamicJAXBContext</code>, using XML Schema as the metadata source.
*
* @param schemaStream
* <code>java.io.InputStream</code> from which to read the XML Schema.
* @param resolver
* An <code>org.xml.sax.EntityResolver</code>, used to resolve schema imports. Can be null.
* @param classLoader
* The application's current class loader, which will be used to first lookup
* classes to see if they exist before new <code>DynamicTypes</code> are generated. Can be
* <code>null</code>, in which case <code>Thread.currentThread().getContextClassLoader()</code> will be used.
* @param properties
* Map of properties to use when creating a new <code>DynamicJAXBContext</code>. Can be null.
*
* @return
* A new instance of <code>DynamicJAXBContext</code>.
*
* @throws JAXBException
* if an error was encountered while creating the <code>DynamicJAXBContext</code>.
*/
public static DynamicJAXBContext createContextFromXSD(InputStream schemaStream, EntityResolver resolver, ClassLoader classLoader, Map<String, ?> properties) throws JAXBException {
if (schemaStream == null) {
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.nullInputStream());
}
DynamicJAXBContext ctx = new DynamicJAXBContext(new SchemaContextInput(schemaStream, resolver, properties, classLoader));
fixDateTimeConversion(ctx);
return ctx;
}
/**
* Create a <code>DynamicJAXBContext</code>, using XML Schema as the metadata source.
*
* @param schemaSource
* <code>javax.xml.transform.Source</code> from which to read the XML Schema.
* @param resolver
* An <code>org.xml.sax.EntityResolver</code>, used to resolve schema imports. Can be null.
* @param classLoader
* The application's current class loader, which will be used to first lookup
* classes to see if they exist before new <code>DynamicTypes</code> are generated. Can be
* <code>null</code>, in which case <code>Thread.currentThread().getContextClassLoader()</code> will be used.
* @param properties
* Map of properties to use when creating a new <code>DynamicJAXBContext</code>. Can be null.
*
* @return
* A new instance of <code>DynamicJAXBContext</code>.
*
* @throws JAXBException
* if an error was encountered while creating the <code>DynamicJAXBContext</code>.
*/
public static DynamicJAXBContext createContextFromXSD(Source schemaSource, EntityResolver resolver, ClassLoader classLoader, Map<String, Object> properties) throws JAXBException {
if (schemaSource == null) {
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.nullSource());
}
DynamicJAXBContext ctx = new DynamicJAXBContext(new SchemaContextInput(schemaSource, resolver, properties, classLoader));
fixDateTimeConversion(ctx);
return ctx;
}
/**
* Create a <code>DynamicJAXBContext</code>, using an EclipseLink OXM file as the metadata source.
*
* @param classLoader
* The application's current class loader, which will be used to first lookup
* classes to see if they exist before new <code>DynamicTypes</code> are generated. Can be
* <code>null</code>, in which case <code>Thread.currentThread().getContextClassLoader()</code> will be used.
* @param properties
* Map of properties to use when creating a new <code>DynamicJAXBContext</code>. This map must
* contain a key of JAXBContextProperties.OXM_METADATA_SOURCE, which can have several possible values:
*
* <ul>
* <li>One of the following, pointing to your OXM file: <code>java.io.File</code>, <code>java.io.InputStream</code>, <code>java.io.Reader</code>, <code>java.net.URL</code>,<br>
* <code>javax.xml.stream.XMLEventReader</code>, <code>javax.xml.stream.XMLStreamReader</code>, <code>javax.xml.transform.Source</code>,<br>
* <code>org.w3c.dom.Node</code>, or <code>org.xml.sax.InputSource</code>.
* <li>A <code>List</code> of objects from the set above.
* <li>A <code>Map&lt;String, Object&gt;</code>, where <code>String</code> is a package name, and <code>Object</code> is the pointer to the OXM file, from the set<br>
* of possibilities above. If using this option, a <code>package-name</code> element is not required in the <code>xml-bindings</code>
* element of your OXM file.
* </ul>
*
*
* @return
* A new instance of <code>DynamicJAXBContext</code>.
*
* @throws JAXBException
* if an error was encountered while creating the <code>DynamicJAXBContext</code>.
*/
public static DynamicJAXBContext createContextFromOXM(ClassLoader classLoader, Map<String, ?> properties) throws JAXBException {
if (properties == null || (properties.get(JAXBContextProperties.OXM_METADATA_SOURCE) == null && properties.get(JAXBContextFactory.ECLIPSELINK_OXM_XML_KEY) == null)) {
throw new JAXBException(org.eclipse.persistence.exceptions.JAXBException.oxmKeyNotFound());
}
return new DynamicJAXBContext(new MetadataContextInput(properties, classLoader));
}
/**
* Ensures that XSD dateTimes will always be unmarshalled as XMLGregorianCalendars, and never
* as GregorianCalendars. CALENDAR entries are removed from the default XMLConversionManager,
* and replaced with XML_GREGORIAN_CALENDAR.
*/
private static void fixDateTimeConversion(DynamicJAXBContext ctx) {
XMLConversionManager conversionManager = (XMLConversionManager) ctx.getXMLContext().getSession().getDatasourcePlatform().getConversionManager();
Map<QName, Class<?>> defaultXmlTypes = XMLConversionManager.getDefaultXMLTypes();
defaultXmlTypes.remove(Constants.DATE_TIME_QNAME);
defaultXmlTypes.put(Constants.DATE_TIME_QNAME, CoreClassConstants.XML_GREGORIAN_CALENDAR);
Map<Class<?>, QName> defaultJavaTypes = XMLConversionManager.getDefaultJavaTypes();
defaultJavaTypes.remove(CoreClassConstants.CALENDAR);
defaultJavaTypes.put(CoreClassConstants.XML_GREGORIAN_CALENDAR, Constants.DATE_TIME_QNAME);
}
}