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