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}