/* * Copyright 2004-2005 Revolution Systems Inc. * * 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 com.revolsys.record.io.format.xml; import java.io.IOException; import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.ConvertUtilsBean; import org.apache.commons.beanutils.Converter; import org.apache.commons.beanutils.converters.IntegerConverter; import org.apache.commons.beanutils.converters.ShortConverter; import com.revolsys.beans.EnumConverter; import com.revolsys.spring.resource.Resource; import com.revolsys.util.CaseConverter; import com.revolsys.util.Exceptions; /** * <p> * The XmlProcessor class provides a framework for processing an XML Document to * Construct a new Java Object representation of the document or to perform other * actions on the contents of the document. The XmlProcessor uses the STAX API * to read the XML document using the {@link StaxReader}, by using the * streaming API the XML parser will not load the XML document into memory * (although the Java Object representation created by subclasses of * XmlProcessor would likely be in memory at all times). * </p> * <p> * Users of a XmlProcessor implementation construct a new instance of the * subclass and invoke the {@link #process(StaxReader)} method to process * the document and return the result object generated by the processor. * </p> * <p> * To process a document using the XmlProcessor a subclass of XmlProcessor must * be created for each XML namespace that needs to be processed. For each XML * element defined for the XML namespace a process method must be defined in the * subclass. The name of the process method must have the "process" prefix * followed bfollowed by the XML element name, have only an * {@link StaxReader} parameter and return an Object (any subclass of * Object can be returned). * </p> * <p> * For example the process method for the XML element BankAccount would have the * following signature. * </p> * * <pre> * public BankAccount processBankAccount(StaxReader parser); * </pre> * <p> * For example the process method for the XML element firstName would have the * following signature. <b>Note that the part of the method name for the XML * element must have the same case as the XML element name. * </p> * * <pre> * public String processfirstName(StaxReader parser); * </pre> * <p> * The process methods read the attributes from the element and can either * Construct a newn Object for the XML element or perform other processing on the * element. This object can be returned from the process method so that the * calling method can access the object. If an XML element has child elements it * can either ignore them using the * {@link StaxReader#skipSubTree()} method or process the child * element using the {@link #process(StaxReader)} method that will invoke * the appropriate process method for that element. * </p> * <p> * The following example shows the implementation of a process method for a * Person element that has a firstName and lastName element. As no children are * expected the {@link StaxReader#skipSubTree()} method is used to * skip to the end of the element. * </p> * * <pre> * public Person processPerson(final StaxReader parser) * throws XMLStreamException, IOException { * String firstName = parser.getFieldValue(null, "firstName"); * String lastName = parser.getFieldValue(null, "lastName"); * Person person = new Person(firstName, lastName); * StaxUtils.skipSubTree(parser); * return person; * } * </pre> * <p> * The following example shows the implementation of a process method for a * Family element that has one or more Person or Pet child elements. If an * unexpected element occurs and error will be recorded. * </p> * * <pre> * public Family processFamily(final StaxReader parser) * throws XMLStreamException, IOException { * Family family = new Family(); * while (parser.nextTag() == StaxReader.START_ELEMENT) { * Object object = process(parser); * if (object instanceof Person) { * config.addPerson((Person)object); * } else if (object instanceof Pet) { * config.addPet((Pet)object); * } else { * context.addError("Unexpected Element:" + object, null, * parser.getLocation()); * } * } * return family; * } * </pre> * * @author Paul Austin */ public abstract class XmlProcessor { /** The arguments a processor method must have. */ private static final Class<?>[] PROCESS_METHOD_ARGS = new Class[] { StaxReader.class }; /** The cache of processor classes to method caches. */ private static final Map<Class<?>, Map<String, Method>> PROCESSOR_METHOD_CACHE = new HashMap<>(); /** * Create the cache of process methods from the specified class. * * @param processorClass The XmlPorcessor class. * @return The map of method names to process methods. */ private static Map<String, Method> getMethodCache(final Class<?> processorClass) { Map<String, Method> methodCache = PROCESSOR_METHOD_CACHE.get(processorClass); if (methodCache == null) { methodCache = new HashMap<>(); PROCESSOR_METHOD_CACHE.put(processorClass, methodCache); final Method[] methods = processorClass.getMethods(); for (final Method method : methods) { final String methodName = method.getName(); if (methodName.startsWith("process")) { if (Arrays.equals(method.getParameterTypes(), PROCESS_METHOD_ARGS)) { final String name = methodName.substring(7); methodCache.put(name, method); } } } } return methodCache; } /** The context for processing of the XML Document. */ private XmlProcessorContext context = new SimpleXmlProcessorContext(); /** The cache of XML element names to processor methods. */ private final Map<String, Method> methodCache; /** The XML namespace URI processed by this processor. */ private final String namespaceUri; private Map<String, Class<?>> tagNameClassMap = new HashMap<>(); private final Map<QName, Converter> typePathConverterMap = new HashMap<>(); /** * Construct a new XmlProcessor for the XML Namespace URI. * * @param namespaceUri The XML Namespace URI. */ protected XmlProcessor(final String namespaceUri) { initConverters(); this.namespaceUri = namespaceUri; this.typePathConverterMap.put(XmlConstants.XS_SHORT, new ShortConverter()); this.typePathConverterMap.put(XmlConstants.XS_INT, new IntegerConverter()); this.methodCache = getMethodCache(getClass()); } public XmlProcessor(final String namespaceUri, final Map<String, Class<?>> tagNameClassMap) { this(namespaceUri); this.tagNameClassMap = tagNameClassMap; } /** * Get the context for processing the XML Document. * * @return The context for processing the XML Document. */ public final XmlProcessorContext getContext() { return this.context; } public String getNamespaceUri() { return this.namespaceUri; } /** * Get the method to process the XML element. * * @param element The element to process. * @return The method to process the XML element. */ private Method getProcessMethod(final QName element) { final String elementName = element.getLocalPart(); final Method method = this.methodCache.get(elementName); return method; } protected void initConverters() { registerEnumConverter(Enum.class); } @SuppressWarnings("unchecked") public <T> T parseObject(final StaxReader parser, final Class<? extends T> objectClass) throws IOException { try { if (objectClass == null) { Object object = null; while (parser.nextTag() == XMLStreamConstants.START_ELEMENT) { if (object != null) { throw new IllegalArgumentException( "Expecting a single child element " + parser.getLocation()); } object = process(parser); } return (T)object; } else { final T object = objectClass.newInstance(); if (object instanceof Collection) { final Collection<Object> collection = (Collection<Object>)object; while (parser.nextTag() == XMLStreamConstants.START_ELEMENT) { final Object value = process(parser); collection.add(value); } } else { while (parser.nextTag() == XMLStreamConstants.START_ELEMENT) { final String tagName = parser.getName().getLocalPart(); final Object value = process(parser); try { String propertyName; if (tagName.length() > 1 && Character.isLowerCase(tagName.charAt(1))) { propertyName = CaseConverter.toLowerFirstChar(tagName); } else { propertyName = tagName; } BeanUtils.setProperty(object, propertyName, value); } catch (final Throwable e) { e.printStackTrace(); } } } return object; } } catch (final InstantiationException e) { throw new IllegalArgumentException(e); } catch (final IllegalAccessException e) { throw new IllegalArgumentException(e); } } @SuppressWarnings("unchecked") public <T> T process(final Resource resource) { try { final StaxReader xmlReader = StaxReader.newXmlReader(resource); xmlReader.skipToStartElement(); return (T)process(xmlReader); } catch (final RuntimeException e) { throw e; } catch (final Error e) { throw e; } catch (final Exception e) { throw new RuntimeException("Unable to parse: " + resource, e); } } /** * <p> * The process method is used to return an object representation of the * current XML element and subtree from the {@link StaxReader}. The * method finds the process method in the subclass that has the method name * with the prefix "process" followed by the XML element name, have only an * {@link StaxReader} parameter and return an Object (any subclass of * Object can be returned). * </p> * <p> * For example the process method for the XML element BankAccount would have * the following signature. * </p> * * <pre> * public BankAccount processBankAccount(StaxReader parser); * </pre> * <p> * For example the process method for the XML element firstName would have the * following signature. <b>Note that the part of the method name for the XML * element must have the same case as the XML element name. * </p> * * <pre> * public String processfirstName(StaxReader parser); * </pre> * * @param parser The STAX XML parser. * @return The object representation of the XML subtree. * @throws IOException If an I/O exception occurs. * @throws XMLStreamException If an exception processing the XML occurs. */ @SuppressWarnings("unchecked") public <T> T process(final StaxReader parser) throws IOException { final QName element = parser.getName(); final String tagName = element.getLocalPart(); final QName xsiName = parser.getQNameAttribute(XsiConstants.TYPE); boolean hasMapping = false; Class<?> objectClass = null; if (xsiName == null) { objectClass = this.tagNameClassMap.get(tagName); if (this.tagNameClassMap.containsKey(tagName)) { objectClass = this.tagNameClassMap.get(tagName); hasMapping = true; } } else { final String xsiLocalName = xsiName.getLocalPart(); final Converter converter = this.typePathConverterMap.get(xsiName); if (converter != null) { final String text = parser.getElementText(); return (T)converter.convert(null, text); } else if (this.tagNameClassMap.containsKey(xsiLocalName)) { objectClass = this.tagNameClassMap.get(xsiLocalName); hasMapping = true; } else if (this.tagNameClassMap.containsKey(tagName)) { objectClass = this.tagNameClassMap.get(tagName); hasMapping = true; } } if (hasMapping) { return (T)parseObject(parser, objectClass); } else { try { final Method method = getProcessMethod(element); if (method == null) { return (T)parser.getElementText(); } else { return (T)method.invoke(this, new Object[] { parser }); } } catch (final IllegalAccessException e) { throw new RuntimeException(e); } catch (final InvocationTargetException e) { final Throwable t = e.getTargetException(); if (t instanceof RuntimeException) { throw (RuntimeException)t; } else if (t instanceof Error) { throw (Error)t; } else if (t instanceof IOException) { throw (IOException)t; } else { throw Exceptions.wrap(e); } } } } @SuppressWarnings("unchecked") public <T> T process(final String xml) { try { final StringReader reader = new StringReader(xml); final StaxReader xmlReader = StaxReader.newXmlReader(reader); xmlReader.skipToStartElement(); return (T)process(xmlReader); } catch (final RuntimeException e) { throw e; } catch (final Error e) { throw e; } catch (final Exception e) { throw new RuntimeException("Unable to parse: " + xml, e); } } public void registerEnumConverter(final Class<? extends Enum> enumClass) { final BeanUtilsBean beanUtilsBean = BeanUtilsBean.getInstance(); final ConvertUtilsBean convertUtils = beanUtilsBean.getConvertUtils(); final EnumConverter enumConverter = new EnumConverter(); convertUtils.register(enumConverter, enumClass); } /** * Set the context for processing the XML Document. * * @param context The context for processing the XML Document. */ public final void setContext(final XmlProcessorContext context) { this.context = context; } }