/* * Copyright 2013-2014 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.cloud.aws.messaging.listener; import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.cloud.aws.messaging.listener.annotation.SqsListener; import org.springframework.cloud.aws.messaging.listener.support.AcknowledgmentHandlerMethodArgumentResolver; import org.springframework.cloud.aws.messaging.support.NotificationMessageArgumentResolver; import org.springframework.cloud.aws.messaging.support.NotificationSubjectArgumentResolver; import org.springframework.cloud.aws.messaging.support.converter.ObjectMessageConverter; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.SimpleMessageConverter; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver; import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver; import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; import org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; import org.springframework.util.ClassUtils; import org.springframework.util.comparator.ComparableComparator; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; /** * @author Agim Emruli * @author Alain Sahli * @since 1.0 */ public class QueueMessageHandler extends AbstractMethodMessageHandler<QueueMessageHandler.MappingInformation> { static final String LOGICAL_RESOURCE_ID = "LogicalResourceId"; static final String ACKNOWLEDGMENT = "Acknowledgment"; private static final boolean JACKSON_2_PRESENT = ClassUtils.isPresent( "com.fasterxml.jackson.databind.ObjectMapper", QueueMessageHandler.class.getClassLoader()); @Override protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); resolvers.addAll(getCustomArgumentResolvers()); resolvers.add(new HeaderMethodArgumentResolver(null, null)); resolvers.add(new HeadersMethodArgumentResolver()); resolvers.add(new NotificationSubjectArgumentResolver()); resolvers.add(new AcknowledgmentHandlerMethodArgumentResolver(ACKNOWLEDGMENT)); CompositeMessageConverter compositeMessageConverter = createPayloadArgumentCompositeConverter(); resolvers.add(new NotificationMessageArgumentResolver(compositeMessageConverter)); resolvers.add(new PayloadArgumentResolver(compositeMessageConverter, new NoOpValidator())); return resolvers; } @Override protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() { ArrayList<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(); handlers.addAll(this.getCustomReturnValueHandlers()); return handlers; } @Override protected boolean isHandler(Class<?> beanType) { return true; } @Override protected MappingInformation getMappingForMethod(Method method, Class<?> handlerType) { SqsListener sqsListenerAnnotation = AnnotationUtils.findAnnotation(method, SqsListener.class); if (sqsListenerAnnotation != null && sqsListenerAnnotation.value().length > 0) { if (sqsListenerAnnotation.deletionPolicy() == SqsMessageDeletionPolicy.NEVER && hasNoAcknowledgmentParameter(method.getParameterTypes())) { this.logger.warn("Listener method '" + method.getName() + "' in type '" + method.getDeclaringClass().getName() + "' has deletion policy 'NEVER' but does not have a parameter of type Acknowledgment."); } return new MappingInformation(resolveDestinationNames(sqsListenerAnnotation.value()), sqsListenerAnnotation.deletionPolicy()); } MessageMapping messageMappingAnnotation = AnnotationUtils.findAnnotation(method, MessageMapping.class); if (messageMappingAnnotation != null && messageMappingAnnotation.value().length > 0) { return new MappingInformation(resolveDestinationNames(messageMappingAnnotation.value()), SqsMessageDeletionPolicy.ALWAYS); } return null; } private boolean hasNoAcknowledgmentParameter(Class<?>[] parameterTypes) { for (Class<?> parameterType : parameterTypes) { if (ClassUtils.isAssignable(Acknowledgment.class, parameterType)) { return false; } } return true; } private Set<String> resolveDestinationNames(String[] destinationNames) { Set<String> result = new HashSet<>(destinationNames.length); for (String destinationName : destinationNames) { result.addAll(Arrays.asList(resolveName(destinationName))); } return result; } private String[] resolveName(String name) { if (!(getApplicationContext() instanceof ConfigurableApplicationContext)) { return wrapInStringArray(name); } ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) getApplicationContext(); ConfigurableBeanFactory configurableBeanFactory = applicationContext.getBeanFactory(); String placeholdersResolved = configurableBeanFactory.resolveEmbeddedValue(name); BeanExpressionResolver exprResolver = configurableBeanFactory.getBeanExpressionResolver(); if (exprResolver == null) { return wrapInStringArray(name); } Object result = exprResolver.evaluate(placeholdersResolved, new BeanExpressionContext(configurableBeanFactory, null)); if (result instanceof String[]) { return (String[]) result; } else if (result != null) { return wrapInStringArray(result); } else { return wrapInStringArray(name); } } private static String[] wrapInStringArray(Object valueToWrap) { return new String[]{valueToWrap.toString()}; } @Override protected Set<String> getDirectLookupDestinations(MappingInformation mapping) { return mapping.getLogicalResourceIds(); } @Override protected String getDestination(Message<?> message) { return message.getHeaders().get(LOGICAL_RESOURCE_ID).toString(); } @Override protected MappingInformation getMatchingMapping(MappingInformation mapping, Message<?> message) { if (mapping.getLogicalResourceIds().contains(getDestination(message))) { return mapping; } else { return null; } } @Override protected Comparator<MappingInformation> getMappingComparator(Message<?> message) { return new ComparableComparator<>(); } @Override protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) { return new AnnotationExceptionHandlerMethodResolver(beanType); } @Override protected void handleNoMatch(Set<MappingInformation> ts, String lookupDestination, Message<?> message) { this.logger.warn("No match found"); } @Override protected void processHandlerMethodException(HandlerMethod handlerMethod, Exception ex, Message<?> message) { super.processHandlerMethodException(handlerMethod, ex, message); throw new MessagingException("An exception occurred while invoking the handler method", ex); } private CompositeMessageConverter createPayloadArgumentCompositeConverter() { List<MessageConverter> payloadArgumentConverters = new ArrayList<>(); if (JACKSON_2_PRESENT) { MappingJackson2MessageConverter jacksonMessageConverter = new MappingJackson2MessageConverter(); jacksonMessageConverter.setSerializedPayloadClass(String.class); jacksonMessageConverter.setStrictContentTypeMatch(true); payloadArgumentConverters.add(jacksonMessageConverter); } ObjectMessageConverter objectMessageConverter = new ObjectMessageConverter(); objectMessageConverter.setStrictContentTypeMatch(true); payloadArgumentConverters.add(objectMessageConverter); payloadArgumentConverters.add(new SimpleMessageConverter()); return new CompositeMessageConverter(payloadArgumentConverters); } @SuppressWarnings("ComparableImplementedButEqualsNotOverridden") protected static class MappingInformation implements Comparable<MappingInformation> { private final Set<String> logicalResourceIds; private final SqsMessageDeletionPolicy deletionPolicy; public MappingInformation(Set<String> logicalResourceIds, SqsMessageDeletionPolicy deletionPolicy) { this.logicalResourceIds = Collections.unmodifiableSet(logicalResourceIds); this.deletionPolicy = deletionPolicy; } public Set<String> getLogicalResourceIds() { return this.logicalResourceIds; } public SqsMessageDeletionPolicy getDeletionPolicy() { return this.deletionPolicy; } @SuppressWarnings("NullableProblems") @Override public int compareTo(MappingInformation o) { return 0; } } private static final class NoOpValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return false; } @Override public void validate(Object target, Errors errors) { } } }