/* * Copyright 2002-2004 the original author or authors. * * 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.springframework.jms; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.math.BigDecimal; import java.sql.Timestamp; import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.TimeZone; import javax.jms.JMSException; import javax.jms.MapMessage; import javax.jms.Message; import javax.jms.Session; import org.springframework.beans.factory.InitializingBean; import org.springframework.jms.support.converter.MessageConversionException; import org.springframework.jms.support.converter.MessageConverter; /** * Converts between a JavaBean and a MapMessage. * @author Mark Pollack * @author Jawaid Hakim */ public class MapMessageConverter implements MessageConverter, InitializingBean { //TODO refactor to use Spring bean package classes... private static Map primitiveTypes = new HashMap(); private static Map primitiveWrapperTypes = new HashMap(); private static Map primitiveWrapperTypesArray = new HashMap(); private static Map primitiveTypesArray = new HashMap(); private static final Object[] EMPTY_PARAMS = { }; private static final ThreadLocal DATETIME_FORMAT_THREADLOCAL = new ThreadLocal(); private static final ThreadLocal BIGDECIMAL_FORMAT_THREADLOCAL = new ThreadLocal(); static { primitiveTypes.put("short", ""); primitiveTypes.put("int", ""); primitiveTypes.put("long", ""); primitiveTypes.put("float", ""); primitiveTypes.put("double", ""); primitiveTypes.put("byte", ""); primitiveTypes.put("boolean", ""); primitiveTypes.put("char", ""); primitiveWrapperTypes.put("java.lang.Short", ""); primitiveWrapperTypes.put("java.lang.Integer", ""); primitiveWrapperTypes.put("java.lang.Long", ""); primitiveWrapperTypes.put("java.lang.Float", ""); primitiveWrapperTypes.put("java.lang.Double", ""); primitiveWrapperTypes.put("java.lang.Byte", ""); primitiveWrapperTypes.put("java.lang.Boolean", ""); primitiveWrapperTypes.put("java.lang.Char", ""); primitiveTypesArray.put("[S", ""); primitiveTypesArray.put("[I", ""); primitiveTypesArray.put("[L", ""); primitiveTypesArray.put("[F", ""); primitiveTypesArray.put("[D", ""); primitiveTypesArray.put("[B", ""); primitiveTypesArray.put("[Z", ""); primitiveTypesArray.put("[C", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Short;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Integer;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Long;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Float;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Double;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Byte;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Boolean;", ""); primitiveWrapperTypesArray.put("[Ljava.lang.Char;", ""); } private Map beanInfo = new HashMap(); private String dateFormatPattern = "EEE, d MMM yyyy HH:mm:ss z"; private String extensionPropertyName = "JMS_TIBCO_MSG_EXT"; private String packageName; private String unqualifiedClassnameFieldName = "uqn__"; private boolean arraySupportEnabled = false; private boolean lenient = true; private boolean nestedMessageSupportEnabled = true; /** * Constructor for normal bean usage. * @param packageName The package name to prepend to the unqualified name contained in the * MapMessage. */ public MapMessageConverter(String packageName) { setPackageName(packageName); } /** * Set the support level for Arrays. * @param b if true Arrays are supported. */ public void setArraySupportEnabled(boolean b) { arraySupportEnabled = b; } /** * Does the JMS provider support use of Array types in a MapMessage. * Default value is false. * @return true if provider supprts Array types, false otherwise. */ public boolean isArraySupportEnabled() { return arraySupportEnabled; } /** * Set the dateformat pattern. * @param pattern pattern to use. */ public void setDateFormatPatter(String pattern) { dateFormatPattern = pattern; } /** * Get the data format pattern. The default date format pattern is based on * the Internet Engineering Task Force (IETF) Request for Comments (RFC) * 1123 and is <code>EEE, d MMM yyyy HH:mm:ss z</code>. * * @return Date format pattern */ public String getDateFormatPattern() { return dateFormatPattern; } /** * @param string */ public void setExtensionPropertyName(String string) { extensionPropertyName = string; } /** * Set the name of a boolean JMS property to set when using MapMessage features * that are not specified in the specification but supported by providers, such as * nested map messages or arrays. * @return Name of the JMS Property to use to flag the message as an extended message. */ public String getExtensionPropertyName() { return extensionPropertyName; } /** * Set the lenient setting for the date formatter. * @param b true or false. */ public void setLenient(boolean b) { lenient = b; } /** * Get the lenient setting for the date formatter. Default locale is <tt>true</tt>. * * @return Lenient setting. */ public boolean getLenient() { return lenient; } /** * Set the support level for nested MapMessages * @param b if true, nested messages are supported. */ public void setNestedMessageSupportEnabled(boolean b) { nestedMessageSupportEnabled = b; } /** * Does the JMS provider support the use of nested MapMessages. * Default value is true. * @return true if provider supports nested MapMessages, false otherwise. */ public boolean isNestedMessageSupportEnabled() { return nestedMessageSupportEnabled; } /** * Set the package name that will be used to construct a fully * qualified classname from the unqualified name contained in * the message during unmarshalling. A trailing '.' is removed if * provided. * * @param name The name of the package to construct a FQN. */ public void setPackageName(String name) { if (name.endsWith(".")) { name = name.substring(0, name.lastIndexOf(".")); } packageName = name; } /** * Get the name of the package that is used to construct a FQN * when unmarshalling. * @return package name used during unmarshalling. */ public String getPackageName() { return packageName; } /** * Set the name of the field that will be used to identify the * unqualified classname of the marshalled Java object * @param string fieldname containing unqualified classname. */ public void setUnqualifiedClassnameFieldName(String string) { unqualifiedClassnameFieldName = string; } /** * Get the name of the field used to idientify the * unqualified classname of the marshalled Java object. * @return */ public String getUnqualifiedClassnameFieldName() { return unqualifiedClassnameFieldName; } public void afterPropertiesSet() throws Exception { if (getPackageName() == null) { throw new IllegalArgumentException("packageName is required"); } } public MapMessageConverter.RBeanInfo createBeanInfo( String name, Class bean) { try { RBeanInfo info = (RBeanInfo) beanInfo.get(name); if (info != null) { return info; } if (bean == null) { return null; } BeanInfo bi = Introspector.getBeanInfo(bean); Map getters = new HashMap(); Map setters = new HashMap(); PropertyDescriptor[] props = bi.getPropertyDescriptors(); for (int i = 0; i < props.length; ++i) { PropertyDescriptor prop = props[i]; String propName = prop.getName(); Method readMethod = prop.getReadMethod(); Method writeMethod = prop.getWriteMethod(); if ((readMethod == null) || (writeMethod == null) || !(!propName.startsWith("get") && !propName.startsWith("set"))) { continue; } String ucPropName = propName.substring(0, 1).toUpperCase() + propName.substring(1); getters.put(ucPropName, readMethod); setters.put(ucPropName, writeMethod); } return new RBeanInfo(name, getters, setters); } catch (IntrospectionException e) { return new RBeanInfo( bean.getClass().getName(), new HashMap(), new HashMap()); } } public Object fromMessage(Message message) { if (!(message instanceof MapMessage)) { throw new MessageConversionException("Did not provide MapMessageConverter with a MapMessage"); } return unmarshal((MapMessage) message); } public Message toMessage(Object object, Session session) { try { Class beanClass = object.getClass(); RBeanInfo beanInfo = createBeanInfo(beanClass.getName(), beanClass); MapMessage nestedMsg = session.createMapMessage(); nestedMsg.setString( getUnqualifiedClassnameFieldName(), getBeanClassName(beanInfo.getName())); for (Iterator getters = beanInfo.getGetters().values().iterator(); getters.hasNext(); ) { Method method = (Method) getters.next(); Object val = method.invoke(object, EMPTY_PARAMS); if (val != null) { String fldName = method.getName().substring(3); Class valType = val.getClass(); if (valType.equals(String.class)) { nestedMsg.setObject(fldName, val.toString()); } else if (isPrimitiveWrapperType(valType)) { nestedMsg.setObject(fldName, val); } else if (valType.equals(BigDecimal.class)) { nestedMsg.setObject( fldName, getBigDecimalFormatter().format(val)); } else if (valType.equals(Date.class)) { String sVal = getDateTimeFormatter().format((Date) val); nestedMsg.setObject(fldName, sVal); } else if (valType.equals(Timestamp.class)) { Timestamp realVal = (Timestamp) val; String sVal = getDateTimeFormatter().format( new Date( realVal.getTime() + (realVal.getNanos() / 1000000))); nestedMsg.setObject(fldName, sVal); } else if (valType.isArray()) { if (isArraySupportEnabled()) { int len = Array.getLength(val); nestedMsg.setBooleanProperty( getExtensionPropertyName(), true); Class arrayType = val.getClass(); if (isPrimitiveArray(arrayType) || isPrimitiveWrapperArray(arrayType)) { // TODO: assumes that arrays of primitives and // primitive wrappers are supported by the JMS // provider nestedMsg.setObject(fldName, val); continue; } MapMessage arrayMsg = session.createMapMessage(); int realLen = 0; for (int i = 0; i < len; ++i) { Object elem = Array.get(val, i); if (elem != null) { String index = String.valueOf(i); if (elem.getClass().equals(String.class)) { arrayMsg.setObject(index, elem); } else if ( valType.equals(BigDecimal.class)) { arrayMsg.setObject( index, getBigDecimalFormatter().format( elem)); } else if ( elem.getClass().equals(Date.class)) { arrayMsg.setObject( index, getDateTimeFormatter().format( (Date) elem)); } else if ( elem.getClass().equals( Timestamp.class)) { Timestamp realVal = (Timestamp) elem; arrayMsg.setObject( index, getDateTimeFormatter().format( new Date( realVal.getTime() + (realVal.getNanos() / 1000000)))); } else { // TODO: assumes that this is a bean if (realLen == 0) { arrayMsg.setBooleanProperty( getExtensionPropertyName(), true); } arrayMsg.setObject( String.valueOf(realLen), toMessage(elem, session)); } ++realLen; } } arrayMsg.setIntProperty("length", realLen); nestedMsg.setObject(fldName, arrayMsg); } } else if (val instanceof Message) { if (isNestedMessageSupportEnabled()) { nestedMsg.setBooleanProperty( getExtensionPropertyName(), true); nestedMsg.setObject(fldName, val); } } else if (val instanceof java.util.Collection) { // TODO // throw new ConverterException("Cannot marshal // collection classes."); } else { // Handle everything else like a bean nestedMsg.setBooleanProperty( getExtensionPropertyName(), true); nestedMsg.setObject(fldName, toMessage(val, session)); } } } return nestedMsg; } catch (Exception ex) { throw new MessageConversionException("error", ex); } } /** * Get the number formatter. This returns a Threadsafe <tt>NumberFormat</tt> * instance. * * @return Threadsafe <tt>DateFormat</tt> instance. The <tt>TimeZone</tt> * of the <tt>DateFormat</tt> instance if set to <tt>GMT</tt> * and the parsing is set to <tt>lenient</tt>. */ protected NumberFormat getBigDecimalFormatter() { NumberFormat numberFmt = (NumberFormat) BIGDECIMAL_FORMAT_THREADLOCAL.get(); if (numberFmt == null) { numberFmt = NumberFormat.getNumberInstance(); BIGDECIMAL_FORMAT_THREADLOCAL.set(numberFmt); } return numberFmt; } /** * Get the date formatter. This returns a Threadsafe <tt>DateFormat</tt> * instance. * * @return Threadsafe <tt>DateFormat</tt> instance. The <tt>TimeZone</tt> * of the <tt>DateFormat</tt> instance if set to <tt>GMT</tt> * and the parsing is set to <tt>lenient</tt>. */ protected DateFormat getDateTimeFormatter() { DateFormat dateFmt = (DateFormat) DATETIME_FORMAT_THREADLOCAL.get(); if (dateFmt == null) { dateFmt = new SimpleDateFormat(getDateFormatPattern()); dateFmt.setTimeZone(TimeZone.getTimeZone("GMT")); dateFmt.setLenient(getLenient()); DATETIME_FORMAT_THREADLOCAL.set(dateFmt); } return dateFmt; } /** * Get the unqualified name of the bean class. * * @param fullName * Fully qualified name of the bean class. This can also be the * unqualified name. * @return Unqualified name of the bean class. */ private static String getBeanClassName(String fullName) { int index = fullName.lastIndexOf("."); return fullName.substring(index + 1); } /** * Determine if a bean attribute is an array of <tt>primitive</tt> types. * A primitive array type can be directly inserted into a * <tt>MapMessage</tt>. * * @param cls * Bean class. * @return Returns <tt>true</tt> if the bean class is an array of * <tt>primitive</tt> types. Otherwise, returns <tt>false</tt>. */ private static boolean isPrimitiveArray(Class cls) { String name = cls.getName(); return primitiveTypesArray.containsKey(name); } /** * Determine if a bean class is an array of primitive wrapper type. A * primitive wrapper array type can be directly inserted into a * <tt>MapMessage</tt>. * * @param cls * Bean class. * @return Returns <tt>true</tt> if the bean class is an array of * primitive wrapper types. Otherwise, returns <tt>false</tt>. */ private static boolean isPrimitiveWrapperArray(Class cls) { String name = cls.getName(); return primitiveWrapperTypesArray.containsKey(name); } /** * Determine if a bean class is a primitive wrapper type. A primitive * wrapper type can be directly inserted into a <tt>MapMessage</tt>. * * @param cls * Bean class. * @return Returns <tt>true</tt> if the bean class is a supported * non-primitive type. Otherwise, returns <tt>false</tt>. */ private static boolean isPrimitiveWrapperType(Class cls) { String name = cls.getName(); return primitiveTypes.containsKey(name) || primitiveWrapperTypes.containsKey(name); } private Object unmarshal(MapMessage source) { String unqualifiedName; try { unqualifiedName = source.getString(getUnqualifiedClassnameFieldName()); } catch (JMSException e) { throw new MessageConversionException( "Could not find field named " + this.unqualifiedClassnameFieldName + "in message when unmarshalling", e); } if (unqualifiedName == null) { throw new MessageConversionException("Unqualified classname field not found"); } String fullName = getPackageName() + "." + unqualifiedName; return unmarshal(source, unqualifiedName, fullName); } /** * Unmarshal a bean from a JMS message. * * @param source * JMS message. * @param beanName * Short name of bean. * @param fullName * Full name of bean (includes the package). * @return New bean instance. * @throws ConverterException */ private Object unmarshal( MapMessage source, String beanName, String fullName) throws MessageConversionException { try { Object bean = Class.forName(fullName).newInstance(); // Set bean properties RBeanInfo beanInfo = createBeanInfo(beanName, bean.getClass()); for (Iterator setters = beanInfo.getSetters().values().iterator(); setters.hasNext(); ) { Method method = (Method) setters.next(); String fldName = method.getName().substring(3); if (source.itemExists(fldName)) { Object val = source.getObject(fldName); Class[] methodParameters = method.getParameterTypes(); Class valType = methodParameters[0]; if (valType.equals(String.class)) { Object[] parameters = { val.toString()}; method.invoke(bean, parameters); } else if (isPrimitiveWrapperType(valType)) { Object[] parameters = { val }; method.invoke(bean, parameters); } else if (valType.equals(BigDecimal.class)) { Object[] parameters = { new BigDecimal(val.toString())}; method.invoke(bean, parameters); } else if (valType.equals(Date.class)) { Object[] parameters = { getDateTimeFormatter().parse(val.toString())}; method.invoke(bean, parameters); } else if (valType.equals(Timestamp.class)) { Object[] parameters = { new Timestamp( getDateTimeFormatter() .parse(val.toString()) .getTime())}; method.invoke(bean, parameters); } else if (valType.isArray()) { if (isPrimitiveArray(valType)) { if (isArraySupportEnabled()) { Object[] parameters = { val }; method.invoke(bean, parameters); } } else if (isPrimitiveWrapperArray(valType)) { if (isArraySupportEnabled()) { Object[] parameters = { val }; method.invoke(bean, parameters); } } else { // BigDecimal, String, Date, or Bean array MapMessage arrayMsg = (MapMessage) val; int len = arrayMsg.getIntProperty("length"); String arrayClsName = valType.getName(); Object newArray = Array.newInstance( Class.forName( arrayClsName.substring( 2, arrayClsName.indexOf(';'))), len); for (int i = 0; i < len; ++i) { Object elem = arrayMsg.getObject(String.valueOf(i)); if (arrayClsName .equals("[Ljava.lang.String;")) { Array.set(newArray, i, elem); } else if ( arrayClsName.equals( "[Ljava.math.BigDecimal;")) { Array.set( newArray, i, new BigDecimal(elem.toString())); } else if ( arrayClsName.equals("[Ljava.util.Date;")) { Array.set( newArray, i, getDateTimeFormatter().parse( elem.toString())); } else if ( arrayClsName.equals( "[Ljava.sql.Timestamp;")) { Array.set( newArray, i, new Timestamp( getDateTimeFormatter() .parse(elem.toString()) .getTime())); } else if (elem instanceof MapMessage) { //TODO assumes that this is a bean Array.set( newArray, i, unmarshal((MapMessage) elem)); } } Object[] parameters = { newArray }; method.invoke(bean, parameters); } } else if (val instanceof MapMessage) { if (isNestedMessageSupportEnabled()) { MapMessage msg = (MapMessage) val; String beanFieldName = msg.getString( getUnqualifiedClassnameFieldName()); if (beanFieldName != null) { Object[] parameters = { unmarshal(msg)}; method.invoke(bean, parameters); } else { Object[] parameters = { msg }; method.invoke(bean, parameters); } } } else { Object[] parameters = { val }; method.invoke(bean, parameters); } } } return bean; } catch (Exception ex) { throw new MessageConversionException( "Exception unmarshalling message", ex); } } public class RBeanInfo { private final Map getters_; private final Map setters_; private final String name_; public RBeanInfo(String name, Map getters, Map setters) { name_ = name; getters_ = getters; setters_ = setters; } public Map getGetters() { return getters_; } public String getName() { return name_; } public Map getSetters() { return setters_; } } }