001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.messaging.simp.annotation.support;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Comparator;
023import java.util.LinkedHashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.commons.logging.Log;
029
030import org.springframework.beans.factory.config.ConfigurableBeanFactory;
031import org.springframework.context.ApplicationContext;
032import org.springframework.context.ConfigurableApplicationContext;
033import org.springframework.context.EmbeddedValueResolverAware;
034import org.springframework.context.SmartLifecycle;
035import org.springframework.core.annotation.AnnotatedElementUtils;
036import org.springframework.core.convert.ConversionService;
037import org.springframework.format.support.DefaultFormattingConversionService;
038import org.springframework.lang.Nullable;
039import org.springframework.messaging.Message;
040import org.springframework.messaging.MessageChannel;
041import org.springframework.messaging.SubscribableChannel;
042import org.springframework.messaging.converter.ByteArrayMessageConverter;
043import org.springframework.messaging.converter.CompositeMessageConverter;
044import org.springframework.messaging.converter.MessageConverter;
045import org.springframework.messaging.converter.StringMessageConverter;
046import org.springframework.messaging.core.AbstractMessageSendingTemplate;
047import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
048import org.springframework.messaging.handler.HandlerMethod;
049import org.springframework.messaging.handler.annotation.MessageMapping;
050import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver;
051import org.springframework.messaging.handler.annotation.support.DestinationVariableMethodArgumentResolver;
052import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver;
053import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver;
054import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver;
055import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver;
056import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
057import org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler;
058import org.springframework.messaging.handler.invocation.CompletableFutureReturnValueHandler;
059import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
060import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
061import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandlerComposite;
062import org.springframework.messaging.handler.invocation.ListenableFutureReturnValueHandler;
063import org.springframework.messaging.handler.invocation.ReactiveReturnValueHandler;
064import org.springframework.messaging.simp.SimpAttributesContextHolder;
065import org.springframework.messaging.simp.SimpLogging;
066import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
067import org.springframework.messaging.simp.SimpMessageMappingInfo;
068import org.springframework.messaging.simp.SimpMessageSendingOperations;
069import org.springframework.messaging.simp.SimpMessageTypeMessageCondition;
070import org.springframework.messaging.simp.SimpMessagingTemplate;
071import org.springframework.messaging.simp.annotation.SubscribeMapping;
072import org.springframework.messaging.support.MessageHeaderAccessor;
073import org.springframework.messaging.support.MessageHeaderInitializer;
074import org.springframework.stereotype.Controller;
075import org.springframework.util.AntPathMatcher;
076import org.springframework.util.Assert;
077import org.springframework.util.ClassUtils;
078import org.springframework.util.CollectionUtils;
079import org.springframework.util.PathMatcher;
080import org.springframework.util.StringValueResolver;
081import org.springframework.validation.Validator;
082
083/**
084 * A handler for messages delegating to {@link MessageMapping @MessageMapping}
085 * and {@link SubscribeMapping @SubscribeMapping} annotated methods.
086 *
087 * <p>Supports Ant-style path patterns with template variables.
088 *
089 * @author Rossen Stoyanchev
090 * @author Brian Clozel
091 * @author Juergen Hoeller
092 * @since 4.0
093 */
094public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHandler<SimpMessageMappingInfo>
095                implements EmbeddedValueResolverAware, SmartLifecycle {
096
097        private static final boolean reactorPresent = ClassUtils.isPresent(
098                        "reactor.core.publisher.Flux", SimpAnnotationMethodMessageHandler.class.getClassLoader());
099
100
101        private final SubscribableChannel clientInboundChannel;
102
103        private final SimpMessageSendingOperations clientMessagingTemplate;
104
105        private final SimpMessageSendingOperations brokerTemplate;
106
107        private MessageConverter messageConverter;
108
109        private ConversionService conversionService = new DefaultFormattingConversionService();
110
111        private PathMatcher pathMatcher = new AntPathMatcher();
112
113        private boolean slashPathSeparator = true;
114
115        @Nullable
116        private Validator validator;
117
118        @Nullable
119        private StringValueResolver valueResolver;
120
121        @Nullable
122        private MessageHeaderInitializer headerInitializer;
123
124        private volatile boolean running = false;
125
126        private final Object lifecycleMonitor = new Object();
127
128
129        /**
130         * Create an instance of SimpAnnotationMethodMessageHandler with the given
131         * message channels and broker messaging template.
132         * @param clientInboundChannel the channel for receiving messages from clients (e.g. WebSocket clients)
133         * @param clientOutboundChannel the channel for messages to clients (e.g. WebSocket clients)
134         * @param brokerTemplate a messaging template to send application messages to the broker
135         */
136        public SimpAnnotationMethodMessageHandler(SubscribableChannel clientInboundChannel,
137                        MessageChannel clientOutboundChannel, SimpMessageSendingOperations brokerTemplate) {
138
139                Assert.notNull(clientInboundChannel, "clientInboundChannel must not be null");
140                Assert.notNull(clientOutboundChannel, "clientOutboundChannel must not be null");
141                Assert.notNull(brokerTemplate, "brokerTemplate must not be null");
142
143                this.clientInboundChannel = clientInboundChannel;
144                this.clientMessagingTemplate = new SimpMessagingTemplate(clientOutboundChannel);
145                this.brokerTemplate = brokerTemplate;
146
147                Collection<MessageConverter> converters = new ArrayList<>();
148                converters.add(new StringMessageConverter());
149                converters.add(new ByteArrayMessageConverter());
150                this.messageConverter = new CompositeMessageConverter(converters);
151        }
152
153
154        /**
155         * {@inheritDoc}
156         * <p>Destination prefixes are expected to be slash-separated Strings and
157         * therefore a slash is automatically appended where missing to ensure a
158         * proper prefix-based match (i.e. matching complete segments).
159         * <p>Note however that the remaining portion of a destination after the
160         * prefix may use a different separator (e.g. commonly "." in messaging)
161         * depending on the configured {@code PathMatcher}.
162         */
163        @Override
164        public void setDestinationPrefixes(@Nullable Collection<String> prefixes) {
165                super.setDestinationPrefixes(appendSlashes(prefixes));
166        }
167
168        @Nullable
169        private static Collection<String> appendSlashes(@Nullable Collection<String> prefixes) {
170                if (CollectionUtils.isEmpty(prefixes)) {
171                        return prefixes;
172                }
173                Collection<String> result = new ArrayList<>(prefixes.size());
174                for (String prefix : prefixes) {
175                        if (!prefix.endsWith("/")) {
176                                prefix = prefix + "/";
177                        }
178                        result.add(prefix);
179                }
180                return result;
181        }
182
183        /**
184         * Configure a {@link MessageConverter} to use to convert the payload of a message from
185         * its serialized form with a specific MIME type to an Object matching the target method
186         * parameter. The converter is also used when sending a message to the message broker.
187         * @see CompositeMessageConverter
188         */
189        public void setMessageConverter(MessageConverter converter) {
190                this.messageConverter = converter;
191                ((AbstractMessageSendingTemplate<?>) this.clientMessagingTemplate).setMessageConverter(converter);
192        }
193
194        /**
195         * Return the configured {@link MessageConverter}.
196         */
197        public MessageConverter getMessageConverter() {
198                return this.messageConverter;
199        }
200
201        /**
202         * Configure a {@link ConversionService} to use when resolving method arguments,
203         * for example message header values.
204         * <p>By default, {@link DefaultFormattingConversionService} is used.
205         */
206        public void setConversionService(ConversionService conversionService) {
207                this.conversionService = conversionService;
208        }
209
210        /**
211         * Return the configured {@link ConversionService}.
212         */
213        public ConversionService getConversionService() {
214                return this.conversionService;
215        }
216
217        /**
218         * Set the PathMatcher implementation to use for matching destinations
219         * against configured destination patterns.
220         * <p>By default, {@link AntPathMatcher} is used.
221         */
222        public void setPathMatcher(PathMatcher pathMatcher) {
223                Assert.notNull(pathMatcher, "PathMatcher must not be null");
224                this.pathMatcher = pathMatcher;
225                this.slashPathSeparator = this.pathMatcher.combine("a", "a").equals("a/a");
226        }
227
228        /**
229         * Return the PathMatcher implementation to use for matching destinations.
230         */
231        public PathMatcher getPathMatcher() {
232                return this.pathMatcher;
233        }
234
235        /**
236         * Return the configured Validator instance.
237         */
238        @Nullable
239        public Validator getValidator() {
240                return this.validator;
241        }
242
243        /**
244         * Set the Validator instance used for validating {@code @Payload} arguments.
245         * @see org.springframework.validation.annotation.Validated
246         * @see PayloadMethodArgumentResolver
247         */
248        public void setValidator(@Nullable Validator validator) {
249                this.validator = validator;
250        }
251
252        @Override
253        public void setEmbeddedValueResolver(StringValueResolver resolver) {
254                this.valueResolver = resolver;
255        }
256
257        /**
258         * Configure a {@link MessageHeaderInitializer} to pass on to
259         * {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers}
260         * that send messages from controller return values.
261         * <p>By default, this property is not set.
262         */
263        public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitializer) {
264                this.headerInitializer = headerInitializer;
265        }
266
267        /**
268         * Return the configured header initializer.
269         */
270        @Nullable
271        public MessageHeaderInitializer getHeaderInitializer() {
272                return this.headerInitializer;
273        }
274
275
276        @Override
277        public final void start() {
278                synchronized (this.lifecycleMonitor) {
279                        this.clientInboundChannel.subscribe(this);
280                        this.running = true;
281                }
282        }
283
284        @Override
285        public final void stop() {
286                synchronized (this.lifecycleMonitor) {
287                        this.running = false;
288                        this.clientInboundChannel.unsubscribe(this);
289                }
290        }
291
292        @Override
293        public final void stop(Runnable callback) {
294                synchronized (this.lifecycleMonitor) {
295                        stop();
296                        callback.run();
297                }
298        }
299
300        @Override
301        public final boolean isRunning() {
302                return this.running;
303        }
304
305
306        @Override
307        protected List<HandlerMethodArgumentResolver> initArgumentResolvers() {
308                ApplicationContext context = getApplicationContext();
309                ConfigurableBeanFactory beanFactory = (context instanceof ConfigurableApplicationContext ?
310                                ((ConfigurableApplicationContext) context).getBeanFactory() : null);
311
312                List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
313
314                // Annotation-based argument resolution
315                resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory));
316                resolvers.add(new HeadersMethodArgumentResolver());
317                resolvers.add(new DestinationVariableMethodArgumentResolver(this.conversionService));
318
319                // Type-based argument resolution
320                resolvers.add(new PrincipalMethodArgumentResolver());
321                resolvers.add(new MessageMethodArgumentResolver(this.messageConverter));
322
323                resolvers.addAll(getCustomArgumentResolvers());
324                resolvers.add(new PayloadMethodArgumentResolver(this.messageConverter, this.validator));
325
326                return resolvers;
327        }
328
329        @Override
330        protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
331                List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
332
333                // Single-purpose return value types
334
335                handlers.add(new ListenableFutureReturnValueHandler());
336                handlers.add(new CompletableFutureReturnValueHandler());
337                if (reactorPresent) {
338                        handlers.add(new ReactiveReturnValueHandler());
339                }
340
341                // Annotation-based return value types
342
343                SendToMethodReturnValueHandler sendToHandler =
344                                new SendToMethodReturnValueHandler(this.brokerTemplate, true);
345                sendToHandler.setHeaderInitializer(this.headerInitializer);
346                handlers.add(sendToHandler);
347
348                SubscriptionMethodReturnValueHandler subscriptionHandler =
349                                new SubscriptionMethodReturnValueHandler(this.clientMessagingTemplate);
350                subscriptionHandler.setHeaderInitializer(this.headerInitializer);
351                handlers.add(subscriptionHandler);
352
353                // Custom return value types
354
355                handlers.addAll(getCustomReturnValueHandlers());
356
357                // Catch-all
358
359                sendToHandler = new SendToMethodReturnValueHandler(this.brokerTemplate, false);
360                sendToHandler.setHeaderInitializer(this.headerInitializer);
361                handlers.add(sendToHandler);
362
363                return handlers;
364        }
365
366        @Override
367        protected Log getReturnValueHandlerLogger() {
368                return SimpLogging.forLog(HandlerMethodReturnValueHandlerComposite.defaultLogger);
369        }
370
371        @Override
372        protected Log getHandlerMethodLogger() {
373                return SimpLogging.forLog(HandlerMethod.defaultLogger);
374        }
375
376
377        @Override
378        protected boolean isHandler(Class<?> beanType) {
379                return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
380        }
381
382        @Override
383        @Nullable
384        protected SimpMessageMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
385                MessageMapping messageAnn = AnnotatedElementUtils.findMergedAnnotation(method, MessageMapping.class);
386                if (messageAnn != null) {
387                        MessageMapping typeAnn = AnnotatedElementUtils.findMergedAnnotation(handlerType, MessageMapping.class);
388                        // Only actually register it if there are destinations specified;
389                        // otherwise @MessageMapping is just being used as a (meta-annotation) marker.
390                        if (messageAnn.value().length > 0 || (typeAnn != null && typeAnn.value().length > 0)) {
391                                SimpMessageMappingInfo result = createMessageMappingCondition(messageAnn.value());
392                                if (typeAnn != null) {
393                                        result = createMessageMappingCondition(typeAnn.value()).combine(result);
394                                }
395                                return result;
396                        }
397                }
398
399                SubscribeMapping subscribeAnn = AnnotatedElementUtils.findMergedAnnotation(method, SubscribeMapping.class);
400                if (subscribeAnn != null) {
401                        MessageMapping typeAnn = AnnotatedElementUtils.findMergedAnnotation(handlerType, MessageMapping.class);
402                        // Only actually register it if there are destinations specified;
403                        // otherwise @SubscribeMapping is just being used as a (meta-annotation) marker.
404                        if (subscribeAnn.value().length > 0 || (typeAnn != null && typeAnn.value().length > 0)) {
405                                SimpMessageMappingInfo result = createSubscribeMappingCondition(subscribeAnn.value());
406                                if (typeAnn != null) {
407                                        result = createMessageMappingCondition(typeAnn.value()).combine(result);
408                                }
409                                return result;
410                        }
411                }
412
413                return null;
414        }
415
416        private SimpMessageMappingInfo createMessageMappingCondition(String[] destinations) {
417                String[] resolvedDestinations = resolveEmbeddedValuesInDestinations(destinations);
418                return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE,
419                                new DestinationPatternsMessageCondition(resolvedDestinations, this.pathMatcher));
420        }
421
422        private SimpMessageMappingInfo createSubscribeMappingCondition(String[] destinations) {
423                String[] resolvedDestinations = resolveEmbeddedValuesInDestinations(destinations);
424                return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.SUBSCRIBE,
425                                new DestinationPatternsMessageCondition(resolvedDestinations, this.pathMatcher));
426        }
427
428        /**
429         * Resolve placeholder values in the given array of destinations.
430         * @return a new array with updated destinations
431         * @since 4.2
432         */
433        protected String[] resolveEmbeddedValuesInDestinations(String[] destinations) {
434                if (this.valueResolver == null) {
435                        return destinations;
436                }
437                String[] result = new String[destinations.length];
438                for (int i = 0; i < destinations.length; i++) {
439                        result[i] = this.valueResolver.resolveStringValue(destinations[i]);
440                }
441                return result;
442        }
443
444        @Override
445        protected Set<String> getDirectLookupDestinations(SimpMessageMappingInfo mapping) {
446                Set<String> result = new LinkedHashSet<>();
447                for (String pattern : mapping.getDestinationConditions().getPatterns()) {
448                        if (!this.pathMatcher.isPattern(pattern)) {
449                                result.add(pattern);
450                        }
451                }
452                return result;
453        }
454
455        @Override
456        @Nullable
457        protected String getDestination(Message<?> message) {
458                return SimpMessageHeaderAccessor.getDestination(message.getHeaders());
459        }
460
461        @Override
462        protected String getLookupDestination(@Nullable String destination) {
463                if (destination == null) {
464                        return null;
465                }
466                if (CollectionUtils.isEmpty(getDestinationPrefixes())) {
467                        return destination;
468                }
469                for (String prefix : getDestinationPrefixes()) {
470                        if (destination.startsWith(prefix)) {
471                                if (this.slashPathSeparator) {
472                                        return destination.substring(prefix.length() - 1);
473                                }
474                                else {
475                                        return destination.substring(prefix.length());
476                                }
477                        }
478                }
479                return null;
480        }
481
482        @Override
483        @Nullable
484        protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message<?> message) {
485                return mapping.getMatchingCondition(message);
486
487        }
488
489        @Override
490        protected Comparator<SimpMessageMappingInfo> getMappingComparator(final Message<?> message) {
491                return (info1, info2) -> info1.compareTo(info2, message);
492        }
493
494        @Override
495        protected void handleMatch(SimpMessageMappingInfo mapping, HandlerMethod handlerMethod,
496                        String lookupDestination, Message<?> message) {
497
498                Set<String> patterns = mapping.getDestinationConditions().getPatterns();
499                if (!CollectionUtils.isEmpty(patterns)) {
500                        String pattern = patterns.iterator().next();
501                        Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(pattern, lookupDestination);
502                        if (!CollectionUtils.isEmpty(vars)) {
503                                MessageHeaderAccessor mha = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class);
504                                Assert.state(mha != null && mha.isMutable(), "Mutable MessageHeaderAccessor required");
505                                mha.setHeader(DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars);
506                        }
507                }
508
509                try {
510                        SimpAttributesContextHolder.setAttributesFromMessage(message);
511                        super.handleMatch(mapping, handlerMethod, lookupDestination, message);
512                }
513                finally {
514                        SimpAttributesContextHolder.resetAttributes();
515                }
516        }
517
518        @Override
519        protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) {
520                return new AnnotationExceptionHandlerMethodResolver(beanType);
521        }
522
523}