001/*
002 * Copyright 2002-2017 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.annotation.Annotation;
020import java.security.Principal;
021import java.util.Map;
022
023import org.springframework.core.MethodParameter;
024import org.springframework.core.annotation.AnnotatedElementUtils;
025import org.springframework.core.annotation.AnnotationUtils;
026import org.springframework.messaging.Message;
027import org.springframework.messaging.MessageChannel;
028import org.springframework.messaging.MessageHeaders;
029import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
030import org.springframework.messaging.handler.annotation.SendTo;
031import org.springframework.messaging.handler.annotation.support.DestinationVariableMethodArgumentResolver;
032import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
033import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
034import org.springframework.messaging.simp.SimpMessageSendingOperations;
035import org.springframework.messaging.simp.SimpMessageType;
036import org.springframework.messaging.simp.SimpMessagingTemplate;
037import org.springframework.messaging.simp.annotation.SendToUser;
038import org.springframework.messaging.simp.user.DestinationUserNameProvider;
039import org.springframework.messaging.support.MessageHeaderInitializer;
040import org.springframework.util.Assert;
041import org.springframework.util.ObjectUtils;
042import org.springframework.util.PropertyPlaceholderHelper;
043import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
044import org.springframework.util.StringUtils;
045
046/**
047 * A {@link HandlerMethodReturnValueHandler} for sending to destinations specified in a
048 * {@link SendTo} or {@link SendToUser} method-level annotations.
049 *
050 * <p>The value returned from the method is converted, and turned to a {@link Message} and
051 * sent through the provided {@link MessageChannel}. The message is then enriched with the
052 * session id of the input message as well as the destination from the annotation(s).
053 * If multiple destinations are specified, a copy of the message is sent to each destination.
054 *
055 * @author Rossen Stoyanchev
056 * @author Sebastien Deleuze
057 * @since 4.0
058 */
059public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
060
061        private final SimpMessageSendingOperations messagingTemplate;
062
063        private final boolean annotationRequired;
064
065        private String defaultDestinationPrefix = "/topic";
066
067        private String defaultUserDestinationPrefix = "/queue";
068
069        private PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, false);
070
071        private MessageHeaderInitializer headerInitializer;
072
073
074        public SendToMethodReturnValueHandler(SimpMessageSendingOperations messagingTemplate, boolean annotationRequired) {
075                Assert.notNull(messagingTemplate, "'messagingTemplate' must not be null");
076                this.messagingTemplate = messagingTemplate;
077                this.annotationRequired = annotationRequired;
078        }
079
080
081        /**
082         * Configure a default prefix to add to message destinations in cases where a method
083         * is not annotated with {@link SendTo @SendTo} or does not specify any destinations
084         * through the annotation's value attribute.
085         * <p>By default, the prefix is set to "/topic".
086         */
087        public void setDefaultDestinationPrefix(String defaultDestinationPrefix) {
088                this.defaultDestinationPrefix = defaultDestinationPrefix;
089        }
090
091        /**
092         * Return the configured default destination prefix.
093         * @see #setDefaultDestinationPrefix(String)
094         */
095        public String getDefaultDestinationPrefix() {
096                return this.defaultDestinationPrefix;
097        }
098
099        /**
100         * Configure a default prefix to add to message destinations in cases where a
101         * method is annotated with {@link SendToUser @SendToUser} but does not specify
102         * any destinations through the annotation's value attribute.
103         * <p>By default, the prefix is set to "/queue".
104         */
105        public void setDefaultUserDestinationPrefix(String prefix) {
106                this.defaultUserDestinationPrefix = prefix;
107        }
108
109        /**
110         * Return the configured default user destination prefix.
111         * @see #setDefaultUserDestinationPrefix(String)
112         */
113        public String getDefaultUserDestinationPrefix() {
114                return this.defaultUserDestinationPrefix;
115        }
116
117        /**
118         * Configure a {@link MessageHeaderInitializer} to apply to the headers of all
119         * messages sent to the client outbound channel.
120         * <p>By default this property is not set.
121         */
122        public void setHeaderInitializer(MessageHeaderInitializer headerInitializer) {
123                this.headerInitializer = headerInitializer;
124        }
125
126        /**
127         * Return the configured header initializer.
128         */
129        public MessageHeaderInitializer getHeaderInitializer() {
130                return this.headerInitializer;
131        }
132
133
134        @Override
135        public boolean supportsReturnType(MethodParameter returnType) {
136                return (returnType.hasMethodAnnotation(SendTo.class) ||
137                                AnnotatedElementUtils.hasAnnotation(returnType.getDeclaringClass(), SendTo.class) ||
138                                returnType.hasMethodAnnotation(SendToUser.class) ||
139                                AnnotatedElementUtils.hasAnnotation(returnType.getDeclaringClass(), SendToUser.class) ||
140                                !this.annotationRequired);
141        }
142
143        @Override
144        public void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message)
145                        throws Exception {
146
147                if (returnValue == null) {
148                        return;
149                }
150
151                MessageHeaders headers = message.getHeaders();
152                String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
153                PlaceholderResolver varResolver = initVarResolver(headers);
154                Object annotation = findAnnotation(returnType);
155
156                if (annotation instanceof SendToUser) {
157                        SendToUser sendToUser = (SendToUser) annotation;
158                        boolean broadcast = sendToUser.broadcast();
159                        String user = getUserName(message, headers);
160                        if (user == null) {
161                                if (sessionId == null) {
162                                        throw new MissingSessionUserException(message);
163                                }
164                                user = sessionId;
165                                broadcast = false;
166                        }
167                        String[] destinations = getTargetDestinations(sendToUser, message, this.defaultUserDestinationPrefix);
168                        for (String destination : destinations) {
169                                destination = this.placeholderHelper.replacePlaceholders(destination, varResolver);
170                                if (broadcast) {
171                                        this.messagingTemplate.convertAndSendToUser(
172                                                        user, destination, returnValue, createHeaders(null, returnType));
173                                }
174                                else {
175                                        this.messagingTemplate.convertAndSendToUser(
176                                                        user, destination, returnValue, createHeaders(sessionId, returnType));
177                                }
178                        }
179                }
180                else {
181                        SendTo sendTo = (SendTo) annotation;  // possibly null
182                        String[] destinations = getTargetDestinations(sendTo, message, this.defaultDestinationPrefix);
183                        for (String destination : destinations) {
184                                destination = this.placeholderHelper.replacePlaceholders(destination, varResolver);
185                                this.messagingTemplate.convertAndSend(destination, returnValue, createHeaders(sessionId, returnType));
186                        }
187                }
188        }
189
190        private Object findAnnotation(MethodParameter returnType) {
191                Annotation[] anns = new Annotation[4];
192                anns[0] = AnnotatedElementUtils.findMergedAnnotation(returnType.getMethod(), SendToUser.class);
193                anns[1] = AnnotatedElementUtils.findMergedAnnotation(returnType.getMethod(), SendTo.class);
194                anns[2] = AnnotatedElementUtils.findMergedAnnotation(returnType.getDeclaringClass(), SendToUser.class);
195                anns[3] = AnnotatedElementUtils.findMergedAnnotation(returnType.getDeclaringClass(), SendTo.class);
196
197                if (anns[0] != null && !ObjectUtils.isEmpty(((SendToUser) anns[0]).value())) {
198                        return anns[0];
199                }
200                if (anns[1] != null && !ObjectUtils.isEmpty(((SendTo) anns[1]).value())) {
201                        return anns[1];
202                }
203                if (anns[2] != null && !ObjectUtils.isEmpty(((SendToUser) anns[2]).value())) {
204                        return anns[2];
205                }
206                if (anns[3] != null && !ObjectUtils.isEmpty(((SendTo) anns[3]).value())) {
207                        return anns[3];
208                }
209
210                for (int i=0; i < 4; i++) {
211                        if (anns[i] != null) {
212                                return anns[i];
213                        }
214                }
215
216                return null;
217        }
218
219        @SuppressWarnings("unchecked")
220        private PlaceholderResolver initVarResolver(MessageHeaders headers) {
221                String name = DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER;
222                Map<String, String> vars = (Map<String, String>) headers.get(name);
223                return new DestinationVariablePlaceholderResolver(vars);
224        }
225
226        protected String getUserName(Message<?> message, MessageHeaders headers) {
227                Principal principal = SimpMessageHeaderAccessor.getUser(headers);
228                if (principal != null) {
229                        return (principal instanceof DestinationUserNameProvider ?
230                                        ((DestinationUserNameProvider) principal).getDestinationUserName() : principal.getName());
231                }
232                return null;
233        }
234
235        protected String[] getTargetDestinations(Annotation annotation, Message<?> message, String defaultPrefix) {
236                if (annotation != null) {
237                        String[] value = (String[]) AnnotationUtils.getValue(annotation);
238                        if (!ObjectUtils.isEmpty(value)) {
239                                return value;
240                        }
241                }
242                String name = DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER;
243                String destination = (String) message.getHeaders().get(name);
244                if (!StringUtils.hasText(destination)) {
245                        throw new IllegalStateException("No lookup destination header in " + message);
246                }
247
248                return (destination.startsWith("/") ?
249                                new String[] {defaultPrefix + destination} : new String[] {defaultPrefix + '/' + destination});
250        }
251
252        private MessageHeaders createHeaders(String sessionId, MethodParameter returnType) {
253                SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
254                if (getHeaderInitializer() != null) {
255                        getHeaderInitializer().initHeaders(headerAccessor);
256                }
257                if (sessionId != null) {
258                        headerAccessor.setSessionId(sessionId);
259                }
260                headerAccessor.setHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER, returnType);
261                headerAccessor.setLeaveMutable(true);
262                return headerAccessor.getMessageHeaders();
263        }
264
265
266        @Override
267        public String toString() {
268                return "SendToMethodReturnValueHandler [annotationRequired=" + annotationRequired + "]";
269        }
270
271
272        private static class DestinationVariablePlaceholderResolver implements PlaceholderResolver {
273
274                private final Map<String, String> vars;
275
276                public DestinationVariablePlaceholderResolver(Map<String, String> vars) {
277                        this.vars = vars;
278                }
279
280                @Override
281                public String resolvePlaceholder(String placeholderName) {
282                        return (this.vars != null ? this.vars.get(placeholderName) : null);
283                }
284        }
285
286}