AMQP-277 Add DefaultType to DefaultClassMapper

Permit the use of a JsonMessageConverter when no type information
is provided in the MessageProperties and the type is known.
This commit is contained in:
Gary Russell
2012-11-06 07:32:44 -05:00
parent 9c29a74add
commit 8f533c86f5
5 changed files with 123 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2012 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
@@ -22,21 +22,37 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ClassUtils;
/**
*
* Maps to/from JSON using type information in the {@link MessageProperties};
* the default name of the message property containing the type is '__TypeId__'.
* An optional property {@link #setDefaultType(Class)}
* is provided that allows mapping to a statically defined type, if no message property is
* found in the message properties.
* @author Mark Pollack
*
* @author Gary Russell
*
*/
public class DefaultClassMapper implements ClassMapper, InitializingBean {
public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
private Map<String, Class<?>> idClassMapping = new HashMap<String, Class<?>>();
private volatile Map<String, Class<?>> idClassMapping = new HashMap<String, Class<?>>();
private Map<Class<?>, String> classIdMapping = new HashMap<Class<?>, String>();
private volatile Map<Class<?>, String> classIdMapping = new HashMap<Class<?>, String>();
private String defaultHashtableTypeId = "Hashtable";
private final String defaultHashtableTypeId = "Hashtable";
private Class<?> defaultHashtableClass = Hashtable.class;
private volatile Class<?> defaultHashtableClass = Hashtable.class;
private volatile Class<?> defaultType;
/**
* The type returned by {@link #toClass(MessageProperties)} if no type information
* is found in the message properties.
* @param defaultType the defaultType to set
*/
public void setDefaultType(Class<?> defaultType) {
this.defaultType = defaultType;
}
public void setDefaultHashtableClass(Class<?> defaultHashtableClass) {
this.defaultHashtableClass = defaultHashtableClass;
@@ -105,9 +121,15 @@ public class DefaultClassMapper implements ClassMapper, InitializingBean {
classId = classIdFieldNameValue.toString();
}
if (classId == null) {
throw new MessageConversionException(
"failed to convert Message content. Could not resolve "
+ getClassIdFieldName() + " in header");
if (this.defaultType != null) {
return this.defaultType;
}
else {
throw new MessageConversionException(
"failed to convert Message content. Could not resolve "
+ getClassIdFieldName() + " in header " +
"and no defaultType provided");
}
}
return toClass(classId);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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
@@ -14,6 +14,7 @@ package org.springframework.amqp.support.converter;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.matchers.JUnitMatchers.containsString;
import static org.mockito.BDDMockito.given;
@@ -28,15 +29,18 @@ import org.junit.runner.RunWith;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.JsonMessageConverterTests.Foo;
/**
* @author James Carr
*
* @author Gary Russell
*
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultClassMapperTest {
@Spy
DefaultClassMapper classMapper = new DefaultClassMapper();
private final MessageProperties props = new MessageProperties();
@Test
@@ -115,6 +119,17 @@ public class DefaultClassMapperTest {
assertThat(className, equalTo("Hashtable"));
}
@SuppressWarnings("unchecked")
@Test
public void shouldUseDefaultType() {
props.getHeaders().clear();
classMapper.setDefaultType(Foo.class);
Class<Foo> clazz = (Class<Foo>) classMapper.toClass(props);
assertSame(Foo.class, clazz);
classMapper.setDefaultType(null);
}
private Map<String, Class<?>> map(String string, Class<?> class1) {
Map<String, Class<?>> map = new HashMap<String, Class<?>>();
map.put(string, class1);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
* Copyright 2002-2012 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,
@@ -10,6 +10,7 @@
package org.springframework.amqp.support.converter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.math.BigDecimal;
import java.util.Hashtable;
@@ -18,19 +19,29 @@ import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.BeanSerializerFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Mark Pollack
* @author Dave Syer
* @author Sam Nelson
* @author Gary Russell
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class JsonMessageConverterTests {
private JsonMessageConverter converter;
private SimpleTrade trade;
@Autowired
private JsonMessageConverter jsonConverterWithDefaultType;
@Before
public void before(){
converter = new JsonMessageConverter();
@@ -43,7 +54,7 @@ public class JsonMessageConverterTests {
trade.setRequestId("R123");
trade.setTicker("VMW");
trade.setUserName("Joe Trader");
}
@Test
public void simpleTrade() {
@@ -58,7 +69,7 @@ public class JsonMessageConverterTests {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializerFactory(BeanSerializerFactory.instance);
converter.setJsonObjectMapper(mapper);
Message message = converter.toMessage(trade, new MessageProperties());
SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message);
@@ -82,25 +93,49 @@ public class JsonMessageConverterTests {
Hashtable<String, String> hashtable = new Hashtable<String, String>();
hashtable.put("TICKER", "VMW");
hashtable.put("PRICE", "103.2");
Message message = converter.toMessage(hashtable, new MessageProperties());
Hashtable<String, String> marhsalledHashtable = (Hashtable<String, String>) converter.fromMessage(message);
assertEquals("VMW", marhsalledHashtable.get("TICKER"));
assertEquals("103.2", marhsalledHashtable.get("PRICE"));
}
@Test
public void shouldUseClassMapperWhenProvided() {
Message message = converter.toMessage(trade, new MessageProperties());
converter.setClassMapper(new DefaultClassMapper());
converter.setJavaTypeMapper(null);
SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message);
assertEquals(trade, marshalledTrade);
}
@Test
public void testDefaultType() {
byte[] bytes = "{\"name\" : \"foo\" }".getBytes();
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("application/json");
Message message = new Message(bytes, messageProperties);
JsonMessageConverter converter = new JsonMessageConverter();
DefaultClassMapper classMapper = new DefaultClassMapper();
classMapper.setDefaultType(Foo.class);
converter.setClassMapper(classMapper);
Object foo = converter.fromMessage(message);
assertTrue(foo instanceof Foo);
}
@Test
public void testDefaultTypeConfig() {
byte[] bytes = "{\"name\" : \"foo\" }".getBytes();
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("application/json");
Message message = new Message(bytes, messageProperties);
Object foo = jsonConverterWithDefaultType.fromMessage(message);
assertTrue(foo instanceof Foo);
}
public static class Foo {
private String name = "foo";

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="jsonConverterWithDefaultType" class="org.springframework.amqp.support.converter.JsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType"
value="org.springframework.amqp.support.converter.JsonMessageConverterTests$Foo" />
</bean>
</property>
</bean>
</beans>

View File

@@ -742,6 +742,22 @@ Object receiveAndConvert(String queueName) throws AmqpException;]]></programlist
</bean>
</property>
</bean>]]></programlisting>
<para>As shown above, the <classname>JsonMessageConverter</classname> uses a
<classname>DefaultClassMapper</classname> by default. Type information is
added to (and retrieved from) the <classname>MessageProperties</classname>.
If an inbound message does not contain type information in the
<classname>MessageProperties</classname>, but you know the expected type,
you can configure a static type using the <code>defaultType</code> property</para>
<programlisting language="xml"><![CDATA[<bean id="jsonConverterWithDefaultType" class="org.springframework.amqp.support.converter.JsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType"
value="foo.PurchaseOrder" />
</bean>
</property>
</bean>]]></programlisting>
</sect2>
<sect2>