Merge pull request #88 from aspan/AMQP-300

This commit is contained in:
Gary Russell
2013-03-22 16:40:21 -04:00
13 changed files with 1064 additions and 153 deletions

View File

@@ -42,6 +42,7 @@ subprojects { subproject ->
commonsIoVersion = '1.4'
erlangOtpVersion = '1.5.3'
jacksonVersion = '1.4.3'
jackson2Version = '2.0.1'
junitVersion = '4.8.2'
log4jVersion = '1.2.15'
mockitoVersion = '1.8.4'
@@ -122,7 +123,8 @@ project('spring-amqp') {
compile ("org.springframework:spring-context:$springVersion", optional)
compile ("org.codehaus.jackson:jackson-core-asl:$jacksonVersion", optional)
compile ("org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion", optional)
compile ("com.fasterxml.jackson.core:jackson-core:$jackson2Version", optional)
compile ("com.fasterxml.jackson.core:jackson-databind:$jackson2Version", optional)
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2002-2013 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.amqp.support.converter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.InitializingBean;
/**
* @author Mark Pollack
* @author Sam Nelson
* @author Andreas Asplund
*/
public abstract class AbstractJavaTypeMapper implements InitializingBean {
public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
public static final String DEFAULT_CONTENT_CLASSID_FIELD_NAME = "__ContentTypeId__";
public static final String DEFAULT_KEY_CLASSID_FIELD_NAME = "__KeyTypeId__";
private Map<String, Class<?>> idClassMapping = new HashMap<String, Class<?>>();
private final Map<Class<?>, String> classIdMapping = new HashMap<Class<?>, String>();
public String getClassIdFieldName() {
return DEFAULT_CLASSID_FIELD_NAME;
}
public String getContentClassIdFieldName() {
return DEFAULT_CONTENT_CLASSID_FIELD_NAME;
}
public String getKeyClassIdFieldName() {
return DEFAULT_KEY_CLASSID_FIELD_NAME;
}
public void setIdClassMapping(Map<String, Class<?>> idClassMapping) {
this.idClassMapping = idClassMapping;
}
protected void addHeader(MessageProperties properties, String headerName,
Class<?> clazz) {
if (classIdMapping.containsKey(clazz)) {
properties.getHeaders().put(headerName, classIdMapping.get(clazz));
}
else {
properties.getHeaders().put(headerName, clazz.getName());
}
}
protected String retrieveHeader(MessageProperties properties,
String headerName) {
Map<String, Object> headers = properties.getHeaders();
Object classIdFieldNameValue = headers.get(headerName);
String classId = null;
if (classIdFieldNameValue != null) {
classId = classIdFieldNameValue.toString();
}
if (classId == null) {
throw new MessageConversionException(
"failed to convert Message content. Could not resolve "
+ headerName + " in header");
}
return classId;
}
private void validateIdTypeMapping() {
Map<String, Class<?>> finalIdClassMapping = new HashMap<String, Class<?>>();
for (Map.Entry<String, Class<?>> entry : idClassMapping.entrySet()) {
String id = entry.getKey();
Class<?> clazz = entry.getValue();
finalIdClassMapping.put(id, clazz);
classIdMapping.put(clazz, id);
}
this.idClassMapping = finalIdClassMapping;
}
public Map<String, Class<?>> getIdClassMapping() {
return Collections.unmodifiableMap(idClassMapping);
}
public void afterPropertiesSet() throws Exception {
validateIdTypeMapping();
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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,
* either express or implied. See the License for the specific language governing permissions and limitations under the
* License.
*/
package org.springframework.amqp.support.converter;
/**
*
* @author Mark Pollack
* @author James Carr
* @author Dave Syer
* @author Sam Nelson
* @author Andreas Asplund
*/
public abstract class AbstractJsonMessageConverter extends AbstractMessageConverter {
public static final String DEFAULT_CHARSET = "UTF-8";
private volatile String defaultCharset = DEFAULT_CHARSET;
private ClassMapper classMapper = null;
public ClassMapper getClassMapper() {
return classMapper;
}
public void setClassMapper(ClassMapper classMapper) {
this.classMapper = classMapper;
}
/**
* Specify the default charset to use when converting to or from text-based
* Message body content. If not specified, the charset will be "UTF-8".
*/
public void setDefaultCharset(String defaultCharset) {
this.defaultCharset = (defaultCharset != null) ? defaultCharset
: DEFAULT_CHARSET;
}
public String getDefaultCharset() {
return defaultCharset;
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2002-2013 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.amqp.support.converter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.util.ClassUtils;
/**
* @author Mark Pollack
* @author Sam Nelson
* @author Andreas Asplund
*/
public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implements Jackson2JavaTypeMapper, ClassMapper {
public JavaType toJavaType(MessageProperties properties) {
JavaType classType = getClassIdType(retrieveHeader(properties,
getClassIdFieldName()));
if (!classType.isContainerType()) {
return classType;
}
JavaType contentClassType = getClassIdType(retrieveHeader(properties,
getContentClassIdFieldName()));
if (classType.getKeyType() == null) {
return CollectionType.construct(
classType.getRawClass(),
contentClassType);
}
JavaType keyClassType = getClassIdType(retrieveHeader(properties,
getKeyClassIdFieldName()));
JavaType mapType = MapType.construct(
classType.getRawClass(), keyClassType,
contentClassType);
return mapType;
}
private JavaType getClassIdType(String classId) {
if (getIdClassMapping().containsKey(classId)) {
return TypeFactory.defaultInstance().constructType(getIdClassMapping().get(classId));
}
try {
return TypeFactory.defaultInstance().constructType(ClassUtils.forName(classId, getClass()
.getClassLoader()));
} catch (ClassNotFoundException e) {
throw new MessageConversionException(
"failed to resolve class name. Class not found [" + classId
+ "]", e);
} catch (LinkageError e) {
throw new MessageConversionException(
"failed to resolve class name. Linkage error [" + classId
+ "]", e);
}
}
public void fromJavaType(JavaType javaType, MessageProperties properties) {
addHeader(properties, getClassIdFieldName(),
(Class<?>) javaType.getRawClass());
if (javaType.isContainerType()) {
addHeader(properties, getContentClassIdFieldName(), javaType
.getContentType().getRawClass());
}
if (javaType.getKeyType() != null) {
addHeader(properties, getKeyClassIdFieldName(), javaType
.getKeyType().getRawClass());
}
}
public void fromClass(Class<?> clazz, MessageProperties properties) {
fromJavaType(TypeFactory.defaultInstance().constructType(clazz), properties);
}
public Class<?> toClass(MessageProperties properties) {
return toJavaType(properties).getRawClass();
}
}

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-2013 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,
@@ -8,45 +8,23 @@
*/
package org.springframework.amqp.support.converter;
/**
* @author Mark Pollack
* @author Sam Nelson
*/
import static org.codehaus.jackson.map.type.TypeFactory.collectionType;
import static org.codehaus.jackson.map.type.TypeFactory.mapType;
import static org.codehaus.jackson.map.type.TypeFactory.type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.codehaus.jackson.type.JavaType;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ClassUtils;
public class DefaultJavaTypeMapper implements JavaTypeMapper, ClassMapper,
InitializingBean {
public static final String DEFAULT_CLASSID_FIELD_NAME = "__TypeId__";
public static final String DEFAULT_CONTENT_CLASSID_FIELD_NAME = "__ContentTypeId__";
public static final String DEFAULT_KEY_CLASSID_FIELD_NAME = "__KeyTypeId__";
private Map<String, Class<?>> idClassMapping = new HashMap<String, Class<?>>();
private Map<Class<?>, String> classIdMapping = new HashMap<Class<?>, String>();
public String getClassIdFieldName() {
return DEFAULT_CLASSID_FIELD_NAME;
}
public String getContentClassIdFieldName() {
return DEFAULT_CONTENT_CLASSID_FIELD_NAME;
}
public String getKeyClassIdFieldName() {
return DEFAULT_KEY_CLASSID_FIELD_NAME;
}
/**
* @author Mark Pollack
* @author Sam Nelson
* @author Andreas Asplund
*/
public class DefaultJavaTypeMapper extends AbstractJavaTypeMapper implements JavaTypeMapper, ClassMapper {
@SuppressWarnings({ "unchecked", "rawtypes" })
public JavaType toJavaType(MessageProperties properties) {
@@ -74,47 +52,29 @@ public class DefaultJavaTypeMapper implements JavaTypeMapper, ClassMapper,
}
private JavaType getClassIdType(String classId) {
if (this.idClassMapping.containsKey(classId)) {
return type(idClassMapping.get(classId));
if (getIdClassMapping().containsKey(classId)) {
return type(getIdClassMapping().get(classId));
}
try {
return type(ClassUtils.forName(classId, getClass()
.getClassLoader()));
} catch (ClassNotFoundException e) {
}
catch (ClassNotFoundException e) {
throw new MessageConversionException(
"failed to resolve class name. Class not found [" + classId
+ "]", e);
} catch (LinkageError e) {
}
catch (LinkageError e) {
throw new MessageConversionException(
"failed to resolve class name. Linkage error [" + classId
+ "]", e);
}
}
private String retrieveHeader(MessageProperties properties,
String headerName) {
Map<String, Object> headers = properties.getHeaders();
Object classIdFieldNameValue = headers.get(headerName);
String classId = null;
if (classIdFieldNameValue != null) {
classId = classIdFieldNameValue.toString();
}
if (classId == null) {
throw new MessageConversionException(
"failed to convert Message content. Could not resolve "
+ headerName + " in header");
}
return classId;
}
public void setIdClassMapping(Map<String, Class<?>> idClassMapping) {
this.idClassMapping = idClassMapping;
}
public void fromJavaType(JavaType javaType, MessageProperties properties) {
addHeader(properties, getClassIdFieldName(),
(Class<?>) javaType.getRawClass());
javaType.getRawClass());
if (javaType.isContainerType()) {
addHeader(properties, getContentClassIdFieldName(), javaType
@@ -127,30 +87,6 @@ public class DefaultJavaTypeMapper implements JavaTypeMapper, ClassMapper,
}
}
public void afterPropertiesSet() throws Exception {
validateIdTypeMapping();
}
private void addHeader(MessageProperties properties, String headerName,
Class<?> clazz) {
if (classIdMapping.containsKey(clazz)) {
properties.getHeaders().put(headerName, classIdMapping.get(clazz));
} else {
properties.getHeaders().put(headerName, clazz.getName());
}
}
private void validateIdTypeMapping() {
Map<String, Class<?>> finalIdClassMapping = new HashMap<String, Class<?>>();
for (Entry<String, Class<?>> entry : idClassMapping.entrySet()) {
String id = entry.getKey();
Class<?> clazz = entry.getValue();
finalIdClassMapping.put(id, clazz);
classIdMapping.put(clazz, id);
}
this.idClassMapping = finalIdClassMapping;
}
public void fromClass(Class<?> clazz, MessageProperties properties) {
fromJavaType(type(clazz), properties);

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2002-2013 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.amqp.support.converter;
import com.fasterxml.jackson.databind.JavaType;
import org.springframework.amqp.core.MessageProperties;
/**
* Strategy for setting metadata on messages such that one can create the class that needs to be instantiated when
* receiving a message.
*
* @author Mark Pollack
* @author James Carr
* @author Sam Nelson
* @author Andreas Asplund
*/
public interface Jackson2JavaTypeMapper extends ClassMapper {
void fromJavaType(JavaType javaType, MessageProperties properties);
JavaType toJavaType(MessageProperties properties);
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2002-2013 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.amqp.support.converter;
import java.io.IOException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* JSON converter that uses the Jackson 2 Json library.
*
* @author Mark Pollack
* @author James Carr
* @author Dave Syer
* @author Sam Nelson
* @author Andreas Asplund
*/
public class Jackson2JsonMessageConverter extends AbstractJsonMessageConverter {
private static Log log = LogFactory.getLog(Jackson2JsonMessageConverter.class);
private ObjectMapper jsonObjectMapper = new ObjectMapper();
private Jackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper();
public Jackson2JsonMessageConverter() {
super();
initializeJsonObjectMapper();
}
public Jackson2JavaTypeMapper getJavaTypeMapper() {
return javaTypeMapper;
}
public void setJavaTypeMapper(Jackson2JavaTypeMapper javaTypeMapper) {
this.javaTypeMapper = javaTypeMapper;
}
/**
* The {@link com.fasterxml.jackson.databind.ObjectMapper} to use instead of using the default. An
* alternative to injecting a mapper is to extend this class and override
* {@link #initializeJsonObjectMapper()}.
*
* @param jsonObjectMapper
* the object mapper to set
*/
public void setJsonObjectMapper(ObjectMapper jsonObjectMapper) {
this.jsonObjectMapper = jsonObjectMapper;
}
/**
* Subclass and override to customize.
*/
protected void initializeJsonObjectMapper() {
jsonObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public Object fromMessage(Message message)
throws MessageConversionException {
Object content = null;
MessageProperties properties = message.getMessageProperties();
if (properties != null) {
String contentType = properties.getContentType();
if (contentType != null && contentType.contains("json")) {
String encoding = properties.getContentEncoding();
if (encoding == null) {
encoding = getDefaultCharset();
}
try {
if (getClassMapper() == null) {
JavaType targetJavaType = getJavaTypeMapper()
.toJavaType(message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
encoding, targetJavaType);
}
else {
Class<?> targetClass = getClassMapper().toClass(
message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
encoding, targetClass);
}
}
catch (IOException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
}
}
else {
log.warn("Could not convert incoming message with content-type ["
+ contentType + "]");
}
}
if (content == null) {
content = message.getBody();
}
return content;
}
private Object convertBytesToObject(byte[] body, String encoding,
JavaType targetJavaType) throws JsonParseException,
JsonMappingException, IOException {
String contentAsString = new String(body, encoding);
return jsonObjectMapper.readValue(contentAsString, targetJavaType);
}
private Object convertBytesToObject(byte[] body, String encoding,
Class<?> targetClass) throws JsonParseException,
JsonMappingException, IOException {
String contentAsString = new String(body, encoding);
return jsonObjectMapper.readValue(contentAsString, jsonObjectMapper.constructType(targetClass));
}
@Override
protected Message createMessage(Object objectToConvert,
MessageProperties messageProperties)
throws MessageConversionException {
byte[] bytes = null;
try {
String jsonString = jsonObjectMapper
.writeValueAsString(objectToConvert);
bytes = jsonString.getBytes(getDefaultCharset());
}
catch (IOException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
messageProperties.setContentEncoding(getDefaultCharset());
if (bytes != null) {
messageProperties.setContentLength(bytes.length);
}
if (getClassMapper() == null) {
getJavaTypeMapper().fromJavaType(jsonObjectMapper.constructType(objectToConvert.getClass()),
messageProperties);
}
else {
getClassMapper().fromClass(objectToConvert.getClass(),
messageProperties);
}
return new Message(bytes, messageProperties);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
* Copyright 2002-2013 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,
@@ -12,11 +12,9 @@ package org.springframework.amqp.support.converter;
import static org.codehaus.jackson.map.type.TypeFactory.type;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.JsonMappingException;
@@ -32,44 +30,21 @@ import org.springframework.amqp.core.MessageProperties;
* @author James Carr
* @author Dave Syer
* @author Sam Nelson
* @author Andreas Asplund
*/
public class JsonMessageConverter extends AbstractMessageConverter {
public class JsonMessageConverter extends AbstractJsonMessageConverter {
private static Log log = LogFactory.getLog(JsonMessageConverter.class);
public static final String DEFAULT_CHARSET = "UTF-8";
private volatile String defaultCharset = DEFAULT_CHARSET;
private ObjectMapper jsonObjectMapper = new ObjectMapper();
private JavaTypeMapper javaTypeMapper = new DefaultJavaTypeMapper();
private ClassMapper classMapper = null;
public ClassMapper getClassMapper() {
return classMapper;
}
public void setClassMapper(ClassMapper classMapper) {
this.classMapper = classMapper;
}
public JsonMessageConverter() {
super();
initializeJsonObjectMapper();
}
/**
* Specify the default charset to use when converting to or from text-based
* Message body content. If not specified, the charset will be "UTF-8".
*/
public void setDefaultCharset(String defaultCharset) {
this.defaultCharset = (defaultCharset != null) ? defaultCharset
: DEFAULT_CHARSET;
}
public JavaTypeMapper getJavaTypeMapper() {
return javaTypeMapper;
}
@@ -110,7 +85,7 @@ public class JsonMessageConverter extends AbstractMessageConverter {
if (contentType != null && contentType.contains("json")) {
String encoding = properties.getContentEncoding();
if (encoding == null) {
encoding = this.defaultCharset;
encoding = getDefaultCharset();
}
try {
@@ -119,26 +94,20 @@ public class JsonMessageConverter extends AbstractMessageConverter {
.toJavaType(message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
encoding, targetJavaType);
} else {
}
else {
Class<?> targetClass = getClassMapper().toClass(
message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
encoding, targetClass);
}
} catch (UnsupportedEncodingException e) {
throw new MessageConversionException(
"Failed to convert json-based Message content", e);
} catch (JsonParseException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
} catch (JsonMappingException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
} catch (IOException e) {
}
catch (IOException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
}
} else {
}
else {
log.warn("Could not convert incoming message with content-type ["
+ contentType + "]");
}
@@ -171,22 +140,14 @@ public class JsonMessageConverter extends AbstractMessageConverter {
try {
String jsonString = jsonObjectMapper
.writeValueAsString(objectToConvert);
bytes = jsonString.getBytes(this.defaultCharset);
} catch (UnsupportedEncodingException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
} catch (JsonGenerationException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
} catch (JsonMappingException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
} catch (IOException e) {
bytes = jsonString.getBytes(getDefaultCharset());
}
catch (IOException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
messageProperties.setContentEncoding(this.defaultCharset);
messageProperties.setContentEncoding(getDefaultCharset());
if (bytes != null) {
messageProperties.setContentLength(bytes.length);
}
@@ -195,7 +156,8 @@ public class JsonMessageConverter extends AbstractMessageConverter {
getJavaTypeMapper().fromJavaType(type(objectToConvert.getClass()),
messageProperties);
} else {
}
else {
getClassMapper().fromClass(objectToConvert.getClass(),
messageProperties);

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2002-2013 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.amqp.support.converter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.MapType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import org.junit.Test;
import static org.junit.matchers.JUnitMatchers.containsString;
import org.junit.runner.RunWith;
import static org.mockito.BDDMockito.given;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.amqp.core.MessageProperties;
/**
* @author James Carr
* @author Sam Nelson
* @author Andreas Asplund
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultJackson2JavaTypeMapperTest {
@Spy
DefaultJackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper();
private final MessageProperties properties = new MessageProperties();
@SuppressWarnings("rawtypes")
private Class<ArrayList> containerClass = ArrayList.class;
@SuppressWarnings("rawtypes")
private Class<HashMap> mapClass = HashMap.class;
@Test
public void shouldThrowAnExceptionWhenClassIdNotPresent() {
try {
javaTypeMapper.toJavaType(properties);
}
catch (MessageConversionException e) {
String classIdFieldName = javaTypeMapper.getClassIdFieldName();
assertThat(e.getMessage(), containsString("Could not resolve " + classIdFieldName + " in header"));
return;
}
fail();
}
@Test
public void shouldLookInTheClassIdFieldNameToFindTheClassName() {
properties.getHeaders().put("type", "java.lang.String");
given(javaTypeMapper.getClassIdFieldName()).willReturn("type");
JavaType javaType = javaTypeMapper.toJavaType(properties);
assertThat(javaType, equalTo(TypeFactory.defaultInstance().constructType(String.class)));
}
@Test
public void shouldUseTheClassProvidedByTheLookupMapIfPresent() {
properties.getHeaders().put("__TypeId__", "trade");
javaTypeMapper.setIdClassMapping(map("trade", SimpleTrade.class));
JavaType javaType = javaTypeMapper.toJavaType(properties);
assertEquals(javaType, TypeFactory.defaultInstance().constructType(SimpleTrade.class));
}
@Test
public void fromJavaTypeShouldPopulateWithJavaTypeNameByDefault() {
javaTypeMapper.fromJavaType(TypeFactory.defaultInstance().constructType(SimpleTrade.class), properties);
String className = (String) properties.getHeaders().get(javaTypeMapper.getClassIdFieldName());
assertThat(className, equalTo(SimpleTrade.class.getName()));
}
@Test
public void shouldUseSpecialNameForClassIfPresent() throws Exception {
javaTypeMapper.setIdClassMapping(map("daytrade", SimpleTrade.class));
javaTypeMapper.afterPropertiesSet();
javaTypeMapper.fromJavaType(TypeFactory.defaultInstance().constructType(SimpleTrade.class), properties);
String className = (String) properties.getHeaders().get(javaTypeMapper.getClassIdFieldName());
assertThat(className, equalTo("daytrade"));
}
@Test
public void shouldThrowAnExceptionWhenContentClassIdIsNotPresentWhenClassIdIsContainerType() {
properties.getHeaders().put(javaTypeMapper.getClassIdFieldName(), ArrayList.class.getName());
try {
javaTypeMapper.toJavaType(properties);
}
catch (MessageConversionException e) {
String contentClassIdFieldName = javaTypeMapper.getContentClassIdFieldName();
assertThat(e.getMessage(), containsString("Could not resolve " + contentClassIdFieldName + " in header"));
return;
}
fail();
}
@Test
public void shouldLookInTheContentClassIdFieldNameToFindTheContainerClassIDWhenClassIdIsContainerType() {
properties.getHeaders().put("contentType", "java.lang.String");
properties.getHeaders().put(javaTypeMapper.getClassIdFieldName(), ArrayList.class.getName());
given(javaTypeMapper.getContentClassIdFieldName()).willReturn("contentType");
JavaType javaType = javaTypeMapper.toJavaType(properties);
assertThat((CollectionType)javaType, equalTo(TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, String.class)));
}
@Test
public void shouldUseTheContentClassProvidedByTheLookupMapIfPresent() {
properties.getHeaders().put(javaTypeMapper.getClassIdFieldName(), containerClass.getName());
properties.getHeaders().put("__ContentTypeId__", "trade");
Map<String, Class<?>> map = map("trade", SimpleTrade.class);
map.put(javaTypeMapper.getClassIdFieldName(), containerClass);
javaTypeMapper.setIdClassMapping(map);
JavaType javaType = javaTypeMapper.toJavaType(properties);
assertThat((CollectionType)javaType, equalTo(TypeFactory.defaultInstance().constructCollectionType(containerClass, TypeFactory.defaultInstance().constructType(SimpleTrade.class))));
}
@Test
public void fromJavaTypeShouldPopulateWithContentTypeJavaTypeNameByDefault() {
javaTypeMapper.fromJavaType(TypeFactory.defaultInstance().constructCollectionType(containerClass, TypeFactory.defaultInstance().constructType(SimpleTrade.class)),
properties);
String className = (String) properties.getHeaders().get(javaTypeMapper.getClassIdFieldName());
String contentClassName = (String) properties.getHeaders().get(javaTypeMapper.getContentClassIdFieldName());
assertThat(className, equalTo(ArrayList.class.getName()));
assertThat(contentClassName, equalTo(SimpleTrade.class.getName()));
}
@Test
public void shouldThrowAnExceptionWhenKeyClassIdIsNotPresentWhenClassIdIsAMap() {
properties.getHeaders().put(javaTypeMapper.getClassIdFieldName(), HashMap.class.getName());
properties.getHeaders().put(javaTypeMapper.getKeyClassIdFieldName(), String.class.getName());
try {
javaTypeMapper.toJavaType(properties);
}
catch (MessageConversionException e) {
String contentClassIdFieldName = javaTypeMapper.getContentClassIdFieldName();
assertThat(e.getMessage(), containsString("Could not resolve " + contentClassIdFieldName + " in header"));
return;
}
fail();
}
@Test
public void shouldLookInTheValueClassIdFieldNameToFindTheValueClassIDWhenClassIdIsAMap() {
properties.getHeaders().put("keyType", "java.lang.Integer");
properties.getHeaders().put(javaTypeMapper.getContentClassIdFieldName(), "java.lang.String");
properties.getHeaders().put(javaTypeMapper.getClassIdFieldName(), HashMap.class.getName());
given(javaTypeMapper.getKeyClassIdFieldName()).willReturn("keyType");
JavaType javaType = javaTypeMapper.toJavaType(properties);
assertThat((MapType)javaType, equalTo(TypeFactory.defaultInstance().constructMapType(HashMap.class, Integer.class, String.class)));
}
@Test
public void shouldUseTheKeyClassProvidedByTheLookupMapIfPresent() {
properties.getHeaders().put(javaTypeMapper.getClassIdFieldName(), mapClass.getName());
properties.getHeaders().put(javaTypeMapper.getContentClassIdFieldName(), "java.lang.String");
properties.getHeaders().put("__KeyTypeId__", "trade");
Map<String, Class<?>> map = map("trade", SimpleTrade.class);
map.put(javaTypeMapper.getClassIdFieldName(), mapClass);
map.put(javaTypeMapper.getContentClassIdFieldName(), String.class);
javaTypeMapper.setIdClassMapping(map);
JavaType javaType = javaTypeMapper.toJavaType(properties);
assertThat((MapType)javaType,
equalTo(TypeFactory.defaultInstance().constructMapType(mapClass, TypeFactory.defaultInstance().constructType(SimpleTrade.class),
TypeFactory.defaultInstance().constructType(String.class))));
}
@Test
public void fromJavaTypeShouldPopulateWithKeyTypeAndContentJavaTypeNameByDefault() {
javaTypeMapper.fromJavaType(TypeFactory.defaultInstance().constructMapType(mapClass, TypeFactory.defaultInstance().constructType(SimpleTrade.class),
TypeFactory.defaultInstance().constructType(String.class)), properties);
String className = (String) properties.getHeaders().get(javaTypeMapper.getClassIdFieldName());
String contentClassName = (String) properties.getHeaders().get(javaTypeMapper.getContentClassIdFieldName());
String keyClassName = (String) properties.getHeaders().get(javaTypeMapper.getKeyClassIdFieldName());
assertThat(className, equalTo(HashMap.class.getName()));
assertThat(contentClassName, equalTo(String.class.getName()));
assertThat(keyClassName, equalTo(SimpleTrade.class.getName()));
}
@Test
public void fromClassShouldPopulateWithJavaTypeNameByDefault() {
javaTypeMapper.fromClass(SimpleTrade.class, properties);
String className = (String) properties.getHeaders().get(javaTypeMapper.getClassIdFieldName());
assertThat(className, equalTo(SimpleTrade.class.getName()));
}
@Test
public void toClassShouldUseTheClassProvidedByTheLookupMapIfPresent() {
properties.getHeaders().put("__TypeId__", "trade");
javaTypeMapper.setIdClassMapping(map("trade", SimpleTrade.class));
Class<?> clazz = javaTypeMapper.toClass(properties);
assertEquals(SimpleTrade.class, clazz);
}
private Map<String, Class<?>> map(String string, Class<?> clazz) {
Map<String, Class<?>> map = new HashMap<String, Class<?>>();
map.put(string, clazz);
return map;
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright 2002-2013 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.amqp.support.converter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.math.BigDecimal;
import java.util.Hashtable;
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;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
/**
* @author Mark Pollack
* @author Dave Syer
* @author Sam Nelson
* @author Gary Russell
* @author Andreas Asplund
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class Jackson2JsonMessageConverterTests {
private Jackson2JsonMessageConverter converter;
private SimpleTrade trade;
@Autowired
private Jackson2JsonMessageConverter jsonConverterWithDefaultType;
@Before
public void before(){
converter = new Jackson2JsonMessageConverter();
trade = new SimpleTrade();
trade.setAccountName("Acct1");
trade.setBuyRequest(true);
trade.setOrderType("Market");
trade.setPrice(new BigDecimal(103.30));
trade.setQuantity(100);
trade.setRequestId("R123");
trade.setTicker("VMW");
trade.setUserName("Joe Trader");
}
@Test
public void simpleTrade() {
Message message = converter.toMessage(trade, new MessageProperties());
SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message);
assertEquals(trade, marshalledTrade);
}
@Test
public void simpleTradeOverrideMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializerFactory(BeanSerializerFactory.instance);
converter.setJsonObjectMapper(mapper);
Message message = converter.toMessage(trade, new MessageProperties());
SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message);
assertEquals(trade, marshalledTrade);
}
@Test
public void nestedBean() {
Bar bar = new Bar();
bar.getFoo().setName("spam");
Message message = converter.toMessage(bar, new MessageProperties());
Bar marshalled = (Bar) converter.fromMessage(message);
assertEquals(bar, marshalled);
}
@Test
@SuppressWarnings("unchecked")
public void hashtable() {
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 shouldUseClassMapperWhenProvidedOutbound() {
converter.setClassMapper(new DefaultClassMapper());
converter.setJavaTypeMapper(null);
Message message = converter.toMessage(trade, new MessageProperties());
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);
}
@Test
public void testNoJsonContentType() {
byte[] bytes = "{\"name\" : \"foo\" }".getBytes();
MessageProperties messageProperties = new MessageProperties();
Message message = new Message(bytes, messageProperties);
Object foo = jsonConverterWithDefaultType.fromMessage(message);
assertEquals(new String(bytes), new String((byte[]) foo));
}
public static class Foo {
private String name = "foo";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Foo other = (Foo) obj;
if (name == null) {
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
public static class Bar {
private String name = "bar";
private Foo foo = new Foo();
public Foo getFoo() {
return foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((foo == null) ? 0 : foo.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Bar other = (Bar) obj;
if (foo == null) {
if (other.foo != null) {
return false;
}
}
else if (!foo.equals(other.foo)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
* Copyright 2002-2013 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,
@@ -112,6 +112,16 @@ public class JsonMessageConverterTests {
assertEquals(trade, marshalledTrade);
}
@Test
public void shouldUseClassMapperWhenProvidedOutbound() {
converter.setClassMapper(new DefaultClassMapper());
converter.setJavaTypeMapper(null);
Message message = converter.toMessage(trade, new MessageProperties());
SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message);
assertEquals(trade, marshalledTrade);
}
@Test
public void testDefaultType() {
byte[] bytes = "{\"name\" : \"foo\" }".getBytes();
@@ -136,6 +146,15 @@ public class JsonMessageConverterTests {
assertTrue(foo instanceof Foo);
}
@Test
public void testNoJsonContentType() {
byte[] bytes = "{\"name\" : \"foo\" }".getBytes();
MessageProperties messageProperties = new MessageProperties();
Message message = new Message(bytes, messageProperties);
Object foo = jsonConverterWithDefaultType.fromMessage(message);
assertEquals(new String(bytes), new String((byte[]) foo));
}
public static class Foo {
private String name = "foo";
@@ -157,14 +176,24 @@ public class JsonMessageConverterTests {
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Foo other = (Foo) obj;
if (name == null) {
if (other.name != null) return false;
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) return false;
else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
@@ -200,18 +229,32 @@ public class JsonMessageConverterTests {
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Bar other = (Bar) obj;
if (foo == null) {
if (other.foo != null) return false;
if (other.foo != null) {
return false;
}
}
else if (!foo.equals(other.foo)) return false;
else if (!foo.equals(other.foo)) {
return false;
}
if (name == null) {
if (other.name != null) return false;
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) return false;
else if (!name.equals(other.name)) {
return false;
}
return true;
}
}

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.Jackson2JsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType"
value="org.springframework.amqp.support.converter.Jackson2JsonMessageConverterTests$Foo" />
</bean>
</property>
</bean>
</beans>

View File

@@ -533,7 +533,7 @@ Object receiveAndConvert(String queueName) throws AmqpException;]]></programlist
</para>
</sect2>
<sect2><title>Asynchronous Consumer</title><para>For asynchronous Message
<sect2 id="async-consumer"><title>Asynchronous Consumer</title><para>For asynchronous Message
reception, a dedicated component (not the
<interfacename>AmqpTemplate</interfacename>) is involved. That component
is a container for a Message consuming callback. We will look at the
@@ -666,7 +666,10 @@ void convertAndSend(String exchange, String routingKey, Object message,
<programlisting language="java"><![CDATA[Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;]]></programlisting>
<note>
The <classname>MessageListenerAdapter</classname> mentioned in <xref linkend="async-consumer"/>
also uses a <classname>MessageConverter</classname>.
</note>
<sect2>
<title>SimpleMessageConverter</title>
@@ -723,15 +726,20 @@ Object receiveAndConvert(String queueName) throws AmqpException;]]></programlist
</sect2>
<sect2>
<title>JsonMessageConverter</title>
<title>JsonMessageConverter and Jackson2JsonMessageConverter</title>
<para>As mentioned in the previous section, relying on Java
serialization is generally not recommended. One rather common
alternative that is more flexible and portable across different
languages and platforms is JSON (JavaScript Object Notation). An
implementation is available and can be configured on any
languages and platforms is JSON (JavaScript Object Notation). Two
implementations are available and can be configured on any
<classname>RabbitTemplate</classname> instance to override its usage of
the <classname>SimpleMessageConverter</classname> default.</para>
the <classname>SimpleMessageConverter</classname> default.
The <classname>JsonMessageConverter</classname> which uses the
<code>org.codehaus.jackson</code> 1.x library
and <classname>Jackson2JsonMessageConverter</classname> which uses the
<code>com.fasterxml.jackson</code> 2.x library.
</para>
<programlisting language="xml"><![CDATA[<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<property name="connectionFactory" ref="rabbitConnectionFactory"/>
@@ -743,7 +751,16 @@ Object receiveAndConvert(String queueName) throws AmqpException;]]></programlist
</property>
</bean>]]></programlisting>
<para>As shown above, the <classname>JsonMessageConverter</classname> uses a
<programlisting language="xml"><![CDATA[<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<property name="connectionFactory" ref="rabbitConnectionFactory"/>
<property name="messageConverter">
<bean class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter">
<!-- if necessary, override the DefaultClassMapper -->
<property name="classMapper" ref="customClassMapper"/>
</bean>
</property>
</bean>]]></programlisting>
<para>As shown above, the <classname>JsonMessageConverter</classname> and <classname>Jackson2JsonMessageConverter</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
@@ -758,6 +775,15 @@ Object receiveAndConvert(String queueName) throws AmqpException;]]></programlist
</bean>
</property>
</bean>]]></programlisting>
<programlisting language="xml"><![CDATA[<bean id="jsonConverterWithDefaultType" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType"
value="foo.PurchaseOrder" />
</bean>
</property>
</bean>]]></programlisting>
</sect2>
<sect2>