All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xmlbeam.XBProjector Maven / Gradle / Ivy

/**
 *  Copyright 2012 Sven Ewald
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.xmlbeam;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.net.URISyntaxException;
import java.text.Format;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xmlbeam.annotation.XBDelete;
import org.xmlbeam.annotation.XBDocURL;
import org.xmlbeam.annotation.XBRead;
import org.xmlbeam.annotation.XBUpdate;
import org.xmlbeam.annotation.XBValue;
import org.xmlbeam.annotation.XBWrite;
import org.xmlbeam.config.DefaultXMLFactoriesConfig;
import org.xmlbeam.config.XMLFactoriesConfig;
import org.xmlbeam.dom.DOMAccess;
import org.xmlbeam.externalizer.Externalizer;
import org.xmlbeam.externalizer.ExternalizerAdapter;
import org.xmlbeam.io.XBFileIO;
import org.xmlbeam.io.XBStreamInput;
import org.xmlbeam.io.XBStreamOutput;
import org.xmlbeam.io.XBUrlIO;
import org.xmlbeam.types.DefaultTypeConverter;
import org.xmlbeam.types.StringRenderer;
import org.xmlbeam.types.TypeConverter;
import org.xmlbeam.util.intern.DOMHelper;
import org.xmlbeam.util.intern.ReflectionHelper;

/**
 * 

* Overview
* The class XMLProjector is a tool to create, read or write so called "projections". Projections * are Java interfaces associated to XML documents. Projections may contain methods annotated with * XPath selectors. These XPath expressions define the subset of XML data which is "projected" to * Java values and objects. *

*

* Getters
* For getter methods (methods with a name-prefix "get", returning a value) the XPath-selected nodes * are converted to the method return type. This works with all java primitive types, Strings, and * lists or arrays containing primitives or Strings. *

*

* Setters
* Setter methods (method with a name starting with "set", having a parameter) can be defined to * modify the content of the associated XML document. Not all XPath capabilities define writable * projections, so the syntax is limited to selectors of elements and attributes. In contrast to * Java Beans a setter method may define a return value with the projection interface type. If this * return value is defined, the current projection instance is returned. This allows the definition * of projections according to the fluent interface pattern (aka Builder Pattern). *

*

* Sub Projections
* For the purpose of accessing structured data elements in the XML document you may define * "sub projections" which are projections associated to elements instead to documents. Sub * projections can be used as return type of getters and as parameters of setters. This works even * in arrays or lists. Because of the infamous Java type erasure you have to specify the component * type of the sub projection for a getter returning a list of sub projections. This type is defined * as second parameter "targetType" in the {@link XBRead} annotation. *

*

* Dynamic Projections
* XPath expressions are evaluated during runtime when the corresponding methods are called. Its * possible to use placeholder ("{0}, {1}, {2},... ) in the expression that will be substituted with * method parameters before the expression is evaluated. Therefore getters and setters may have * multiple parameters which will be applied via a {@link MessageFormat} to build up the final XPath * expression. The first parameter of a setter will be used for both, setting the document value and * replacing the placeholder "{0}". *

*

* Projection Mixins
* A mixin is defined as an object implementing a super interface of a projection. You may associate * a mixin with a projection type to add your own code to a projection. This way you can implement * validators, make a projection comparable or even share common business logic between multiple * projections. *

* * @author Sven Ewald */ @SuppressWarnings("serial") public class XBProjector implements Serializable, ProjectionFactory { private static final Externalizer NOOP_EXTERNALIZER = new ExternalizerAdapter(); private final ConfigBuilder configBuilder = new ConfigBuilder(); private Externalizer externalizer = NOOP_EXTERNALIZER; private final Set flags; /** * A variation of the builder pattern. All methods to configure the projector are hidden in this * builder class. */ public class ConfigBuilder implements ProjectionFactoryConfig { /** * Access the {@link XMLFactoriesConfig} as the given subtype to conveniently access * additional methods. * * @param clazz * @return casted XMLFactoriesConfig */ public T as(final Class clazz) { return clazz.cast(xMLFactoriesConfig); } /** * {@inheritDoc} */ @Override public TypeConverter getTypeConverter() { return XBProjector.this.typeConverter; } /** * Cast the type converter to the current type. * * @param clazz * @return Type converter casted down to clazz. */ public T getTypeConverterAs(final Class clazz) { return clazz.cast(getTypeConverter()); } /** * {@inheritDoc} */ @Override public ConfigBuilder setTypeConverter(final TypeConverter converter) { XBProjector.this.typeConverter = converter; return this; } /** * {@inheritDoc} */ @Override public ConfigBuilder setExternalizer(final Externalizer e10r) { XBProjector.this.externalizer = e10r == null ? NOOP_EXTERNALIZER : e10r; return this; } /** * {@inheritDoc} */ @Override public Externalizer getExternalizer() { return XBProjector.this.externalizer; } /** * @param clazz * @return Externalizer cast down to the type clazz */ public T getExternalizerAs(final Class clazz) { return clazz.cast(getExternalizer()); } /** * {@inheritDoc} */ @Override public TransformerFactory createTransformerFactory() { return XBProjector.this.xMLFactoriesConfig.createTransformerFactory(); } /** * {@inheritDoc} */ @Override public DocumentBuilderFactory createDocumentBuilderFactory() { return XBProjector.this.xMLFactoriesConfig.createDocumentBuilderFactory(); } /** * {@inheritDoc} */ @Override public XPathFactory createXPathFactory() { return XBProjector.this.xMLFactoriesConfig.createXPathFactory(); } /** * {@inheritDoc} */ @Override public Transformer createTransformer(final Document... document) { return XBProjector.this.xMLFactoriesConfig.createTransformer(document); } /** * {@inheritDoc} */ @Override public DocumentBuilder createDocumentBuilder() { return XBProjector.this.xMLFactoriesConfig.createDocumentBuilder(); } /** * {@inheritDoc} */ @Override public XPath createXPath(final Document... document) { return XBProjector.this.xMLFactoriesConfig.createXPath(document); } /** * @return StringRenderer used to convert objects into strings */ public StringRenderer getStringRenderer() { return XBProjector.this.stringRenderer; } /** * Cast the type StringRenderer to the current type. * * @param clazz * @return StringRenderer casted down to clazz. */ public T getStringRendererAs(final Class clazz) { return clazz.cast(getTypeConverter()); } /** * @param renderer to be used to convert objects into strings * @return this for convenience */ public ConfigBuilder setStringRenderer(final StringRenderer renderer) { XBProjector.this.stringRenderer= renderer; return this; } } /** * A variation of the builder pattern. Mixin related methods are grouped behind this builder * class. */ class MixinBuilder implements MixinHolder { /** * {@inheritDoc} */ @Override public XBProjector addProjectionMixin(final Class

projectionInterface, final M mixinImplementation) { ensureIsValidProjectionInterface(projectionInterface); Map, Object> map = mixins.containsKey(projectionInterface) ? mixins.get(projectionInterface) : new HashMap, Object>(); for (Class type : ReflectionHelper.findAllCommonSuperInterfaces(projectionInterface, mixinImplementation.getClass())) { map.put(type, mixinImplementation); } mixins.put(projectionInterface, map); return XBProjector.this; } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public M getProjectionMixin(final Class

projectionInterface, final Class mixinInterface) { if (!mixins.containsKey(projectionInterface)) { return null; } return (M) mixins.get(projectionInterface).get(mixinInterface); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public M removeProjectionMixin(final Class

projectionInterface, final Class mixinInterface) { if (!mixins.containsKey(projectionInterface)) { return null; } return (M) mixins.get(projectionInterface).remove(mixinInterface); } } /** * A variation of the builder pattern. IO related methods are grouped behind this builder class. */ class IOBuilder implements ProjectionIO { /** * {@inheritDoc} */ @Override public XBFileIO file(final File file) { return new XBFileIO(XBProjector.this, file); } /** * {@inheritDoc} */ @Override public XBFileIO file(final String fileName) { return new XBFileIO(XBProjector.this, fileName); } /** * {@inheritDoc} */ @Override public XBUrlIO url(final String url) { return new XBUrlIO(XBProjector.this, url); } /** * {@inheritDoc} */ @Override public XBStreamInput stream(final InputStream is) { return new XBStreamInput(XBProjector.this, is); } /** * {@inheritDoc} */ @Override public XBStreamOutput stream(final OutputStream os) { return new XBStreamOutput(XBProjector.this, os); } /** * {@inheritDoc} */ @Override public T fromURLAnnotation(final Class projectionInterface, final Object... optionalParams) throws IOException { org.xmlbeam.annotation.XBDocURL doc = projectionInterface.getAnnotation(org.xmlbeam.annotation.XBDocURL.class); if (doc == null) { throw new IllegalArgumentException("Class " + projectionInterface.getCanonicalName() + " must have the " + XBDocURL.class.getName() + " annotation linking to the document source."); } XBUrlIO urlIO = url(MessageFormat.format(doc.value(), optionalParams)); urlIO.addRequestProperties(filterRequestParamsFromParams(doc.value(), optionalParams)); return urlIO.read(projectionInterface); } /** * @param projectionInterface * @param optionalParams * @return */ @SuppressWarnings("unchecked") Map filterRequestParamsFromParams(final String url, final Object... optionalParams) { Map requestParams = new HashMap(); Format[] formats = new MessageFormat(url).getFormatsByArgumentIndex(); for (int i = 0; i < optionalParams.length; ++i) { if (i >= formats.length) { if ((optionalParams[i] instanceof Map)) { requestParams.putAll((Map) optionalParams[i]); } continue; } if (formats[i] == null) { if ((optionalParams[i] instanceof Map)) { requestParams.putAll((Map) optionalParams[i]); } } } return requestParams; } /** * {@inheritDoc} */ @Override public String toURLAnnotationViaPOST(final Object projection, final Object... optionalParams) throws IOException, URISyntaxException { Class projectionInterface = checkProjectionInstance(projection).getProjectionInterface(); org.xmlbeam.annotation.XBDocURL doc = projectionInterface.getAnnotation(org.xmlbeam.annotation.XBDocURL.class); if (doc == null) { throw new IllegalArgumentException("Class " + projectionInterface.getCanonicalName() + " must have the " + XBDocURL.class.getName() + " annotation linking to the document source."); } XBUrlIO urlIO = url(MessageFormat.format(doc.value(), optionalParams)); urlIO.addRequestProperties(filterRequestParamsFromParams(doc.value(), optionalParams)); return urlIO.write(projection); } } /** * {@inheritDoc} */ @Override public T projectEmptyDocument(final Class projectionInterface) { Document document = xMLFactoriesConfig.createDocumentBuilder().newDocument(); return projectDOMNode(document, projectionInterface); } /** * {@inheritDoc} */ @Override public T projectEmptyElement(final String name, final Class projectionInterface) { Document document = xMLFactoriesConfig.createDocumentBuilder().newDocument(); Element element = document.createElement(name); return projectDOMNode(element, projectionInterface); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public T projectDOMNode(final Node documentOrElement, final Class projectionInterface) { ensureIsValidProjectionInterface(projectionInterface); if (documentOrElement == null) { throw new IllegalArgumentException("Parameter node must not be null"); } final Map, Object> mixinsForProjection = mixins.containsKey(projectionInterface) ? Collections.unmodifiableMap(mixins.get(projectionInterface)) : Collections., Object> emptyMap(); final ProjectionInvocationHandler projectionInvocationHandler = new ProjectionInvocationHandler(XBProjector.this, documentOrElement, projectionInterface, mixinsForProjection, flags.contains(Flags.TO_STRING_RENDERS_XML), flags.contains(Flags.ABSENT_IS_EMPTY)); if (flags.contains(Flags.SYNCHRONIZE_ON_DOCUMENTS)) { final Document document = DOMHelper.getOwnerDocumentFor(documentOrElement); InvocationHandler synchronizedInvocationHandler = new InvocationHandler() { @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { synchronized (document) { return projectionInvocationHandler.invoke(proxy, method, args); } } }; return ((T) Proxy.newProxyInstance(projectionInterface.getClassLoader(), new Class[] { projectionInterface, InternalProjection.class, Serializable.class }, synchronizedInvocationHandler)); } return ((T) Proxy.newProxyInstance(projectionInterface.getClassLoader(), new Class[] { projectionInterface, InternalProjection.class, Serializable.class }, projectionInvocationHandler)); } /** * {@inheritDoc} */ @Override public T projectXMLString(final String xmlContent, final Class projectionInterface) { try { ByteArrayInputStream inputStream = new ByteArrayInputStream(xmlContent.getBytes("utf-8")); return new XBStreamInput(this, inputStream).read(projectionInterface); } catch (IOException e) { throw new RuntimeException(e); } } /** * Marker interface to determine if a Projection instance was created by a Projector. This will * be applied automatically to projections. */ interface InternalProjection extends DOMAccess { } private final XMLFactoriesConfig xMLFactoriesConfig; private final Map, Map, Object>> mixins = new HashMap, Map, Object>>(); private TypeConverter typeConverter = new DefaultTypeConverter(Locale.getDefault(), TimeZone.getTimeZone("GMT")); private StringRenderer stringRenderer = (StringRenderer) typeConverter; // private XBProjector(Setflags,XMLFactoriesConfig xMLFactoriesConfig) { // this.xMLFactoriesConfig = xMLFactoriesConfig; // this.isSynchronizeOnDocuments = flags.contains(Flags.SYNCHRONIZE_ON_DOCUMENTS); // } /** * Global projector configuration options. */ public enum Flags { /** * Enables thread safety by removing concurrent DOM access. Useful if the underlying DOM * implementation is not thread safe. */ SYNCHRONIZE_ON_DOCUMENTS, /** * Let the projections toString() method render the projection target as XML. Be careful if * your documents get large. toString() might be used frequently by the IDE your debugging * in. */ TO_STRING_RENDERS_XML, /** * Option to strip empty nodes from the result. */ OMIT_EMPTY_NODES, /** * If a node is not present, handle it like it is empty. */ ABSENT_IS_EMPTY } /** * Constructor. Use me to create a projector with defaults. * * @param optionalFlags */ public XBProjector(final Flags... optionalFlags) { this(new DefaultXMLFactoriesConfig(), optionalFlags); } private static > Set unfold(final T[] array) { if ((array == null) || (array.length == 0)) { return Collections.emptySet(); } EnumSet enumSet = EnumSet.of(array[0]); for (int i = 1; i < array.length; ++i) { enumSet.add(array[i]); } return enumSet; } /** * @param xMLFactoriesConfig * @param optionalFlags */ public XBProjector(final XMLFactoriesConfig xMLFactoriesConfig, final Flags... optionalFlags) { this.xMLFactoriesConfig = xMLFactoriesConfig; // isSynchronizeOnDocuments = false; this.flags = unfold(optionalFlags); } /** * Shortcut for creating a {@link ConfigBuilder} object to change the projectors configuration. * * @return a new ConfigBuilder for this projector. */ public ConfigBuilder config() { return configBuilder; } /** * Shortcut for creating a {@link MixinHolder} object add or remove mixins to projections. * * @return a new MixinBuilder for this projector. */ public MixinHolder mixins() { return new MixinBuilder(); } /** * Ensures that the given object is a projection created by a projector. * * @param projection * @return */ private InternalProjection checkProjectionInstance(final Object projection) { if (!(projection instanceof InternalProjection)) { throw new IllegalArgumentException("Given object " + projection + " is not a projection."); } return (InternalProjection) projection; } /** * @param projectionInterface */ private void ensureIsValidProjectionInterface(final Class projectionInterface) { if ((projectionInterface == null) || (!projectionInterface.isInterface()) || ((projectionInterface.getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC)) { throw new IllegalArgumentException("Parameter " + projectionInterface + " is not a public interface."); } if (projectionInterface.isAnnotation()) { throw new IllegalArgumentException("Parameter " + projectionInterface + " is an annotation interface. Remove the @ and try again."); } for (Method method : projectionInterface.getMethods()) { final boolean isRead = (method.getAnnotation(XBRead.class) != null); final boolean isWrite = (method.getAnnotation(XBWrite.class) != null); final boolean isDelete = (method.getAnnotation(XBDelete.class) != null); final boolean isUpdate = (method.getAnnotation(XBUpdate.class) != null); final boolean isExternal = (method.getAnnotation(XBDocURL.class) != null); final boolean isThrowsException = (method.getExceptionTypes().length > 0); if (isRead ? isUpdate || isWrite || isDelete : (isUpdate ? isWrite || isDelete : isWrite && isDelete)) { throw new IllegalArgumentException("Method " + method + " has to many annotations. Decide for one of @" + XBRead.class.getSimpleName() + ", @" + XBWrite.class.getSimpleName() + ", @" + XBUpdate.class.getSimpleName() + ", or @" + XBDelete.class.getSimpleName()); } if (isExternal && (isWrite || isUpdate || isDelete)) { throw new IllegalArgumentException("Method " + method + " was declared as writing projection but has a @" + XBDocURL.class.getSimpleName() + " annotation. Defining external projections is only possible when reading because there is no DOM attached."); } if (isRead) { if (!ReflectionHelper.hasReturnType(method)) { throw new IllegalArgumentException("Method " + method + " has @" + XBRead.class.getSimpleName() + " annotation, but has no return type."); } if (ReflectionHelper.isRawType(method.getGenericReturnType())) { throw new IllegalArgumentException("Method " + method + " has @" + XBRead.class.getSimpleName() + " annotation, but has a raw return type."); } if (method.getExceptionTypes().length > 1) { throw new IllegalArgumentException("Method " + method + " has @" + XBRead.class.getSimpleName() + " annotation, but declares to throw multiple exceptions. Which one should I throw?"); } if (ReflectionHelper.isOptional(method.getReturnType()) && isThrowsException) { throw new IllegalArgumentException("Method " + method + " has an Optional<> return type, but declares to throw an exception. Exception will never be thrown because return value must not be null."); } } if ((isWrite || isUpdate || isDelete) && isThrowsException) { throw new IllegalArgumentException("Method " + method + " declares to throw exception " + method.getExceptionTypes()[0].getSimpleName() + " but is not a reading projection method. When should this exception be thrown?"); } if (isWrite) { if (!ReflectionHelper.hasParameters(method)) { throw new IllegalArgumentException("Method " + method + " has @" + XBWrite.class.getSimpleName() + " annotaion, but has no paramerter"); } } if (isUpdate) { if (!ReflectionHelper.hasParameters(method)) { throw new IllegalArgumentException("Method " + method + " has @" + XBUpdate.class.getSimpleName() + " annotaion, but has no paramerter"); } } for (Class clazz : method.getParameterTypes()) { if (ReflectionHelper.isOptional(clazz)) { throw new IllegalArgumentException("Method " + method + " has java.util.Optional as a parameter type. You simply never should not do this."); } } int count = 0; for (Annotation[] paramAnnotations : method.getParameterAnnotations()) { for (Annotation a : paramAnnotations) { if (XBValue.class.equals(a.annotationType())) { if (!(isWrite || isUpdate)) { throw new IllegalArgumentException("Method " + method + " is not a writing projection method, but has an @" + XBValue.class.getSimpleName() + " annotaion."); } if (count > 0) { throw new IllegalArgumentException("Method " + method + " has multiple @" + XBValue.class.getSimpleName() + " annotaions."); } ++count; } } } } } /** * Access to the input/output features of this projector. * * @return A new IOBuilder providing methods to read or write projections. */ @Override public ProjectionIO io() { return new IOBuilder(); } /** * @param projection * @return an XML string of the projection target. */ @Override public String asString(final Object projection) { if (!(projection instanceof InternalProjection)) { throw new IllegalArgumentException("Argument is not a projection."); } final DOMAccess domAccess = (DOMAccess) projection; return domAccess.asString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy