|
| 1 | +/* |
| 2 | + * Copyright 2018 the original author or authors. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package org.springframework.amqp.support.converter; |
| 18 | + |
| 19 | +import java.io.IOException; |
| 20 | +import java.lang.reflect.Type; |
| 21 | + |
| 22 | +import org.apache.commons.logging.Log; |
| 23 | +import org.apache.commons.logging.LogFactory; |
| 24 | + |
| 25 | +import org.springframework.amqp.core.Message; |
| 26 | +import org.springframework.amqp.core.MessageProperties; |
| 27 | +import org.springframework.beans.factory.BeanClassLoaderAware; |
| 28 | +import org.springframework.core.ParameterizedTypeReference; |
| 29 | +import org.springframework.util.Assert; |
| 30 | +import org.springframework.util.ClassUtils; |
| 31 | +import org.springframework.util.MimeType; |
| 32 | + |
| 33 | +import com.fasterxml.jackson.databind.JavaType; |
| 34 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 35 | + |
| 36 | +/** |
| 37 | + * Abstract Jackson2 message converter. |
| 38 | + * |
| 39 | + * @author Mark Pollack |
| 40 | + * @author James Carr |
| 41 | + * @author Dave Syer |
| 42 | + * @author Sam Nelson |
| 43 | + * @author Andreas Asplund |
| 44 | + * @author Artem Bilan |
| 45 | + * @author Mohammad Hewedy |
| 46 | + * |
| 47 | + * @since 2.1 |
| 48 | + */ |
| 49 | +public abstract class AbstractJackson2MessageConverter extends AbstractMessageConverter |
| 50 | + implements BeanClassLoaderAware, SmartMessageConverter { |
| 51 | + |
| 52 | + protected final Log log = LogFactory.getLog(getClass()); |
| 53 | + |
| 54 | + public static final String DEFAULT_CHARSET = "UTF-8"; |
| 55 | + |
| 56 | + private volatile String defaultCharset = DEFAULT_CHARSET; |
| 57 | + |
| 58 | + private ClassMapper classMapper = null; |
| 59 | + |
| 60 | + protected boolean typeMapperSet; |
| 61 | + |
| 62 | + private final MimeType contentType; |
| 63 | + |
| 64 | + protected final ObjectMapper objectMapper; |
| 65 | + |
| 66 | + private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); |
| 67 | + |
| 68 | + private Jackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper(); |
| 69 | + |
| 70 | + /** |
| 71 | + * Construct with the provided {@link ObjectMapper} instance. |
| 72 | + * @param objectMapper the {@link ObjectMapper} to use. |
| 73 | + * @param contentType content type of the message |
| 74 | + * @param trustedPackages the trusted Java packages for deserialization |
| 75 | + * @see DefaultJackson2JavaTypeMapper#setTrustedPackages(String...) |
| 76 | + */ |
| 77 | + protected AbstractJackson2MessageConverter(ObjectMapper objectMapper, MimeType contentType, |
| 78 | + String... trustedPackages) { |
| 79 | + |
| 80 | + Assert.notNull(objectMapper, "'objectMapper' must not be null"); |
| 81 | + Assert.notNull(contentType, "'contentType' must not be null"); |
| 82 | + this.objectMapper = objectMapper; |
| 83 | + this.contentType = contentType; |
| 84 | + ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setTrustedPackages(trustedPackages); |
| 85 | + } |
| 86 | + |
| 87 | + public ClassMapper getClassMapper() { |
| 88 | + return this.classMapper; |
| 89 | + |
| 90 | + } |
| 91 | + |
| 92 | + public void setClassMapper(ClassMapper classMapper) { |
| 93 | + this.classMapper = classMapper; |
| 94 | + } |
| 95 | + |
| 96 | + /** |
| 97 | + * Specify the default charset to use when converting to or from text-based |
| 98 | + * Message body content. If not specified, the charset will be "UTF-8". |
| 99 | + * @param defaultCharset The default charset. |
| 100 | + */ |
| 101 | + public void setDefaultCharset(String defaultCharset) { |
| 102 | + this.defaultCharset = (defaultCharset != null) ? defaultCharset |
| 103 | + : DEFAULT_CHARSET; |
| 104 | + } |
| 105 | + |
| 106 | + public String getDefaultCharset() { |
| 107 | + return this.defaultCharset; |
| 108 | + } |
| 109 | + |
| 110 | + @Override |
| 111 | + public void setBeanClassLoader(ClassLoader classLoader) { |
| 112 | + this.classLoader = classLoader; |
| 113 | + if (!this.typeMapperSet) { |
| 114 | + ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setBeanClassLoader(classLoader); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + protected ClassLoader getClassLoader() { |
| 119 | + return this.classLoader; |
| 120 | + } |
| 121 | + |
| 122 | + public Jackson2JavaTypeMapper getJavaTypeMapper() { |
| 123 | + return this.javaTypeMapper; |
| 124 | + } |
| 125 | + |
| 126 | + public void setJavaTypeMapper(Jackson2JavaTypeMapper javaTypeMapper) { |
| 127 | + this.javaTypeMapper = javaTypeMapper; |
| 128 | + this.typeMapperSet = true; |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Return the type precedence. |
| 133 | + * @return the precedence. |
| 134 | + * @see #setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence) |
| 135 | + */ |
| 136 | + public Jackson2JavaTypeMapper.TypePrecedence getTypePrecedence() { |
| 137 | + return this.javaTypeMapper.getTypePrecedence(); |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Set the precedence for evaluating type information in message properties. |
| 142 | + * When using {@code @RabbitListener} at the method level, the framework attempts |
| 143 | + * to determine the target type for payload conversion from the method signature. |
| 144 | + * If so, this type is provided in the |
| 145 | + * {@link MessageProperties#getInferredArgumentType() inferredArgumentType} |
| 146 | + * message property. |
| 147 | + * <p> By default, if the type is concrete (not abstract, not an interface), this will |
| 148 | + * be used ahead of type information provided in the {@code __TypeId__} and |
| 149 | + * associated headers provided by the sender. |
| 150 | + * <p> If you wish to force the use of the {@code __TypeId__} and associated headers |
| 151 | + * (such as when the actual type is a subclass of the method argument type), |
| 152 | + * set the precedence to {@link Jackson2JavaTypeMapper.TypePrecedence#TYPE_ID}. |
| 153 | + * @param typePrecedence the precedence. |
| 154 | + * @see DefaultJackson2JavaTypeMapper#setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence) |
| 155 | + */ |
| 156 | + public void setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence typePrecedence) { |
| 157 | + if (this.typeMapperSet) { |
| 158 | + throw new IllegalStateException("When providing your own type mapper, you should set the precedence on it"); |
| 159 | + } |
| 160 | + if (this.javaTypeMapper instanceof DefaultJackson2JavaTypeMapper) { |
| 161 | + ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setTypePrecedence(typePrecedence); |
| 162 | + } |
| 163 | + else { |
| 164 | + throw new IllegalStateException("Type precedence is available with the DefaultJackson2JavaTypeMapper"); |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + @Override |
| 169 | + public Object fromMessage(Message message) throws MessageConversionException { |
| 170 | + return fromMessage(message, null); |
| 171 | + } |
| 172 | + |
| 173 | + /** |
| 174 | + * {@inheritDoc} |
| 175 | + * @param conversionHint The conversionHint must be a {@link ParameterizedTypeReference}. |
| 176 | + */ |
| 177 | + @Override |
| 178 | + public Object fromMessage(Message message, Object conversionHint) throws MessageConversionException { |
| 179 | + Object content = null; |
| 180 | + MessageProperties properties = message.getMessageProperties(); |
| 181 | + if (properties != null) { |
| 182 | + String contentType = properties.getContentType(); |
| 183 | + if (contentType != null && contentType.contains(this.contentType.getSubtype())) { |
| 184 | + String encoding = properties.getContentEncoding(); |
| 185 | + if (encoding == null) { |
| 186 | + encoding = getDefaultCharset(); |
| 187 | + } |
| 188 | + try { |
| 189 | + if (conversionHint instanceof ParameterizedTypeReference) { |
| 190 | + content = convertBytesToObject(message.getBody(), encoding, |
| 191 | + this.objectMapper.getTypeFactory().constructType( |
| 192 | + ((ParameterizedTypeReference<?>) conversionHint).getType())); |
| 193 | + } |
| 194 | + else if (getClassMapper() == null) { |
| 195 | + JavaType targetJavaType = getJavaTypeMapper() |
| 196 | + .toJavaType(message.getMessageProperties()); |
| 197 | + content = convertBytesToObject(message.getBody(), |
| 198 | + encoding, targetJavaType); |
| 199 | + } |
| 200 | + else { |
| 201 | + Class<?> targetClass = getClassMapper().toClass( |
| 202 | + message.getMessageProperties()); |
| 203 | + content = convertBytesToObject(message.getBody(), |
| 204 | + encoding, targetClass); |
| 205 | + } |
| 206 | + } |
| 207 | + catch (IOException e) { |
| 208 | + throw new MessageConversionException( |
| 209 | + "Failed to convert Message content", e); |
| 210 | + } |
| 211 | + } |
| 212 | + else { |
| 213 | + if (this.log.isWarnEnabled()) { |
| 214 | + this.log.warn("Could not convert incoming message with content-type [" |
| 215 | + + contentType + "], '" + this.contentType.getSubtype() + "' keyword missing."); |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + if (content == null) { |
| 220 | + content = message.getBody(); |
| 221 | + } |
| 222 | + return content; |
| 223 | + } |
| 224 | + |
| 225 | + private Object convertBytesToObject(byte[] body, String encoding, JavaType targetJavaType) throws IOException { |
| 226 | + String contentAsString = new String(body, encoding); |
| 227 | + return this.objectMapper.readValue(contentAsString, targetJavaType); |
| 228 | + } |
| 229 | + |
| 230 | + private Object convertBytesToObject(byte[] body, String encoding, Class<?> targetClass) throws IOException { |
| 231 | + String contentAsString = new String(body, encoding); |
| 232 | + return this.objectMapper.readValue(contentAsString, this.objectMapper.constructType(targetClass)); |
| 233 | + } |
| 234 | + |
| 235 | + @Override |
| 236 | + protected Message createMessage(Object objectToConvert, MessageProperties messageProperties) |
| 237 | + throws MessageConversionException { |
| 238 | + |
| 239 | + return createMessage(objectToConvert, messageProperties, null); |
| 240 | + } |
| 241 | + |
| 242 | + @Override |
| 243 | + protected Message createMessage(Object objectToConvert, MessageProperties messageProperties, Type genericType) |
| 244 | + throws MessageConversionException { |
| 245 | + |
| 246 | + byte[] bytes; |
| 247 | + try { |
| 248 | + String jsonString = this.objectMapper |
| 249 | + .writeValueAsString(objectToConvert); |
| 250 | + bytes = jsonString.getBytes(getDefaultCharset()); |
| 251 | + } |
| 252 | + catch (IOException e) { |
| 253 | + throw new MessageConversionException("Failed to convert Message content", e); |
| 254 | + } |
| 255 | + messageProperties.setContentType(this.contentType.toString()); |
| 256 | + messageProperties.setContentEncoding(getDefaultCharset()); |
| 257 | + messageProperties.setContentLength(bytes.length); |
| 258 | + |
| 259 | + if (getClassMapper() == null) { |
| 260 | + getJavaTypeMapper().fromJavaType(this.objectMapper.constructType( |
| 261 | + genericType == null ? objectToConvert.getClass() : genericType), messageProperties); |
| 262 | + } |
| 263 | + else { |
| 264 | + getClassMapper().fromClass(objectToConvert.getClass(), |
| 265 | + messageProperties); |
| 266 | + } |
| 267 | + |
| 268 | + return new Message(bytes, messageProperties); |
| 269 | + } |
| 270 | + |
| 271 | +} |
0 commit comments