Java tutorial
/** * Copyright 2004-2009 Tobias Gierke <tobias.gierke@code-sourcery.de> * * 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 de.codesourcery.eve.skills.util; import java.beans.IntrospectionException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import org.apache.commons.beanutils.ConvertUtils; /** * Crude XML <-> bean mapper. * * This class uses reflection to map a bean's fields * to XML and vice versa. The generated XML contains * one tag per bean, writing the bean's properties * as attributes. By default, the attribute names * resemble the bean's field names. This can be * changed (to trade readability for speed) by * using the {@link XMLMapper#XMLMapper(Map)} constructor * and providing a property-to-attribute name mapping. * @author tobias.gierke@code-sourcery.de */ public class XMLMapper { private static final String OUTPUT_ENCODING = "ISO-8859-1"; private Map<String, String> propertyNameMappings = new HashMap<String, String>(); private static final String QUOTED_NIL = "\\NIL"; private static final String NIL = "NIL"; /** * Provides converters to map field values to * strings and vice versa. * * @author tobias.gierke@code-sourcery.de */ public interface IFieldConverters { /** * Returns the converter to use for a given field. * * If this method returns <code>null</code> , * the Apache Commons bean-utils converters will * be used for this field. * @param f * @return field converter or <code>null</code> to * use the default converter. */ public IFieldConverter getConverter(Field f); } /** * Converts Objects to strings and vice versa. * * @author tobias.gierke@code-sourcery.de */ public interface IFieldConverter { public String[] toString(Object fieldValue); public Object toObject(String[] values, Class<?> targetType); } public XMLMapper() { } /** * Converter that uses Apache Commons beanutils * to convert bean property values to a string * representation and vice versa. */ public static final IFieldConverter DEFAULT_CONVERTER = new IFieldConverter() { @Override public Object toObject(String[] values, Class<?> targetType) { // filter if (values != null) { final int len = values.length; if (!targetType.isArray()) { if (len != 1) { throw new IllegalArgumentException( "Cannot assign array to non-array type " + targetType.getName()); } if (QUOTED_NIL.equals(values[0])) { return NIL; } else if (NIL.equals(values[0])) { return null; } return ConvertUtils.convert(values[0], targetType); } for (int i = 0; i < len; i++) { if (QUOTED_NIL.equals(values[i])) { values[i] = NIL; } else if (NIL.equals(values[i])) { values[i] = null; } } } return ConvertUtils.convert(values, targetType); } @Override public String[] toString(Object fieldValue) { if (fieldValue == null) { return new String[] { NIL }; } else if (NIL.equals(fieldValue)) { return new String[] { QUOTED_NIL }; } if (fieldValue.getClass().isArray()) { // convert array final int len = Array.getLength(fieldValue); final String[] result = new String[len]; for (int i = 0; i < len; i++) { final Object val = Array.get(fieldValue, i); if (val == null) { result[i] = NIL; } else if (NIL.equals(val)) { result[i] = QUOTED_NIL; } else { result[i] = (String) ConvertUtils.convert(val); } } return result; } if (NIL.equals(fieldValue)) { return new String[] { QUOTED_NIL }; } return new String[] { ConvertUtils.convert(fieldValue) }; } }; private static IFieldConverters DEFAULT_CONVERTERS = new IFieldConverters() { @Override public IFieldConverter getConverter(Field f) { return DEFAULT_CONVERTER; } }; // tmp storage private static final StringBuilder builder = new StringBuilder(); private static String quote(String s) { if (s == null || s.trim().length() == 0) { return s; } final char[] chars = s.toCharArray(); builder.setLength(0); final int len = chars.length; for (int i = 0; i < len; i++) { final char c = chars[i]; if (c == '\\' || c == ',') { builder.append('\\'); } builder.append(c); } return builder.toString(); } public XMLMapper(Map<String, String> propertyNameMappings) { if (propertyNameMappings == null) { throw new IllegalArgumentException("propertyNameMappings cannot be NULL"); } this.propertyNameMappings = propertyNameMappings; } private static final class BeanDescription { private Map<String, Field> fields = new HashMap<String, Field>(); public BeanDescription() { } public void addField(Field f) { this.fields.put(f.getName(), f); } public Collection<Field> getFields() { return fields.values(); } public Field getFieldByName(String name) { final Field result = fields.get(name); if (result == null) { throw new IllegalArgumentException("Unknown field " + name); } return result; } } private BeanDescription createBeanDescription(Class<?> clasz) { BeanDescription result = new BeanDescription(); for (java.lang.reflect.Field f : clasz.getDeclaredFields()) { final int modifiers = f.getModifiers(); if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) { continue; } if (!f.isAccessible()) { f.setAccessible(true); } result.addField(f); } return result; } private static final String toAttributeValue(String[] s) { final StringBuilder builder2 = new StringBuilder(); final int len = s.length; for (int i = 0; i < len; i++) { builder2.append(quote(s[i])); if ((i + 1) < len) { builder2.append(','); } } return builder2.toString(); } private static final String[] fromAttributeValue(String attrValue) { if (QUOTED_NIL.equals(attrValue)) { return new String[] { NIL }; } final List<String> result = new ArrayList<String>(); builder.setLength(0); final char[] chars = attrValue.toCharArray(); final int len = attrValue.length(); for (int i = 0; i < len; i++) { final char c = chars[i]; if (c == '\\') { i++; if (i < len) { builder.append(chars[i]); } continue; } else if (c == ',') { result.add(builder.toString()); builder.setLength(0); } else { builder.append(c); } } if (builder.length() > 0) { result.add(builder.toString()); } return result.toArray(new String[result.size()]); } public void write(Collection<?> beans, OutputStream outstream) throws XMLStreamException, IntrospectionException, IllegalArgumentException, IllegalAccessException { write(beans, DEFAULT_CONVERTERS, outstream); } public void write(Collection<?> beans, IFieldConverters converters, OutputStream outstream) throws XMLStreamException, IntrospectionException, IllegalArgumentException, IllegalAccessException { final XMLOutputFactory factory = XMLOutputFactory.newInstance(); final XMLStreamWriter writer = factory.createXMLStreamWriter(outstream, OUTPUT_ENCODING); try { writer.writeStartDocument(OUTPUT_ENCODING, "1.0"); writer.writeStartElement("rows"); if (beans.isEmpty()) { writer.writeEndDocument(); return; } final Class<?> beanClass = beans.iterator().next().getClass(); final BeanDescription desc = createBeanDescription(beanClass); final Collection<Field> fields = desc.getFields(); if (fields.isEmpty()) { writer.writeEndDocument(); return; } for (Object bean : beans) { writer.writeStartElement("row"); for (Field f : fields) { final Object fieldValue = f.get(bean); final String[] values = converters.getConverter(f).toString(fieldValue); final String attrName = this.propertyNameMappings.get(f.getName()); if (values == null) { writer.writeAttribute(f.getName(), NIL); } else if (attrName == null) { writer.writeAttribute(f.getName(), toAttributeValue(values)); } else { writer.writeAttribute(attrName, toAttributeValue(values)); } } writer.writeEndElement(); } writer.writeEndDocument(); } finally { writer.close(); } } public <T> Collection<T> read(Class<T> clasz, InputStream instream) throws XMLStreamException, IOException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException { return read(clasz, DEFAULT_CONVERTERS, instream); } public <T> Collection<T> read(Class<T> clasz, IFieldConverters converters, InputStream instream) throws XMLStreamException, IOException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException { final Collection<T> result = new ArrayList<T>(); try { final BeanDescription desc = createBeanDescription(clasz); /* * Create inverse mapping attribute name -> field. */ final Map<String, Field> inverseMapping = new HashMap<String, Field>(); if (!this.propertyNameMappings.isEmpty()) { // key = property name / value = attribute name for (Map.Entry<String, String> propToAttribute : this.propertyNameMappings.entrySet()) { inverseMapping.put(propToAttribute.getValue(), desc.getFieldByName(propToAttribute.getKey())); } } else { // create default mappings for (Field f : desc.getFields()) { inverseMapping.put(f.getName(), f); } } final int fieldCount = desc.getFields().size(); final XMLInputFactory factory = XMLInputFactory.newInstance(); final XMLStreamReader parser = factory.createXMLStreamReader(instream); boolean inRow = false; final Constructor<T> constructor = clasz.getConstructor(new Class<?>[0]); for (int event = parser.next(); event != XMLStreamConstants.END_DOCUMENT; event = parser.next()) { switch (event) { case XMLStreamConstants.START_ELEMENT: if ("row".equals(parser.getLocalName())) { // parse row if (inRow) { throw new XMLStreamException("Found nested <row> tag ?", parser.getLocation()); } inRow = true; final T bean = constructor.newInstance(new Object[0]); for (int i = 0; i < fieldCount; i++) { final String attrName = parser.getAttributeLocalName(i); final String attrValue = parser.getAttributeValue(i); final Field field = inverseMapping.get(attrName); if (!NIL.equals(attrValue)) { final Object fieldValue = converters.getConverter(field) .toObject(fromAttributeValue(attrValue), field.getType()); field.set(bean, fieldValue); } else { field.set(bean, null); } } result.add(bean); } break; case XMLStreamConstants.END_ELEMENT: if ("row".equals(parser.getLocalName())) { // parse row if (!inRow) { throw new XMLStreamException("Found </row> tag without start tag at ", parser.getLocation()); } inRow = false; } break; } } } finally { instream.close(); } return result; } }