diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/MessageBody.java b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageBody.java new file mode 100644 index 0000000000..bee4135b12 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageBody.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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.web.messaging; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation indicating a method parameter should be bound to the body of a message. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MessageBody { + + /** + * Whether body content is required. + *

Default is {@code true}, leading to an exception thrown in case + * there is no body content. Switch this to {@code false} if you prefer + * {@code null} to be passed when the body content is {@code null}. + */ + boolean required() default true; + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/MessageBroker.java b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageBroker.java new file mode 100644 index 0000000000..69262ac734 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageBroker.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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.web.messaging; + +import java.io.IOException; + +import org.springframework.http.MediaType; +import org.springframework.web.messaging.converter.ContentTypeNotSupportedException; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface MessageBroker { + + void send(String destination, Object content, MediaType contentType) + throws IOException, ContentTypeNotSupportedException; + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/MessageMapping.java b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageMapping.java new file mode 100644 index 0000000000..f203bb1623 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageMapping.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2013 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.web.messaging; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MessageMapping { + + /** + * The primary mapping expressed by this annotation. + *

The destination for a message (e.g. "/topic/echo"). + */ + String[] value() default {}; + + /** + * TODO + */ + MessageType messageType() default MessageType.NONE; + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/MessageType.java b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageType.java index 9e9ed26121..9ec6b52b21 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/MessageType.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/MessageType.java @@ -23,6 +23,6 @@ package org.springframework.web.messaging; */ public enum MessageType { - CONNECT, SUBSCRIBE, UNSUBSCRIBE, SEND, NONE + CONNECT, SUBSCRIBE, UNSUBSCRIBE, SEND, OTHER, NONE } diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/Subscription.java b/spring-websocket/src/main/java/org/springframework/web/messaging/Subscription.java new file mode 100644 index 0000000000..30e05b651f --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/Subscription.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2013 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.web.messaging; + +import java.io.IOException; + +import org.springframework.http.MediaType; +import org.springframework.web.messaging.converter.ContentTypeNotSupportedException; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface Subscription { + + void reply(Object content) throws IOException, ContentTypeNotSupportedException; + + void reply(Object content, MediaType mediaType) throws IOException, ContentTypeNotSupportedException; + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/converter/AbstractMessageConverter.java b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/AbstractMessageConverter.java new file mode 100644 index 0000000000..d4aa4c7056 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/AbstractMessageConverter.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2012 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.web.messaging.converter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; + +/** + * Abstract base class for most {@link MessageConverter} implementations. + * + *

This base class adds support for setting supported {@code MediaTypes}, through the + * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property. + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + * @since 4.0 + */ +public abstract class AbstractMessageConverter implements MessageConverter { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private List supportedMediaTypes = Collections.emptyList(); + + + /** + * Construct an {@code AbstractMessageConverter} with no supported media types. + * @see #setSupportedMediaTypes + */ + protected AbstractMessageConverter() { + } + + /** + * Construct an {@code AbstractMessageConverter} with one supported media type. + * + * @param supportedMediaType the supported media type + */ + protected AbstractMessageConverter(MediaType supportedMediaType) { + setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); + } + + /** + * Construct an {@code AbstractMessageConverter} with multiple supported media type. + * + * @param supportedMediaTypes the supported media types + */ + protected AbstractMessageConverter(MediaType... supportedMediaTypes) { + setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); + } + + + /** + * Set the list of {@link MediaType} objects supported by this converter. + */ + public void setSupportedMediaTypes(List supportedMediaTypes) { + Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' must not be empty"); + this.supportedMediaTypes = new ArrayList(supportedMediaTypes); + } + + @Override + public List getSupportedMediaTypes() { + return Collections.unmodifiableList(this.supportedMediaTypes); + } + + /** + * This implementation checks if the given class is {@linkplain #supports(Class) + * supported}, and if the {@linkplain #getSupportedMediaTypes() supported media types} + * {@linkplain MediaType#includes(MediaType) include} the given media type. + */ + @Override + public boolean canConvertFromPayload(Class clazz, MediaType mediaType) { + return supports(clazz) && canConvertFrom(mediaType); + } + + /** + * Indicates whether the given class is supported by this converter. + * @param clazz the class to test for support + * @return {@code true} if supported; {@code false} otherwise + */ + protected abstract boolean supports(Class clazz); + + /** + * Returns true if any of the {@linkplain #setSupportedMediaTypes(List) supported + * media types} include the given media type. + * + * @param mediaType the media type to read, can be {@code null} if not specified. + * Typically the value of a {@code Content-Type} header. + * @return {@code true} if the supported media types include the media type, or if the + * media type is {@code null} + */ + protected boolean canConvertFrom(MediaType mediaType) { + if (mediaType == null) { + return true; + } + for (MediaType supportedMediaType : getSupportedMediaTypes()) { + if (supportedMediaType.includes(mediaType)) { + return true; + } + } + return false; + } + + /** + * This implementation checks if the given class is {@linkplain #supports(Class) + * supported}, and if the {@linkplain #getSupportedMediaTypes() supported media types} + * {@linkplain MediaType#includes(MediaType) include} the given media type. + */ + @Override + public boolean canConvertToPayload(Class clazz, MediaType mediaType) { + return supports(clazz) && canConvertTo(mediaType); + } + + /** + * Returns {@code true} if the given media type includes any of the + * {@linkplain #setSupportedMediaTypes(List) supported media types}. + * + * @param mediaType the media type to write, can be {@code null} if not specified. + * Typically the value of an {@code Accept} header. + * @return {@code true} if the supported media types are compatible with the media + * type, or if the media type is {@code null} + */ + protected boolean canConvertTo(MediaType mediaType) { + if (mediaType == null || MediaType.ALL.equals(mediaType)) { + return true; + } + for (MediaType supportedMediaType : getSupportedMediaTypes()) { + if (supportedMediaType.isCompatibleWith(mediaType)) { + return true; + } + } + return false; + } + + /** + * This implementation simply delegates to + * {@link #convertFromPayloadInternal(Class, MediaType, byte[])}. Future + * implementations might add some default behavior, however. + */ + @Override + public T convertFromPayload(Class clazz, MediaType contentType, byte[] payload) + throws IOException { + + return convertFromPayloadInternal(clazz, contentType, payload); + } + + /** + * Abstract template method that reads the actual object. Invoked from {@link #read}. + * @param clazz the type of object to return + * @param contentType + * @param payload the content to convert from + * @return the converted object + * @throws IOException in case of I/O errors + */ + protected abstract T convertFromPayloadInternal(Class clazz, MediaType contentType, + byte[] payload) throws IOException; + + /** + * This implementation simply delegates to + * {@link #convertToPayloadInternal(Object, MediaType)}. Future + * implementations might add some default behavior, however. + */ + @Override + public byte[] convertToPayload(T t, MediaType contentType) throws IOException { + return convertToPayloadInternal(t, contentType); + } + + protected abstract byte[] convertToPayloadInternal(T t, MediaType contentType) + throws IOException; + + /** + * Returns the default content type for the given type. Called when {@link #write} + * is invoked without a specified content type parameter. + *

By default, this returns the first element of the + * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property, if any. + * Can be overridden in subclasses. + * @param t the type to return the content type for + * @return the content type, or {@code null} if not known + */ + protected MediaType getDefaultContentType(T t) throws IOException { + List mediaTypes = getSupportedMediaTypes(); + return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/converter/ContentTypeNotSupportedException.java b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/ContentTypeNotSupportedException.java new file mode 100644 index 0000000000..18a9c0fec1 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/ContentTypeNotSupportedException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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.web.messaging.converter; + +import org.springframework.http.MediaType; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class ContentTypeNotSupportedException extends Exception { + + private final MediaType mediaType; + + private final Class sourceOrTargetType; + + + public ContentTypeNotSupportedException(MediaType mediaType, Class sourceOrTargetType) { + super("Content type '" + mediaType + "' not supported"); + this.mediaType = mediaType; + this.sourceOrTargetType = sourceOrTargetType; + } + + public MediaType getMediaType() { + return this.mediaType; + } + + public Class getSourceOrTargetType() { + return this.sourceOrTargetType; + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/converter/MappingJackson2MessageConverter.java b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/MappingJackson2MessageConverter.java new file mode 100644 index 0000000000..6dd6405737 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/MappingJackson2MessageConverter.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2013 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.web.messaging.converter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Type; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Implementation of {@link MessageConverter} that can read and write JSON using Jackson 2.x's {@link ObjectMapper}. + *

+ * This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap + * HashMap} instances. + *

+ * By default, this converter supports {@code application/json}. This can be overridden by + * setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. + *

+ * Tested against Jackson 2.2; compatible with Jackson 2.0 and higher. + * + * @author Rossen Stoyanchev + * @author Arjen Poutsma + * @since 4.0 + */ +public class MappingJackson2MessageConverter extends AbstractMessageConverter { + + private ObjectMapper objectMapper = new ObjectMapper(); + + private boolean prefixJson = false; + + private Boolean prettyPrint; + + + /** + * Construct a new {@code MappingJackson2HttpMessageConverter}. + */ + public MappingJackson2MessageConverter() { + super(new MediaType("application", "json"), new MediaType("application", "*+json")); + } + + + /** + * Set the {@code ObjectMapper} for this view. If not set, a default + * {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. + *

+ * Setting a custom-configured {@code ObjectMapper} is one way to take further control + * of the JSON serialization process. For example, an extended + * {@link org.codehaus.jackson.map.SerializerFactory} can be configured that provides + * custom serializers for specific types. The other option for refining the + * serialization process is to use Jackson's provided annotations on the types to be + * serialized, in which case a custom-configured ObjectMapper is unnecessary. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + /** + * Return the underlying {@code ObjectMapper} for this view. + */ + public ObjectMapper getObjectMapper() { + return this.objectMapper; + } + + /** + * Indicate whether the JSON output by this view should be prefixed with "{} &&". + * Default is false. + *

+ * Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. + * The prefix renders the string syntactically invalid as a script so that it cannot + * be hijacked. This prefix does not affect the evaluation of JSON, but if JSON + * validation is performed on the string, the prefix would need to be ignored. + */ + public void setPrefixJson(boolean prefixJson) { + this.prefixJson = prefixJson; + } + + /** + * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *

+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+	 * converter.setObjectMapper(mapper);
+	 * 
+ */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); + } + } + + + @Override + public boolean canConvertFromPayload(Class clazz, MediaType mediaType) { + JavaType javaType = getJavaType(clazz, null); + return (this.objectMapper.canDeserialize(javaType) && canConvertFrom(mediaType)); + } + + @Override + public boolean canConvertToPayload(Class clazz, MediaType mediaType) { + return (this.objectMapper.canSerialize(clazz) && canConvertTo(mediaType)); + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canRead/Write instead + throw new UnsupportedOperationException(); + } + + @Override + protected Object convertFromPayloadInternal(Class clazz, + MediaType contentType, byte[] payload) throws IOException { + + JavaType javaType = getJavaType(clazz, null); + return readJavaType(javaType, payload); + } + + private Object readJavaType(JavaType javaType, byte[] payload) { + try { + return this.objectMapper.readValue(payload, javaType); + } + catch (IOException ex) { + throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); + } + } + + @Override + protected byte[] convertToPayloadInternal(Object object, MediaType contentType) throws IOException { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // The following has been deprecated as late as Jackson 2.2 (April 2013); + // preserved for the time being, for Jackson 2.0/2.1 compatibility. + @SuppressWarnings("deprecation") + JsonGenerator jsonGenerator = + this.objectMapper.getJsonFactory().createJsonGenerator(out, JsonEncoding.UTF8); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + jsonGenerator.useDefaultPrettyPrinter(); + } + + try { + if (this.prefixJson) { + jsonGenerator.writeRaw("{} && "); + } + this.objectMapper.writeValue(jsonGenerator, object); + } + catch (JsonProcessingException ex) { + // TODO: more specific exception + throw new IllegalStateException("Could not write JSON: " + ex.getMessage(), ex); + } + + return out.toByteArray(); + } + + /** + * Return the Jackson {@link JavaType} for the specified type and context class. + *

The default implementation returns {@code typeFactory.constructType(type, contextClass)}, + * but this can be overridden in subclasses, to allow for custom generic collection handling. + * For instance: + *

+	 * protected JavaType getJavaType(Type type) {
+	 *   if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
+	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
+	 *   } else {
+	 *     return super.getJavaType(type);
+	 *   }
+	 * }
+	 * 
+ * @param type the type to return the java type for + * @param contextClass a context class for the target type, for example a class + * in which the target type appears in a method signature, can be {@code null} + * signature, can be {@code null} + * @return the java type + */ + protected JavaType getJavaType(Type type, Class contextClass) { + return this.objectMapper.getTypeFactory().constructType(type, contextClass); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/converter/MessageConverter.java b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/MessageConverter.java new file mode 100644 index 0000000000..da218c1e3a --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/MessageConverter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2010 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.web.messaging.converter; + +import java.io.IOException; +import java.util.List; + +import org.springframework.http.MediaType; + +/** + * Strategy for converting byte a array message payload to and from a typed object. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface MessageConverter { + + + /** + * Whether the given class can be converted from a byte array by this converter. + * + * @param clazz the class to convert from + * @param mediaType the media type of the content, can be {@code null} if not + * specified. Typically the value of a {@code Content-Type} header. + * @return {@code true} if it can be converted; {@code false} otherwise + */ + boolean canConvertFromPayload(Class clazz, MediaType mediaType); + + /** + * Convert the payload of the given type. + * + * @param clazz the type of object to return. This type must have previously been + * passed to {@link #canConvertFromPayload(Class, MediaType)} and it must have + * returned {@code true}. + * @param contentType the content type of the payload, can be {@code null} + * @param payload the payload to convert from + * @return the converted object + * @throws IOException in case of I/O errors + */ + T convertFromPayload(Class clazz, MediaType contentType, byte[] payload) throws IOException; + + /** + * Whether the given class can be converted to a byte array by this converter. + * + * @param clazz the class to test + * @param mediaType the media type of the content, can be {@code null} if not specified. + * @return {@code true} if writable; {@code false} otherwise + */ + boolean canConvertToPayload(Class clazz, MediaType mediaType); + + /** + * Convert the given object to a byte array. + * + * @param t the object to convert. The type of this object must have previously been + * passed to {@link #canConvertToPayload(Class, MediaType)} and it must have returned + * {@code true}. + * @param headers + * @return the output message + * @throws IOException in case of I/O errors + */ + byte[] convertToPayload(T t, MediaType contentType) throws IOException; + + /** + * Return the list of {@link MediaType} objects supported by this converter. + * + * @return the list of supported media types + */ + List getSupportedMediaTypes(); + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/converter/StringMessageConverter.java b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/StringMessageConverter.java new file mode 100644 index 0000000000..e9cb940308 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/converter/StringMessageConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2013 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.web.messaging.converter; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.springframework.http.MediaType; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class StringMessageConverter extends AbstractMessageConverter { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + + public StringMessageConverter() { + super(MediaType.TEXT_PLAIN); + } + + @Override + protected boolean supports(Class clazz) { + return String.class.equals(clazz); + } + + @Override + protected String convertFromPayloadInternal(Class clazz, MediaType contentType, + byte[] payload) throws IOException { + + return new String(payload, UTF_8); + } + + @Override + protected byte[] convertToPayloadInternal(String content, MediaType contentType) throws IOException { + return content.getBytes(UTF_8); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompCommand.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompCommand.java index 83edcb9011..c6db230c60 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompCommand.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompCommand.java @@ -16,6 +16,8 @@ package org.springframework.web.messaging.stomp; +import org.springframework.web.messaging.MessageType; + /** * @@ -41,6 +43,25 @@ public enum StompCommand { CONNECTED, MESSAGE, RECEIPT, - ERROR + ERROR; + + + public MessageType getMessageType() { + if (this == CONNECT) { + return MessageType.CONNECT; + } + else if (this == SUBSCRIBE) { + return MessageType.SUBSCRIBE; + } + else if (this == UNSUBSCRIBE) { + return MessageType.UNSUBSCRIBE; + } + else if (this == SEND) { + return MessageType.SEND; + } + else { + return MessageType.OTHER; + } + } } diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompHeaders.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompHeaders.java index 798418ecad..99a0210d39 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompHeaders.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompHeaders.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -66,6 +67,10 @@ public class StompHeaders implements MultiValueMap, Serializable private static final String DESTINATION = "destination"; + private static final String CONTENT_TYPE = "content-type"; + + private static final String CONTENT_LENGTH = "content-length"; + private static final String HEARTBEAT = "heart-beat"; @@ -129,6 +134,29 @@ public class StompHeaders implements MultiValueMap, Serializable set(DESTINATION, destination); } + public MediaType getContentType() { + String contentType = getFirst(CONTENT_TYPE); + return StringUtils.hasText(contentType) ? MediaType.valueOf(contentType) : null; + } + + public void setContentType(MediaType mediaType) { + if (mediaType != null) { + set(CONTENT_TYPE, mediaType.toString()); + } + else { + remove(CONTENT_TYPE); + } + } + + public Integer getContentLength() { + String contentLength = getFirst(CONTENT_LENGTH); + return StringUtils.hasText(contentLength) ? new Integer(contentLength) : null; + } + + public void setContentLength(int contentLength) { + set(CONTENT_LENGTH, String.valueOf(contentLength)); + } + public long[] getHeartbeat() { String rawValue = getFirst(HEARTBEAT); if (!StringUtils.hasText(rawValue)) { diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompMessage.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompMessage.java index 6c8da5e3e7..78bbe43efd 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompMessage.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/StompMessage.java @@ -66,7 +66,7 @@ public class StompMessage { this.sessionId = sessionId; } - public String getStompSessionId() { + public String getSessionId() { return this.sessionId; } diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AbstractStompService.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AbstractStompService.java index 126c52e1da..426e90228f 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AbstractStompService.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AbstractStompService.java @@ -16,9 +16,15 @@ package org.springframework.web.messaging.stomp.service; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; import org.springframework.web.messaging.stomp.StompCommand; import org.springframework.web.messaging.stomp.StompMessage; @@ -39,6 +45,12 @@ public abstract class AbstractStompService { private final Reactor reactor; + private final List allowedDestinations = new ArrayList(); + + private final List disallowedDestinations = new ArrayList(); + + private final PathMatcher pathMatcher = new AntPathMatcher(); + public AbstractStompService(Reactor reactor) { @@ -54,13 +66,19 @@ public abstract class AbstractStompService { this.reactor.on(Fn.$(StompCommand.SUBSCRIBE), new Consumer>() { @Override public void accept(Event event) { - processSubscribe(event.getData(), event.getReplyTo()); + StompMessage message = event.getData(); + if (isAllowedDestination(message)) { + processSubscribe(event.getData(), event.getReplyTo()); + } } }); this.reactor.on(Fn.$(StompCommand.SEND), new Consumer>() { @Override public void accept(Event event) { - processSend(event.getData()); + StompMessage message = event.getData(); + if (isAllowedDestination(message)) { + processSend(event.getData()); + } } }); this.reactor.on(Fn.$(StompCommand.DISCONNECT), new Consumer>() { @@ -108,10 +126,49 @@ public abstract class AbstractStompService { } + /** + * Ant-style destination patterns that this STOMP service is allowed to process. + */ + public void setAllowedDestinations(String... patterns) { + this.allowedDestinations.clear(); + this.allowedDestinations.addAll(Arrays.asList(patterns)); + } + + /** + * Ant-style destination patterns that this STOMP service should skip. + */ + public void setDisallowedDestinations(String... patterns) { + this.disallowedDestinations.clear(); + this.disallowedDestinations.addAll(Arrays.asList(patterns)); + } + public Reactor getReactor() { return this.reactor; } + private boolean isAllowedDestination(StompMessage message) { + String destination = message.getHeaders().getDestination(); + if (destination == null) { + return true; + } + if (!this.disallowedDestinations.isEmpty()) { + for (String pattern : this.disallowedDestinations) { + if (this.pathMatcher.match(pattern, destination)) { + return false; + } + } + } + if (!this.allowedDestinations.isEmpty()) { + for (String pattern : this.allowedDestinations) { + if (this.pathMatcher.match(pattern, destination)) { + return true; + } + } + return false; + } + return true; + } + protected void processConnect(StompMessage message, Object replyTo) { } diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AnnotationStompService.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AnnotationStompService.java new file mode 100644 index 0000000000..fe880b2e64 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/AnnotationStompService.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.messaging.MessageMapping; +import org.springframework.web.messaging.MessageType; +import org.springframework.web.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.web.messaging.converter.MessageConverter; +import org.springframework.web.messaging.converter.StringMessageConverter; +import org.springframework.web.messaging.stomp.StompMessage; +import org.springframework.web.messaging.stomp.service.support.MessageBodyArgumentResolver; +import org.springframework.web.messaging.stomp.service.support.MessageBrokerArgumentResolver; +import org.springframework.web.messaging.stomp.service.support.SubscriptionArgumentResolver; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.HandlerMethodSelector; + +import reactor.core.Reactor; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class AnnotationStompService extends AbstractStompService + implements ApplicationContextAware, InitializingBean { + + private List> messageConverters; + + private ApplicationContext applicationContext; + + private Map handlerMethods = new HashMap(); + + private MessageMethodArgumentResolverComposite argumentResolvers = new MessageMethodArgumentResolverComposite(); + + + public AnnotationStompService(Reactor reactor) { + super(reactor); + } + + public void setMessageConverters(List> messageConverters) { + this.messageConverters = messageConverters; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() { + + initHandlerMethods(); + + if (this.messageConverters == null) { + this.messageConverters = new ArrayList>(); + this.messageConverters.add(new StringMessageConverter()); + this.messageConverters.add(new MappingJackson2MessageConverter()); + } + this.argumentResolvers.addResolver(new SubscriptionArgumentResolver(getReactor(), this.messageConverters)); + this.argumentResolvers.addResolver(new MessageBrokerArgumentResolver(getReactor(), this.messageConverters)); + this.argumentResolvers.addResolver(new MessageBodyArgumentResolver(this.messageConverters)); + } + + protected void initHandlerMethods() { + String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class); + for (String beanName : beanNames) { + if (isHandler(this.applicationContext.getType(beanName))){ + detectHandlerMethods(beanName); + } + } + } + + protected boolean isHandler(Class beanType) { + return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) || + (AnnotationUtils.findAnnotation(beanType, MessageMapping.class) != null)); + } + + protected void detectHandlerMethods(final Object handler) { + + Class handlerType = (handler instanceof String) ? + this.applicationContext.getType((String) handler) : handler.getClass(); + + final Class userType = ClassUtils.getUserClass(handlerType); + + Set methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() { + @Override + public boolean matches(Method method) { + return AnnotationUtils.findAnnotation(method, MessageMapping.class) != null; + } + }); + + for (Method method : methods) { + MessageMapping mapping = AnnotationUtils.findAnnotation(method, MessageMapping.class); + HandlerMethod handlerMethod = createHandlerMethod(handler, method); + this.handlerMethods.put(mapping, handlerMethod); + } + } + + protected HandlerMethod createHandlerMethod(Object handler, Method method) { + HandlerMethod handlerMethod; + if (handler instanceof String) { + String beanName = (String) handler; + handlerMethod = new HandlerMethod(beanName, this.applicationContext, method); + } + else { + handlerMethod = new HandlerMethod(handler, method); + } + return handlerMethod; + } + + protected HandlerMethod getHandlerMethod(String destination, MessageType messageType) { + for (MessageMapping mapping : this.handlerMethods.keySet()) { + boolean match = false; + for (String mappingDestination : mapping.value()) { + if (destination.equals(mappingDestination)) { + match = true; + break; + } + } + if (match && messageType.equals(mapping.messageType())) { + return this.handlerMethods.get(mapping); + } + } + return null; + } + + @Override + protected void processSubscribe(StompMessage message, Object replyTo) { + handleMessage(message, replyTo, MessageType.SUBSCRIBE); + } + + @Override + protected void processSend(StompMessage message) { + handleMessage(message, null, MessageType.SEND); + } + + private void handleMessage(final StompMessage message, final Object replyTo, MessageType messageType) { + + String destination = message.getHeaders().getDestination(); + + HandlerMethod match = getHandlerMethod(destination, messageType); + if (match == null) { + return; + } + + HandlerMethod handlerMethod = match.createWithResolvedBean(); + InvocableMessageHandlerMethod messageHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod); + messageHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); + + try { + messageHandlerMethod.invoke(message, replyTo); + } + catch (Throwable e) { + // TODO: send error message, or add @ExceptionHandler-like capability + e.printStackTrace(); + } + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/InvocableMessageHandlerMethod.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/InvocableMessageHandlerMethod.java new file mode 100644 index 0000000000..0c648eb35c --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/InvocableMessageHandlerMethod.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2012 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.web.messaging.stomp.service; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.messaging.stomp.StompMessage; +import org.springframework.web.method.HandlerMethod; + +/** + * Invokes the handler method for a given message after resolving + * its method argument values through registered {@link MessageMethodArgumentResolver}s. + *

+ * Argument resolution often requires a {@link WebDataBinder} for data binding or for type + * conversion. Use the {@link #setDataBinderFactory(WebDataBinderFactory)} property to + * supply a binder factory to pass to argument resolvers. + *

+ * Use {@link #setMessageMethodArgumentResolvers(MessageMethodArgumentResolverComposite)} + * to customize the list of argument resolvers. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class InvocableMessageHandlerMethod extends HandlerMethod { + + private MessageMethodArgumentResolverComposite argumentResolvers = new MessageMethodArgumentResolverComposite(); + + private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + + + /** + * Create an instance from a {@code HandlerMethod}. + */ + public InvocableMessageHandlerMethod(HandlerMethod handlerMethod) { + super(handlerMethod); + } + + /** + * Constructs a new handler method with the given bean instance, method name and + * parameters. + * + * @param bean the object bean + * @param methodName the method name + * @param parameterTypes the method parameter types + * @throws NoSuchMethodException when the method cannot be found + */ + public InvocableMessageHandlerMethod( + Object bean, String methodName, Class... parameterTypes) throws NoSuchMethodException { + super(bean, methodName, parameterTypes); + } + + /** + * Set {@link MessageMethodArgumentResolver}s to use to use for resolving method + * argument values. + */ + public void setMessageMethodArgumentResolvers(MessageMethodArgumentResolverComposite argumentResolvers) { + this.argumentResolvers = argumentResolvers; + } + + /** + * Set the ParameterNameDiscoverer for resolving parameter names when needed (e.g. + * default request attribute name). + *

+ * Default is an + * {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer} + * instance. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * TODO + * + * @exception Exception raised if no suitable argument resolver can be found, or the + * method raised an exception + */ + public final Object invoke(StompMessage message, Object replyTo) throws Exception { + + Object[] args = getMethodArgumentValues(message, replyTo); + + if (logger.isTraceEnabled()) { + StringBuilder builder = new StringBuilder("Invoking ["); + builder.append(this.getMethod().getName()).append("] method with arguments "); + builder.append(Arrays.asList(args)); + logger.trace(builder.toString()); + } + + Object returnValue = invoke(args); + + if (logger.isTraceEnabled()) { + logger.trace("Method [" + this.getMethod().getName() + "] returned [" + returnValue + "]"); + } + + return returnValue; + } + + /** + * Get the method argument values for the current request. + */ + private Object[] getMethodArgumentValues(StompMessage message, Object replyTo) throws Exception { + + MethodParameter[] parameters = getMethodParameters(); + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(parameterNameDiscoverer); + GenericTypeResolver.resolveParameterType(parameter, getBean().getClass()); + + args[i] = resolveProvidedArgument(parameter); + if (args[i] != null) { + continue; + } + + if (this.argumentResolvers.supportsParameter(parameter)) { + try { + args[i] = this.argumentResolvers.resolveArgument(parameter, message, replyTo); + continue; + } catch (Exception ex) { + if (logger.isTraceEnabled()) { + logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex); + } + throw ex; + } + } + + if (args[i] == null) { + String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i); + throw new IllegalStateException(msg); + } + } + return args; + } + + private String getArgumentResolutionErrorMessage(String message, int index) { + MethodParameter param = getMethodParameters()[index]; + message += " [" + index + "] [type=" + param.getParameterType().getName() + "]"; + return getDetailedErrorMessage(message); + } + + /** + * Adds HandlerMethod details such as the controller type and method signature to the given error message. + * @param message error message to append the HandlerMethod details to + */ + protected String getDetailedErrorMessage(String message) { + StringBuilder sb = new StringBuilder(message).append("\n"); + sb.append("HandlerMethod details: \n"); + sb.append("Controller [").append(getBeanType().getName()).append("]\n"); + sb.append("Method [").append(getBridgedMethod().toGenericString()).append("]\n"); + return sb.toString(); + } + + /** + * Attempt to resolve a method parameter from the list of provided argument values. + */ + private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) { + if (providedArgs == null) { + return null; + } + for (Object providedArg : providedArgs) { + if (parameter.getParameterType().isInstance(providedArg)) { + return providedArg; + } + } + return null; + } + + /** + * Invoke the handler method with the given argument values. + */ + private Object invoke(Object... args) throws Exception { + ReflectionUtils.makeAccessible(this.getBridgedMethod()); + try { + return getBridgedMethod().invoke(getBean(), args); + } + catch (IllegalArgumentException e) { + String msg = getInvocationErrorMessage(e.getMessage(), args); + throw new IllegalArgumentException(msg, e); + } + catch (InvocationTargetException e) { + // Unwrap for HandlerExceptionResolvers ... + Throwable targetException = e.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else if (targetException instanceof Error) { + throw (Error) targetException; + } + else if (targetException instanceof Exception) { + throw (Exception) targetException; + } + else { + String msg = getInvocationErrorMessage("Failed to invoke controller method", args); + throw new IllegalStateException(msg, targetException); + } + } + } + + private String getInvocationErrorMessage(String message, Object[] resolvedArgs) { + StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message)); + sb.append("Resolved arguments: \n"); + for (int i=0; i < resolvedArgs.length; i++) { + sb.append("[").append(i).append("] "); + if (resolvedArgs[i] == null) { + sb.append("[null] \n"); + } + else { + sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); + sb.append("[value=").append(resolvedArgs[i]).append("]\n"); + } + } + return sb.toString(); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/MessageMethodArgumentResolver.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/MessageMethodArgumentResolver.java new file mode 100644 index 0000000000..362f4818e2 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/MessageMethodArgumentResolver.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service; + +import org.springframework.core.MethodParameter; +import org.springframework.web.messaging.stomp.StompMessage; + + +/** + * Strategy interface for resolving method parameters into argument values in + * the context of a given message. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface MessageMethodArgumentResolver { + + /** + * Whether the given {@linkplain MethodParameter method parameter} is + * supported by this resolver. + * + * @param parameter the method parameter to check + * @return {@code true} if this resolver supports the supplied parameter; + * {@code false} otherwise + */ + boolean supportsParameter(MethodParameter parameter); + + + /** + * Resolves a method parameter into an argument value from a given message. + * + * @param parameter the method parameter to resolve. This parameter must + * have previously been passed to + * {@link #supportsParameter(org.springframework.core.MethodParameter)} + * and it must have returned {@code true} + * @param message + * @param replyTo + * + * @return the resolved argument value, or {@code null}. + * + * @throws Exception in case of errors with the preparation of argument values + */ + Object resolveArgument(MethodParameter parameter, StompMessage message, Object replyTo) throws Exception; + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/MessageMethodArgumentResolverComposite.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/MessageMethodArgumentResolverComposite.java new file mode 100644 index 0000000000..6fcfd24c53 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/MessageMethodArgumentResolverComposite.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2012 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.web.messaging.stomp.service; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.web.messaging.stomp.StompMessage; + +/** + * Resolves method parameters by delegating to a list of registered + * {@link MessageMethodArgumentResolver}. Previously resolved method parameters are cached + * for faster lookups. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class MessageMethodArgumentResolverComposite implements MessageMethodArgumentResolver { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final List argumentResolvers = + new LinkedList(); + + private final Map argumentResolverCache = + new ConcurrentHashMap(256); + + + /** + * Return a read-only list with the contained resolvers, or an empty list. + */ + public List getResolvers() { + return Collections.unmodifiableList(this.argumentResolvers); + } + + /** + * Whether the given {@linkplain MethodParameter method parameter} is supported by any registered + * {@link MessageMethodArgumentResolver}. + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return getArgumentResolver(parameter) != null; + } + + /** + * Iterate over registered {@link MessageMethodArgumentResolver}s and invoke the one that supports it. + * @exception IllegalStateException if no suitable {@link MessageMethodArgumentResolver} is found. + */ + @Override + public Object resolveArgument(MethodParameter parameter, StompMessage message, Object replyTo) throws Exception { + + MessageMethodArgumentResolver resolver = getArgumentResolver(parameter); + Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]"); + return resolver.resolveArgument(parameter, message, replyTo); + } + + /** + * Find a registered {@link MessageMethodArgumentResolver} that supports the given method parameter. + */ + private MessageMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + MessageMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + for (MessageMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) { + if (logger.isTraceEnabled()) { + logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" + + parameter.getGenericParameterType() + "]"); + } + if (methodArgumentResolver.supportsParameter(parameter)) { + result = methodArgumentResolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; + } + + /** + * Add the given {@link MessageMethodArgumentResolver}. + */ + public MessageMethodArgumentResolverComposite addResolver(MessageMethodArgumentResolver argumentResolver) { + this.argumentResolvers.add(argumentResolver); + return this; + } + + /** + * Add the given {@link MessageMethodArgumentResolver}s. + */ + public MessageMethodArgumentResolverComposite addResolvers( + List argumentResolvers) { + if (argumentResolvers != null) { + for (MessageMethodArgumentResolver resolver : argumentResolvers) { + this.argumentResolvers.add(resolver); + } + } + return this; + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/RelayStompService.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/RelayStompService.java index 1c0f6f4138..7ebcbf12b9 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/RelayStompService.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/RelayStompService.java @@ -48,7 +48,7 @@ public class RelayStompService extends AbstractStompService { private Map relaySessions = new ConcurrentHashMap(); - private final StompMessageConverter converter = new StompMessageConverter(); + private final StompMessageConverter messageConverter = new StompMessageConverter(); private final TaskExecutor taskExecutor; @@ -58,9 +58,10 @@ public class RelayStompService extends AbstractStompService { this.taskExecutor = executor; // For now, a naive way to manage socket reading } + protected void processConnect(StompMessage stompMessage, final Object replyTo) { - final String stompSessionId = stompMessage.getStompSessionId(); + final String stompSessionId = stompMessage.getSessionId(); final RelaySession session = new RelaySession(); this.relaySessions.put(stompSessionId, session); @@ -80,19 +81,19 @@ public class RelayStompService extends AbstractStompService { } private void relayStompMessage(StompMessage stompMessage) { - RelaySession session = RelayStompService.this.relaySessions.get(stompMessage.getStompSessionId()); + RelaySession session = RelayStompService.this.relaySessions.get(stompMessage.getSessionId()); Assert.notNull(session, "RelaySession not found"); try { if (logger.isTraceEnabled()) { logger.trace("Forwarding: " + stompMessage); } - byte[] bytes = converter.fromStompMessage(stompMessage); + byte[] bytes = messageConverter.fromStompMessage(stompMessage); session.getOutputStream().write(bytes); session.getOutputStream().flush(); } catch (Exception e) { e.printStackTrace(); - clearRelaySession(stompMessage.getStompSessionId()); + clearRelaySession(stompMessage.getSessionId()); } } @@ -209,7 +210,7 @@ public class RelayStompService extends AbstractStompService { } else if (b == 0x00) { byte[] bytes = out.toByteArray(); - StompMessage message = RelayStompService.this.converter.toStompMessage(bytes); + StompMessage message = RelayStompService.this.messageConverter.toStompMessage(bytes); getReactor().notify(replyTo, Event.wrap(message)); out.reset(); } diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/SimpleStompService.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/SimpleStompService.java index 423071e715..eef0b0152c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/SimpleStompService.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/SimpleStompService.java @@ -29,7 +29,7 @@ import reactor.Fn; import reactor.core.Reactor; import reactor.fn.Consumer; import reactor.fn.Event; -import reactor.fn.Registration; +import reactor.fn.registry.Registration; /** @@ -64,7 +64,7 @@ public class SimpleStompService extends AbstractStompService { getReactor().notify(replyTo, Event.wrap(outMessage)); } }); - addSubscription(message.getStompSessionId(), registration); + addSubscription(message.getSessionId(), registration); } private void addSubscription(String sessionId, Registration registration) { @@ -85,7 +85,7 @@ public class SimpleStompService extends AbstractStompService { @Override protected void processDisconnect(StompMessage message) { - removeSubscriptions(message.getStompSessionId()); + removeSubscriptions(message.getSessionId()); } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/AbstractPayloadSendingArgumentResolver.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/AbstractPayloadSendingArgumentResolver.java new file mode 100644 index 0000000000..1e84aff40c --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/AbstractPayloadSendingArgumentResolver.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service.support; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.web.messaging.converter.ContentTypeNotSupportedException; +import org.springframework.web.messaging.converter.MessageConverter; +import org.springframework.web.messaging.stomp.service.MessageMethodArgumentResolver; + +import reactor.core.Reactor; +import reactor.util.Assert; + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public abstract class AbstractPayloadSendingArgumentResolver implements MessageMethodArgumentResolver { + + private final Reactor reactor; + + private final List> converters; + + + public AbstractPayloadSendingArgumentResolver(Reactor reactor, List> converters) { + Assert.notNull(reactor, "reactor is required"); + this.reactor = reactor; + this.converters = (converters != null) ? converters : new ArrayList>(); + } + + public Reactor getReactor() { + return this.reactor; + } + + public List> getMessageConverters() { + return this.converters; + } + + @SuppressWarnings("unchecked") + protected byte[] convertToPayload(Object content, MediaType contentType) + throws IOException, ContentTypeNotSupportedException { + + if (content == null) { + return null; + } + + Class clazz = content.getClass(); + if (byte[].class.equals(clazz)) { + return (byte[]) content; + } + else if (!CollectionUtils.isEmpty(this.converters)) { + for (MessageConverter converter : getMessageConverters()) { + if (converter.canConvertToPayload(clazz, contentType)) { + return converter.convertToPayload(content, contentType); + + } + } + } + throw new ContentTypeNotSupportedException(contentType, clazz); + } + +} \ No newline at end of file diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/MessageBodyArgumentResolver.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/MessageBodyArgumentResolver.java new file mode 100644 index 0000000000..740b7c1f7c --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/MessageBodyArgumentResolver.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.web.messaging.MessageBody; +import org.springframework.web.messaging.converter.ContentTypeNotSupportedException; +import org.springframework.web.messaging.converter.MessageConverter; +import org.springframework.web.messaging.stomp.StompMessage; +import org.springframework.web.messaging.stomp.service.MessageMethodArgumentResolver; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class MessageBodyArgumentResolver implements MessageMethodArgumentResolver { + + private final List> converters; + + + public MessageBodyArgumentResolver(List> converters) { + this.converters = (converters != null) ? converters : new ArrayList>(); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return true; + } + + @SuppressWarnings("unchecked") + @Override + public Object resolveArgument(MethodParameter parameter, StompMessage message, Object replyTo) + throws Exception { + + byte[] payload = message.getPayload(); + + Class parameterType = parameter.getParameterType(); + if (byte[].class.isAssignableFrom(parameterType)) { + return payload; + } + + Object arg = null; + + MessageBody annot = parameter.getParameterAnnotation(MessageBody.class); + MediaType contentType = message.getHeaders().getContentType(); + + if (annot == null || annot.required()) { + for (MessageConverter converter : this.converters) { + if (converter.canConvertFromPayload(parameterType, contentType)) { + return converter.convertFromPayload(parameterType, contentType, payload); + } + } + throw new ContentTypeNotSupportedException(contentType, parameterType); + } + + return arg; + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/MessageBrokerArgumentResolver.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/MessageBrokerArgumentResolver.java new file mode 100644 index 0000000000..78ac995344 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/MessageBrokerArgumentResolver.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service.support; + +import java.io.IOException; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.web.messaging.MessageBroker; +import org.springframework.web.messaging.converter.ContentTypeNotSupportedException; +import org.springframework.web.messaging.converter.MessageConverter; +import org.springframework.web.messaging.stomp.StompCommand; +import org.springframework.web.messaging.stomp.StompHeaders; +import org.springframework.web.messaging.stomp.StompMessage; + +import reactor.core.Reactor; +import reactor.fn.Event; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class MessageBrokerArgumentResolver extends AbstractPayloadSendingArgumentResolver { + + + public MessageBrokerArgumentResolver(Reactor reactor, List> converters) { + super(reactor, converters); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return MessageBroker.class.equals(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, StompMessage message, Object replyTo) throws Exception { + return new DefaultMessageBroker(message.getSessionId()); + } + + + private class DefaultMessageBroker implements MessageBroker { + + private final String sessionId; + + + public DefaultMessageBroker(String sessionId) { + this.sessionId = sessionId; + } + + @Override + public void send(String destination, Object content, MediaType contentType) + throws IOException, ContentTypeNotSupportedException { + + StompHeaders headers = new StompHeaders(); + headers.setDestination(destination); + headers.setContentType(contentType); + + byte[] payload = convertToPayload(content, contentType); + if (payload != null) { + // TODO: set content-length + } + + StompMessage message = new StompMessage(StompCommand.SEND, headers, payload); + message.setSessionId(this.sessionId); + + getReactor().notify(StompCommand.SEND, Event.wrap(message)); + } + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/SubscriptionArgumentResolver.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/SubscriptionArgumentResolver.java new file mode 100644 index 0000000000..76c52212f9 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/service/support/SubscriptionArgumentResolver.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service.support; + +import java.io.IOException; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.messaging.Subscription; +import org.springframework.web.messaging.converter.ContentTypeNotSupportedException; +import org.springframework.web.messaging.converter.MessageConverter; +import org.springframework.web.messaging.stomp.StompCommand; +import org.springframework.web.messaging.stomp.StompHeaders; +import org.springframework.web.messaging.stomp.StompMessage; + +import reactor.core.Reactor; +import reactor.fn.Event; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class SubscriptionArgumentResolver extends AbstractPayloadSendingArgumentResolver { + + + public SubscriptionArgumentResolver(Reactor reactor, List> converters) { + super(reactor, converters); + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return Subscription.class.equals(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, StompMessage message, Object replyTo) throws Exception { + + Assert.isTrue(StompCommand.SUBSCRIBE.equals(message.getCommand()), "Not a subscribe command"); + + return new DefaultSubscription(message.getHeaders().getDestination(), replyTo); + } + + + private class DefaultSubscription implements Subscription { + + private final String destination; + + private final Object replyTo; + + + public DefaultSubscription(String destination, Object replyTo) { + this.destination = destination; + this.replyTo = replyTo; + } + + @Override + public void reply(Object content) throws IOException, ContentTypeNotSupportedException { + reply(content, null); + } + + @Override + public void reply(Object content, MediaType contentType) + throws IOException, ContentTypeNotSupportedException { + + StompHeaders headers = new StompHeaders(); + headers.setDestination(this.destination); + headers.setContentType(contentType); + + byte[] payload = convertToPayload(content, contentType); + if (payload != null) { + // TODO: set content-length + } + + StompMessage message = new StompMessage(StompCommand.MESSAGE, headers, payload); + + getReactor().notify(this.replyTo, Event.wrap(message)); + } + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/DefaultStompWebSocketHandler.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/DefaultStompWebSocketHandler.java index cfe85ce318..e03bf57371 100644 --- a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/DefaultStompWebSocketHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/DefaultStompWebSocketHandler.java @@ -35,7 +35,7 @@ import reactor.Fn; import reactor.core.Reactor; import reactor.fn.Consumer; import reactor.fn.Event; -import reactor.fn.Registration; +import reactor.fn.registry.Registration; /** * @author Gary Russell @@ -62,19 +62,19 @@ public class DefaultStompWebSocketHandler extends AbstractStompWebSocketHandler StompCommand command = message.getCommand(); if (StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command)) { registerConnectionClosedCallback(session); - connect(session, message); + handleConnect(session, message); } else if (StompCommand.SUBSCRIBE.equals(command)) { - subscribe(session, message); + handleSubscribe(session, message); } else if (StompCommand.UNSUBSCRIBE.equals(command)) { - unsubscribe(session, message); + handleUnsubscribe(session, message); } else if (StompCommand.SEND.equals(command)) { - send(session, message); + handleSend(session, message); } else if (StompCommand.DISCONNECT.equals(command)) { - disconnect(session, message); + handleDisconnect(session, message); } else if (StompCommand.ACK.equals(command) || StompCommand.NACK.equals(command)) { this.reactor.notify(command, Event.wrap(message)); @@ -121,7 +121,7 @@ public class DefaultStompWebSocketHandler extends AbstractStompWebSocketHandler } } - protected void connect(final StompSession session, StompMessage stompMessage) throws IOException { + protected void handleConnect(final StompSession session, StompMessage stompMessage) throws IOException { StompHeaders headers = new StompHeaders(); Set acceptVersions = stompMessage.getHeaders().getAcceptVersion(); @@ -171,7 +171,7 @@ public class DefaultStompWebSocketHandler extends AbstractStompWebSocketHandler this.reactor.notify(StompCommand.CONNECT, Event.wrap(stompMessage, replyToKey)); } - protected void subscribe(final StompSession session, StompMessage message) { + protected void handleSubscribe(final StompSession session, StompMessage message) { final String subscriptionId = message.getHeaders().getId(); String replyToKey = getSubscriptionReplyKey(session, subscriptionId); @@ -218,7 +218,7 @@ public class DefaultStompWebSocketHandler extends AbstractStompWebSocketHandler list.add(registration); } - protected void unsubscribe(StompSession session, StompMessage message) { + protected void handleUnsubscribe(StompSession session, StompMessage message) { cancelRegistration(session, message.getHeaders().getId()); this.reactor.notify(StompCommand.UNSUBSCRIBE, Event.wrap(message)); } @@ -237,11 +237,11 @@ public class DefaultStompWebSocketHandler extends AbstractStompWebSocketHandler } } - protected void send(StompSession session, StompMessage stompMessage) { + protected void handleSend(StompSession session, StompMessage stompMessage) { this.reactor.notify(StompCommand.SEND, Event.wrap(stompMessage)); } - protected void disconnect(StompSession session, StompMessage stompMessage) { + protected void handleDisconnect(StompSession session, StompMessage stompMessage) { removeSubscriptions(session); this.reactor.notify(StompCommand.DISCONNECT, Event.wrap(stompMessage)); } diff --git a/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/StompMessageInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/StompMessageInterceptor.java new file mode 100644 index 0000000000..c72407f10d --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/messaging/stomp/socket/StompMessageInterceptor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.socket; + +import org.springframework.web.messaging.stomp.StompMessage; + + +/** + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface StompMessageInterceptor { + + boolean handleConnect(StompMessage message); + + boolean handleSubscribe(StompMessage message); + + boolean handleUnsubscribe(StompMessage message); + + StompMessage handleSend(StompMessage message); + + void handleDisconnect(); + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/messaging/stomp/service/AnnotationStompServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/messaging/stomp/service/AnnotationStompServiceTests.java new file mode 100644 index 0000000000..f6bb2b7d04 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/messaging/stomp/service/AnnotationStompServiceTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 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.web.messaging.stomp.service; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.stereotype.Controller; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.messaging.MessageMapping; +import org.springframework.web.messaging.MessageType; +import org.springframework.web.method.HandlerMethod; + +import reactor.core.Environment; +import reactor.core.Reactor; + +import static org.junit.Assert.*; + + +/** + * Test fixture for {@link AnnotationStompService}. + * @author Rossen Stoyanchev + */ +public class AnnotationStompServiceTests { + + + private AnnotationStompService service; + + + @Before + public void setup() { + StaticWebApplicationContext wac = new StaticWebApplicationContext(); + wac.registerSingleton("controller", TestController.class); + + Reactor reactor = new Reactor(new Environment()); + this.service = new AnnotationStompService(reactor); + this.service.setApplicationContext(wac); + this.service.afterPropertiesSet(); + } + + @Test + public void noMatch() { + assertNull(this.service.getHandlerMethod("/nomatch", MessageType.CONNECT)); + } + + @Test + public void match() { + HandlerMethod handlerMethod = this.service.getHandlerMethod("/foo", MessageType.SUBSCRIBE); + + assertNotNull(handlerMethod); + assertEquals("handleSubscribe", handlerMethod.getMethod().getName()); + assertEquals("controller", handlerMethod.getBean()); + } + + + @Controller + private static class TestController { + + @MessageMapping(value="/foo", messageType=MessageType.SUBSCRIBE) + public void handleSubscribe() { + } + + } + +}