Java tutorial
/**************************************************************************** * Copyright 2009-2015 Jean-Philippe Gravel, P. Eng. CSDP * * 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.formix.dsx.serialization; import java.lang.reflect.Method; import java.sql.Timestamp; import java.text.ParseException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import org.apache.commons.codec.binary.Base64; import org.formix.dsx.XmlContent; import org.formix.dsx.XmlElement; import org.formix.dsx.XmlException; import org.formix.dsx.XmlText; /** * Serializes an object into its XML representation. * * @author jpgravel * */ public class XmlSerializer { private HashMap<Class<?>, Class<?>> collectionMap; private SortedMap<String, Class<?>> generalTypeMapper; private List<String> classResolutionPackages; private List<SerializationEventListener> deserializationListeners; /** * Creates an instance of XmlSerializer. */ public XmlSerializer() { this.generalTypeMapper = new TreeMap<String, Class<?>>(); this.collectionMap = new HashMap<Class<?>, Class<?>>(); this.collectionMap.put(Collection.class, ArrayList.class); this.collectionMap.put(List.class, ArrayList.class); this.collectionMap.put(Set.class, HashSet.class); this.collectionMap.put(SortedSet.class, TreeSet.class); this.classResolutionPackages = new ArrayList<String>(); this.deserializationListeners = new ArrayList<SerializationEventListener>(); } /** * Gets deserialization listeners. * * @return deserialization listeners. */ public List<SerializationEventListener> getDeserializationListeners() { return deserializationListeners; } /** * Add a new deserialization listener. * * @param l * the listener added. */ public void addDeserializationListener(SerializationEventListener l) { this.deserializationListeners.add(l); } /** * Remove a deserialization listener. * * @param l * The serialization listener to add. */ public void removeDeserializationListener(SerializationEventListener l) { this.deserializationListeners.remove(l); } /** * Returns a map between interfaces and class implementations for any * collection types. * * Default mappings: * * Collection -> ArrayList, List -> ArrayList, Set -> HashSet, * SortedSet -> TreeSet * * @return a map defining which class to instantiate when a Collection is * encountered in a class member. */ public HashMap<Class<?>, Class<?>> getCollectionMap() { return this.collectionMap; } /** * Returns the list of packages to loop into when deserializing an object * and trying to instanciate a type with a simple class name. * * @return class resolution package names. */ public List<String> getClassResolutionPackages() { return this.classResolutionPackages; } /** * Used to quickly map a class simple name to the corresponding Class type. * Mandatory for classes having the XmlRootElement annotation. In the later * case, the XmlRootElement.name (first character capitalized) is used to * map with the class correct type, possibly overriding an existing type * previously mapped during class resolution. * * @param type * the type to be added to the general type mapper of the current * serializer. */ public void registerClass(Class<?> type) { String rootName = type.getSimpleName(); XmlRootElement xmlRootAnnot = type.getAnnotation(XmlRootElement.class); if (xmlRootAnnot != null) rootName = this.capitalize(xmlRootAnnot.name()); this.generalTypeMapper.put(rootName, type); } /** * Add a package to the list of lookup packages. * * @param packageName * the package name to add. */ public void registerPackage(String packageName) { this.classResolutionPackages.add(packageName); } /** * Deserialize the given XML into an object. * * @param root * The root node of the XML to deserialize. * * @return the deserialized object. * * @throws XmlException * Thrown when a problem occurs during deserialization. */ public Object deserialize(XmlElement root) throws XmlException { String simpleName = this.capitalize(root.getName()); Class<?> type; try { type = this.getType(simpleName); } catch (ClassNotFoundException e) { throw new XmlException("Unable to resolve to a type " + "with the given XmlElement root name '" + root.getName() + ". Try to add the right package containing the class " + simpleName + " or use the method XmlSerializer.desirialize(XmlElement, Class<?>) " + "signature instead.", e); } return this.deserialize(root, type); } /** * Deserialize an XML tree into the given type. * * @param root * The XML root element. * * @param type * The type of the desired object. * * @param <T> * The infered type for the parameter {code type}. * * @return a deserialized object from the given XML. * * @throws XmlException * Thrown when a problem occurs during deserialization. */ public <T> T deserialize(XmlElement root, Class<T> type) throws XmlException { if (type.toString().equals("class [B")) { @SuppressWarnings("unchecked") T value = (T) Base64.decodeBase64(root.getChild(0).toString()); return value; } // If the type is a base type from java.util or java.sql or is a // collection then decode it directly. if (type.getName().startsWith("java.") || Collection.class.isAssignableFrom(type)) { @SuppressWarnings("unchecked") T value = (T) this.getValue(root, type, null, null); return value; } String rootName = this.capitalize(root.getName()); String childName = ""; XmlElement childElem = null; if (root.getChild(0) instanceof XmlElement) { childElem = (XmlElement) root.getChild(0); childName = this.capitalize(childElem.getName()); if (rootName.equals(childName)) return this.deserialize(childElem, type); } T target; try { target = type.newInstance(); } catch (Exception e) { throw new XmlException("Unable to instanciate type " + type.getName(), e); } this.onBeforeDeserialization(new SerializationEvent(this, root, target)); SortedMap<String, Method> methods = this.createMethodMap(type); for (XmlContent content : root.getChilds()) { if (content instanceof XmlElement) { XmlElement elem = (XmlElement) content; String methodName = "set" + this.capitalize(elem.getName()); String signature = methods.tailMap(methodName).firstKey(); // If the setter is not found for the specified methodName, skip // this setter. if (signature.startsWith(methodName)) { Method setMethod = methods.get(signature); Class<?> paramType = setMethod.getParameterTypes()[0]; try { Object value = this.getValue(elem, paramType, methods, target); setMethod.invoke(target, new Object[] { value }); } catch (Exception e) { String msg = String.format( "Unable to assign the XMLElement %s to" + " the property [%s.%s] (%s)", elem, type.getName(), setMethod.getName(), signature); throw new XmlException(msg, e); } } else { throw new XmlException(String.format("Unable to find the method %s.", methodName)); } } } this.onAfterDeserialization(new SerializationEvent(this, root, target)); return target; } /** * Method called to fire the beforeDeserialization events. * * @param e * the serialization event. */ protected void onBeforeDeserialization(SerializationEvent e) { for (SerializationEventListener l : this.deserializationListeners) { l.beforeDeserialisation(e); } } /** * Method called to fire the afterDeserialization events. * * @param e * the serialization event. */ protected void onAfterDeserialization(SerializationEvent e) { for (SerializationEventListener l : this.deserializationListeners) { l.afterDeserialisation(e); } } /* * Returns the value of elem as an object of type paramType. The methods and * parent parameters are used in the case that paramType is a subclass of * the Collection type. * * @see XmlSerializer.decodeCollection for further details. * * @param elem The xml element to be instantiated to an object of the given * paramType. * * @param paramType The type to be created by the getValue. * * @param parentMethods The method list of the parent. * * @param parent The instantiated object that will contain the instantiated * elem. * * @return * * @throws XmlException */ private Object getValue(XmlElement elem, Class<?> paramType, SortedMap<String, Method> parentMethods, Object parent) throws XmlException { try { SortedMap<String, Method> methods = this.createMethodMap(paramType); Method paramTypeValueOfMethod = null; if (methods.containsKey("valueOf-String")) paramTypeValueOfMethod = methods.get("valueOf-String"); if (elem.getChilds().size() == 0) { if (Collection.class.isAssignableFrom(paramType)) { return this.getCollectionValue(elem, paramType, parentMethods, parent); } else { return null; } } Object value = null; Iterator<XmlContent> contentIterator = elem.getChilds().iterator(); XmlContent firstChild = contentIterator.next(); while (firstChild.toString().trim().equals("") && contentIterator.hasNext()) { // skip blank lines firstChild = contentIterator.next(); } if (paramType.equals(String.class)) { XmlText text = (XmlText) firstChild; value = text.getText(); } else if (paramType.equals(Timestamp.class)) { value = new Timestamp(this.parseDate(firstChild.toString()).getTime()); } else if (paramType.equals(Date.class)) { value = this.parseDate(firstChild.toString()); } else if (Calendar.class.isAssignableFrom(paramType)) { value = new GregorianCalendar(); ((GregorianCalendar) value).setTime(this.parseDate(firstChild.toString())); } else if (paramTypeValueOfMethod != null) { value = paramTypeValueOfMethod.invoke(null, new Object[] { firstChild.toString() }); } else if (Collection.class.isAssignableFrom(paramType)) { value = this.getCollectionValue(elem, paramType, parentMethods, parent); } else if (Map.class.isAssignableFrom(paramType)) { throw new XmlException("The Map deserialization is not yet implemented."); } else if (this.isSetter(parentMethods, elem.getName()) && (firstChild instanceof XmlElement)) { XmlElement elemFirstChild = (XmlElement) firstChild; Class<?> specifiedType = this.getType(this.capitalize(elemFirstChild.getName())); value = this.deserialize(elemFirstChild, specifiedType); } else { value = this.deserialize(elem, paramType); } return value; } catch (Exception e) { throw new XmlException("Problem getting value of " + elem + " of type " + paramType.getName(), e); } } private boolean isSetter(SortedMap<String, Method> parentMethods, String elemName) { String methodName = "set" + this.capitalize(elemName); String signature = parentMethods.tailMap(methodName).firstKey(); return signature.startsWith(methodName + "-"); } /** * Parse a date. The defeualt implementation expects the following format: * ISO 8601: 1994-11-05T08:15:30-05:00 * * @param dateString * the date string to deserialize. * * @return the corresponding date object. * * @throws ParseException * Thrown if a parsing problem occurs. */ protected Date parseDate(String dateString) throws ParseException { Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})([+-]\\d{2}:?\\d{2})?"); Matcher m = p.matcher(dateString); if (m.find()) { int[] dateTimeParts = new int[6]; String timeZone = ""; for (int g = 1; g <= m.groupCount(); g++) { String group = m.group(g); if (g < 7) dateTimeParts[g - 1] = Integer.parseInt(group); else if (m.group(g) != null) timeZone = m.group(g); } Calendar cal = Calendar.getInstance(); TimeZone tz = TimeZone.getTimeZone("GMT" + timeZone); cal.setTimeZone(tz); cal.set(Calendar.YEAR, dateTimeParts[0]); cal.set(Calendar.MONTH, dateTimeParts[1] - 1); cal.set(Calendar.DAY_OF_MONTH, dateTimeParts[2]); cal.set(Calendar.HOUR_OF_DAY, dateTimeParts[3]); cal.set(Calendar.MINUTE, dateTimeParts[4]); cal.set(Calendar.SECOND, dateTimeParts[5]); return cal.getTime(); } else { return null; } } /** * Format a date to the desired format. The default implementation use the * following format: ISO 8601: 1994-11-05T08:15:30-05:00. * * @param date * The date to format. * * @return the string representation of the date. */ protected String formatDate(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); String dateString = String.format("%1$tFT%1$tT", cal); String timeZone = String.format("%tz", cal); timeZone = timeZone.substring(0, 3) + ":" + timeZone.substring(3); dateString += timeZone; return dateString; } /* * This method is called when paramType is an instance of * java.util.Collection. The created collection is then filled with the * child elements. * * @param elem The element representing a collection. * * @param paramType The type of the desired collection. Before instanciating * a collection using the internal collectionMap, the algorithm tries to * obtain the collection from the current parent object using the method * named "get" + capitalize(elem.getName()). If the call succeeds, the * existing collection is used. Otherwise, a new collection is instantiated * using the collectionMap definitions. * * @param parentMethods The method map (<method name>, Method) of the parent * object. This map is used to obtain the an existing collection object in * the parent getter corresponding to the current elem name. * * @param parent The parent object used to get an existing collection, if * any. * * @return a collection filled with the corresponding child elements. * * @throws XmlException */ @SuppressWarnings("unchecked") private Collection<Object> getCollectionValue(XmlElement elem, Class<?> paramType, SortedMap<String, Method> parentMethods, Object parent) throws XmlException { Collection<Object> col = null; if ((parentMethods != null) && (parent != null)) { String methodName = "get" + this.capitalize(elem.getName()); String signature = parentMethods.tailMap(methodName).firstKey(); Method colGetterMethod = parentMethods.get(signature); try { col = (Collection<Object>) colGetterMethod.invoke(parent, (Object[]) null); } catch (Exception e) { throw new XmlException("Unable to invoke collection getter: " + colGetterMethod.getName(), e); } } try { if (col == null) col = this.createCollection(paramType); } catch (Exception e) { throw new XmlException("Unable to create collection type: " + paramType.getName(), e); } for (XmlContent colContent : elem.getChilds()) { if (colContent instanceof XmlElement) { XmlElement colElem = (XmlElement) colContent; Object objToAdd = null; Class<?> colElemType = null; String colElemTypeName = this.capitalize(colElem.getName()); try { colElemType = this.getType(colElemTypeName); } catch (ClassNotFoundException e) { throw new XmlException( "Unable to find the collection's internal element type: " + colElemTypeName, e); } objToAdd = this.deserialize(colElem, colElemType); col.add(objToAdd); } } return col; } /** * Serializes an object into its XML representation. * * @param target * The object to serialize. * * @return The root XmlElement of the serialized object. * * @throws XmlException * Thrown if a serialization problem occurs. */ public XmlElement serialize(Object target) throws XmlException { Class<?> type = target.getClass(); String rootName = decapitalize(type.getSimpleName()); return this.serialize(target, rootName); } /** * Serializes an object with a different XmlElement root name. * * @param target * the object to serialize. * * @param rootName * the desired root node name. * * @return The root element of the resulting serialization. * * @throws XmlException * Thrown if a serialization problem occurs. */ public XmlElement serialize(Object target, String rootName) throws XmlException { Class<?> type = target.getClass(); XmlRootElement xmlRootAnnot = type.getAnnotation(XmlRootElement.class); if (xmlRootAnnot != null) rootName = xmlRootAnnot.name(); XmlElement root = new XmlElement(rootName); // If the type is a base type from java.util or java.sql then encode it // directly. if (type.getName().startsWith("java.") || (target instanceof Collection)) { this.addXmlContent(root, target); return root; } if (target instanceof Object[]) { this.addXmlContent(root, target); return root; } for (Method method : type.getMethods()) { // Ignores XmlTransient methods. if (method.getAnnotation(XmlTransient.class) != null) continue; String methodName = method.getName(); if ((methodName.startsWith("get") || methodName.startsWith("is")) && !method.getName().equals("getClass")) { // If the get method has a parameter, it may be a shortcut // accessor to an internal map or collection. We skip this // method. if (method.getParameterTypes().length > 0) continue; Object value = null; try { value = method.invoke(target, (Object[]) null); } catch (Exception e) { throw new XmlException(e); } // We don't want null nodes. if (value == null) continue; int idx = 3; if (methodName.startsWith("is")) { idx = 2; } String elemName = decapitalize(method.getName().substring(idx)); XmlElement elem = root.addElement(elemName); // Class<?> retType = method.getReturnType(); this.addXmlContent(elem, value); } } this.registerClass(type); return root; } private void addXmlContent(XmlElement parent, Object value) throws XmlException { if (value == null) { return; } if ((value instanceof Number) || (value instanceof String) || (value instanceof Boolean)) { parent.addText(value.toString()); } else if (value instanceof Date) { parent.addText(this.formatDate((Date) value)); } else if (value instanceof Calendar) { parent.addText(this.formatDate(((Calendar) value).getTime())); } else if (value instanceof Enum) { parent.addText(value.toString()); } else if (value instanceof Collection) { Collection<?> items = (Collection<?>) value; for (Object item : items) parent.addChild(this.serialize(item)); } else if (value instanceof Map) { throw new XmlException("The Map serialization is not yet implemented."); } else if (value instanceof byte[]) { parent.addText(Base64.encodeBase64String((byte[]) value)); } else if (value instanceof Object[]) { for (Object o : (Object[]) value) { parent.addChild(this.serialize(o)); } } else { parent.addChild(this.serialize(value)); } } private String decapitalize(String value) { return value.substring(0, 1).toLowerCase() + value.substring(1); } private String capitalize(String value) { return value.substring(0, 1).toUpperCase() + value.substring(1); } private SortedMap<String, Method> createMethodMap(Class<?> type) { SortedMap<String, Method> map = new TreeMap<String, Method>(); for (Method m : type.getMethods()) { map.put(this.createMethodSignature(m), m); } return map; } private String createMethodSignature(Method m) { String signature = m.getName(); for (Class<?> type : m.getParameterTypes()) signature += "-" + type.getSimpleName(); return signature; } @SuppressWarnings("unchecked") private Collection<Object> createCollection(Class<?> type) throws InstantiationException, IllegalAccessException { Class<?> colMappedType = this.collectionMap.get(type); if (colMappedType != null) return (Collection<Object>) colMappedType.newInstance(); // If the type isn't mapped, maybe this is the direct collection type // implementation. Return the type instantiation. return (Collection<Object>) type.newInstance(); } private Class<?> getType(String simpleName) throws ClassNotFoundException { if (this.generalTypeMapper.containsKey(simpleName)) return this.generalTypeMapper.get(simpleName); List<String> packages = new ArrayList<String>(this.classResolutionPackages); packages.add("java.lang"); packages.add("java.util"); packages.add("java.sql"); for (String packageName : packages) { String className = packageName + "." + simpleName; try { Class<?> type = Class.forName(className); this.registerClass(type); return this.generalTypeMapper.get(simpleName); } catch (Exception e) { // In this algorithm, catching an exception and doing nothing // with it is a normal behavior. This is a violation of the // Exception Pattern which says that an exception should be // exceptional and handled properly. // // This "bad" algorithm is a quick and simple (for the // programmer(myself)) but rather slow (at runtime) replacement // for the correct implementation that should look for the // specific classes in the related class path defined by // packageSearchOrder and instantiate it if found. // // Greatly improved efficiency through the usage of the // generalTypeMapper but still not a good pattern for class // resolution. } } throw new ClassNotFoundException( "Unable to instanciate the given type " + simpleName + " with the internal packageSearchOrder " + this.classResolutionPackages + ". Please add the correct package for the type " + simpleName + " using XmlSerializer.getPackageSearchOrder().add(\"{package_name}\")"); } }