From bf4563e204f1695fa8d4d356432bdd6860b1efac Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 28 Jun 2013 13:14:23 +0200 Subject: [PATCH 1/6] Include target types in MethodReference cache Update the cached MethodExecutor in MethodReference to include the target type. Prevents the incorrect use of the cache when the SpEL expression refers to a different target object. Issue: SPR-10657 --- .../expression/spel/ast/MethodReference.java | 39 +++++++++++-------- .../spel/CachedMethodExecutorTests.java | 22 +++++++++-- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 1e88cebc96..595bddad9d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -48,14 +48,12 @@ public class MethodReference extends SpelNodeImpl { private volatile CachedMethodExecutor cachedExecutor; - public MethodReference(boolean nullSafe, String methodName, int pos, SpelNodeImpl... arguments) { super(pos, arguments); this.name = methodName; this.nullSafe = nullSafe; } - public final String getName() { return this.name; } @@ -102,6 +100,9 @@ public class MethodReference extends SpelNodeImpl { state.popActiveContextObject(); } } + TypedValue activeContextObject = state.getActiveContextObject(); + TypeDescriptor target = (activeContextObject == null ? null + : activeContextObject.getTypeDescriptor()); List argumentTypes = getTypes(arguments); if (currentContext.getValue() == null) { if (this.nullSafe) { @@ -113,7 +114,7 @@ public class MethodReference extends SpelNodeImpl { } } - MethodExecutor executorToUse = getCachedExecutor(argumentTypes); + MethodExecutor executorToUse = getCachedExecutor(target, argumentTypes); if (executorToUse != null) { try { return executorToUse.execute(state.getEvaluationContext(), @@ -139,7 +140,7 @@ public class MethodReference extends SpelNodeImpl { // either there was no accessor or it no longer existed executorToUse = findAccessorForMethod(this.name, argumentTypes, state); - this.cachedExecutor = new CachedMethodExecutor(executorToUse, argumentTypes); + this.cachedExecutor = new CachedMethodExecutor(executorToUse, target, argumentTypes); try { return executorToUse.execute(state.getEvaluationContext(), state.getActiveContextObject().getValue(), arguments); @@ -227,15 +228,15 @@ public class MethodReference extends SpelNodeImpl { : contextObject.getClass())); } - private MethodExecutor getCachedExecutor(List argumentTypes) { - if (this.cachedExecutor == null || !this.cachedExecutor.isSuitable(argumentTypes)) { + private MethodExecutor getCachedExecutor(TypeDescriptor target, + List argumentTypes) { + if (this.cachedExecutor == null || !this.cachedExecutor.isSuitable(target, argumentTypes)) { this.cachedExecutor = null; return null; } return this.cachedExecutor.get(); } - private class MethodValueRef implements ValueRef { private final ExpressionState state; @@ -244,15 +245,19 @@ public class MethodReference extends SpelNodeImpl { private final Object target; + private TypeDescriptor targetType; + private final Object[] arguments; private List argumentTypes; - MethodValueRef(ExpressionState state, EvaluationContext evaluationContext, Object object, Object[] arguments) { + MethodValueRef(ExpressionState state, EvaluationContext evaluationContext, + Object object, Object[] arguments) { this.state = state; this.evaluationContext = evaluationContext; this.target = object; + this.targetType = TypeDescriptor.valueOf(target.getClass()); this.arguments = arguments; this.argumentTypes = getTypes(this.arguments); } @@ -260,7 +265,8 @@ public class MethodReference extends SpelNodeImpl { @Override public TypedValue getValue() { - MethodExecutor executorToUse = getCachedExecutor(this.argumentTypes); + MethodExecutor executorToUse = getCachedExecutor(this.targetType, + this.argumentTypes); if (executorToUse != null) { try { return executorToUse.execute(this.evaluationContext, this.target, this.arguments); @@ -285,7 +291,7 @@ public class MethodReference extends SpelNodeImpl { // either there was no accessor or it no longer existed executorToUse = findAccessorForMethod(MethodReference.this.name, argumentTypes, this.target, this.evaluationContext); - MethodReference.this.cachedExecutor = new CachedMethodExecutor(executorToUse, this.argumentTypes); + MethodReference.this.cachedExecutor = new CachedMethodExecutor(executorToUse, this.targetType, this.argumentTypes); try { return executorToUse.execute(this.evaluationContext, this.target, this.arguments); } @@ -310,23 +316,24 @@ public class MethodReference extends SpelNodeImpl { } } - private static class CachedMethodExecutor { private final MethodExecutor methodExecutor; + private final TypeDescriptor target; + private final List argumentTypes; - - public CachedMethodExecutor(MethodExecutor methodExecutor, + public CachedMethodExecutor(MethodExecutor methodExecutor, TypeDescriptor target, List argumentTypes) { this.methodExecutor = methodExecutor; + this.target = target; this.argumentTypes = argumentTypes; } - - public boolean isSuitable(List argumentTypes) { - return (this.methodExecutor != null && this.argumentTypes.equals(argumentTypes)); + public boolean isSuitable(TypeDescriptor target, List argumentTypes) { + return (this.methodExecutor != null && this.target != null + && this.target.equals(target) && this.argumentTypes.equals(argumentTypes)); } public MethodExecutor get() { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java index 75e76a5903..b23197aa6d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java @@ -45,8 +45,8 @@ public class CachedMethodExecutorTests { @Test - public void testCachedExecution() throws Exception { - Expression expression = this.parser.parseExpression("echo(#something)"); + public void testCachedExecutionForParameters() throws Exception { + Expression expression = this.parser.parseExpression("echo(#var)"); assertMethodExecution(expression, 42, "int: 42"); assertMethodExecution(expression, 42, "int: 42"); @@ -54,18 +54,32 @@ public class CachedMethodExecutorTests { assertMethodExecution(expression, 42, "int: 42"); } + @Test + public void testCachedExecutionForTarget() throws Exception { + Expression expression = this.parser.parseExpression("#var.echo(42)"); + + assertMethodExecution(expression, new RootObject(), "int: 42"); + assertMethodExecution(expression, new RootObject(), "int: 42"); + assertMethodExecution(expression, new BaseObject(), "String: 42"); + assertMethodExecution(expression, new RootObject(), "int: 42"); + } + private void assertMethodExecution(Expression expression, Object var, String expected) { - this.context.setVariable("something", var); + this.context.setVariable("var", var); assertEquals(expected, expression.getValue(this.context)); } - public static class RootObject { + public static class BaseObject { public String echo(String value) { return "String: " + value; } + } + + public static class RootObject extends BaseObject { + public String echo(int value) { return "int: " + value; } From 3157b68e4a4b217082b9460ea653e62efcd012e4 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 28 Jun 2013 16:24:14 -0700 Subject: [PATCH 2/6] Polish MethodReference Polish MethodReference code formatting and remove duplicate code. --- .../expression/spel/ast/MethodReference.java | 318 ++++++++---------- 1 file changed, 140 insertions(+), 178 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 595bddad9d..e514529a03 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -42,122 +42,173 @@ import org.springframework.expression.spel.SpelMessage; */ public class MethodReference extends SpelNodeImpl { - private final String name; - private final boolean nullSafe; + private final String name; + private volatile CachedMethodExecutor cachedExecutor; - public MethodReference(boolean nullSafe, String methodName, int pos, SpelNodeImpl... arguments) { + + public MethodReference(boolean nullSafe, String methodName, int pos, + SpelNodeImpl... arguments) { super(pos, arguments); - this.name = methodName; this.nullSafe = nullSafe; + this.name = methodName; } + public final String getName() { return this.name; } @Override protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { - TypedValue currentContext = state.getActiveContextObject(); - Object[] arguments = new Object[getChildCount()]; - for (int i = 0; i < arguments.length; i++) { - // Make the root object the active context again for evaluating the parameter - // expressions - try { - state.pushActiveContextObject(state.getRootContextObject()); - arguments[i] = this.children[i].getValueInternal(state).getValue(); - } - finally { - state.popActiveContextObject(); - } + Object[] arguments = getArguments(state); + if (state.getActiveContextObject().getValue() == null) { + throwIfNotNullSafe(getArgumentTypes(arguments)); + return ValueRef.NullValueRef.instance; } - if (currentContext.getValue() == null) { - if (this.nullSafe) { - return ValueRef.NullValueRef.instance; - } - else { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED, - FormatHelper.formatMethodForMessage(this.name, getTypes(arguments))); - } - } - return new MethodValueRef(state,state.getEvaluationContext(),state.getActiveContextObject().getValue(),arguments); + return new MethodValueRef(state); } @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - TypedValue currentContext = state.getActiveContextObject(); - Object[] arguments = new Object[getChildCount()]; - for (int i = 0; i < arguments.length; i++) { - // Make the root object the active context again for evaluating the parameter - // expressions - try { - state.pushActiveContextObject(state.getRootContextObject()); - arguments[i] = this.children[i].getValueInternal(state).getValue(); - } - finally { - state.popActiveContextObject(); - } - } - TypedValue activeContextObject = state.getActiveContextObject(); - TypeDescriptor target = (activeContextObject == null ? null - : activeContextObject.getTypeDescriptor()); - List argumentTypes = getTypes(arguments); - if (currentContext.getValue() == null) { - if (this.nullSafe) { - return TypedValue.NULL; - } - else { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED, - FormatHelper.formatMethodForMessage(this.name, argumentTypes)); - } + EvaluationContext evaluationContext = state.getEvaluationContext(); + Object value = state.getActiveContextObject().getValue(); + TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor(); + Object[] arguments = getArguments(state); + return getValueInternal(evaluationContext, value, arguments, targetType); + } + + private TypedValue getValueInternal(EvaluationContext evaluationContext, + Object value, Object[] arguments, TypeDescriptor targetType) { + List argumentTypes = getArgumentTypes(arguments); + + if (value == null) { + throwIfNotNullSafe(argumentTypes); + return TypedValue.NULL; } - MethodExecutor executorToUse = getCachedExecutor(target, argumentTypes); + MethodExecutor executorToUse = getCachedExecutor(targetType, argumentTypes); + if (executorToUse != null) { try { - return executorToUse.execute(state.getEvaluationContext(), - state.getActiveContextObject().getValue(), arguments); + return executorToUse.execute(evaluationContext, value, arguments); } catch (AccessException ae) { // Two reasons this can occur: // 1. the method invoked actually threw a real exception - // 2. the method invoked was not passed the arguments it expected and has become 'stale' + // 2. the method invoked was not passed the arguments it expected and + // has become 'stale' - // In the first case we should not retry, in the second case we should see if there is a - // better suited method. + // In the first case we should not retry, in the second case we should see + // if there is a better suited method. - // To determine which situation it is, the AccessException will contain a cause. - // If the cause is an InvocationTargetException, a user exception was thrown inside the method. + // To determine the situation, the AccessException will contain a cause. + // If the cause is an InvocationTargetException, a user exception was + // thrown inside the method. // Otherwise the method could not be invoked. - throwSimpleExceptionIfPossible(state, ae); + throwSimpleExceptionIfPossible(value, ae); - // at this point we know it wasn't a user problem so worth a retry if a better candidate can be found + // at this point we know it wasn't a user problem so worth a retry if a + // better candidate can be found this.cachedExecutor = null; } } // either there was no accessor or it no longer existed - executorToUse = findAccessorForMethod(this.name, argumentTypes, state); - this.cachedExecutor = new CachedMethodExecutor(executorToUse, target, argumentTypes); + executorToUse = findAccessorForMethod(this.name, argumentTypes, value, evaluationContext); + this.cachedExecutor = new CachedMethodExecutor(executorToUse, targetType, + argumentTypes); try { - return executorToUse.execute(state.getEvaluationContext(), - state.getActiveContextObject().getValue(), arguments); + return executorToUse.execute(evaluationContext, + value, arguments); } - catch (AccessException ae) { + catch (AccessException ex) { // Same unwrapping exception handling as above in above catch block - throwSimpleExceptionIfPossible(state, ae); - throw new SpelEvaluationException( getStartPosition(), ae, SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, - this.name, state.getActiveContextObject().getValue().getClass().getName(), ae.getMessage()); + throwSimpleExceptionIfPossible(value, ex); + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name, + value.getClass().getName(), + ex.getMessage()); } } + private void throwIfNotNullSafe(List argumentTypes) { + if (!this.nullSafe) { + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED, + FormatHelper.formatMethodForMessage(this.name, argumentTypes)); + } + } + + private Object[] getArguments(ExpressionState state) { + Object[] arguments = new Object[getChildCount()]; + for (int i = 0; i < arguments.length; i++) { + // Make the root object the active context again for evaluating the parameter + // expressions + try { + state.pushActiveContextObject(state.getRootContextObject()); + arguments[i] = this.children[i].getValueInternal(state).getValue(); + } + finally { + state.popActiveContextObject(); + } + } + return arguments; + } + + private List getArgumentTypes(Object... arguments) { + List descriptors = new ArrayList(arguments.length); + for (Object argument : arguments) { + descriptors.add(TypeDescriptor.forObject(argument)); + } + return Collections.unmodifiableList(descriptors); + } + + private MethodExecutor getCachedExecutor(TypeDescriptor target, + List argumentTypes) { + if (this.cachedExecutor != null && this.cachedExecutor.isSuitable(target, argumentTypes)) { + return this.cachedExecutor.get(); + } + this.cachedExecutor = null; + return null; + } + + private MethodExecutor findAccessorForMethod(String name, + List argumentTypes, Object contextObject, + EvaluationContext evaluationContext) throws SpelEvaluationException { + + List methodResolvers = evaluationContext.getMethodResolvers(); + if (methodResolvers != null) { + for (MethodResolver methodResolver : methodResolvers) { + try { + MethodExecutor methodExecutor = methodResolver.resolve( + evaluationContext, contextObject, name, argumentTypes); + if (methodExecutor != null) { + return methodExecutor; + } + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.PROBLEM_LOCATING_METHOD, name, + contextObject.getClass()); + } + } + } + + throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_NOT_FOUND, + FormatHelper.formatMethodForMessage(name, argumentTypes), + FormatHelper.formatClassNameForMessage( + contextObject instanceof Class ? ((Class) contextObject) + : contextObject.getClass())); + } + /** - * Decode the AccessException, throwing a lightweight evaluation exception or, if the cause was a RuntimeException, - * throw the RuntimeException directly. + * Decode the AccessException, throwing a lightweight evaluation exception or, if the + * cause was a RuntimeException, throw the RuntimeException directly. */ - private void throwSimpleExceptionIfPossible(ExpressionState state, AccessException ae) { + private void throwSimpleExceptionIfPossible(Object value, AccessException ae) { if (ae.getCause() instanceof InvocationTargetException) { Throwable rootCause = ae.getCause().getCause(); if (rootCause instanceof RuntimeException) { @@ -165,19 +216,12 @@ public class MethodReference extends SpelNodeImpl { } throw new ExpressionInvocationTargetException(getStartPosition(), "A problem occurred when trying to execute method '" + this.name + - "' on object of type '" + state.getActiveContextObject().getValue().getClass().getName() + "'", + "' on object of type '" + + value.getClass().getName() + "'", rootCause); } } - private List getTypes(Object... arguments) { - List descriptors = new ArrayList(arguments.length); - for (Object argument : arguments) { - descriptors.add(TypeDescriptor.forObject(argument)); - } - return Collections.unmodifiableList(descriptors); - } - @Override public String toStringAST() { StringBuilder sb = new StringBuilder(); @@ -192,117 +236,31 @@ public class MethodReference extends SpelNodeImpl { return sb.toString(); } - private MethodExecutor findAccessorForMethod(String name, - List argumentTypes, ExpressionState state) - throws SpelEvaluationException { - return findAccessorForMethod(name, argumentTypes, - state.getActiveContextObject().getValue(), state.getEvaluationContext()); - } - - private MethodExecutor findAccessorForMethod(String name, - List argumentTypes, Object contextObject, EvaluationContext eContext) - throws SpelEvaluationException { - - List methodResolvers = eContext.getMethodResolvers(); - if (methodResolvers != null) { - for (MethodResolver methodResolver : methodResolvers) { - try { - MethodExecutor methodExecutor = methodResolver.resolve(eContext, - contextObject, name, argumentTypes); - if (methodExecutor != null) { - return methodExecutor; - } - } - catch (AccessException ex) { - throw new SpelEvaluationException(getStartPosition(), ex, - SpelMessage.PROBLEM_LOCATING_METHOD, name, - contextObject.getClass()); - } - } - } - throw new SpelEvaluationException( - getStartPosition(), - SpelMessage.METHOD_NOT_FOUND, - FormatHelper.formatMethodForMessage(name, argumentTypes), - FormatHelper.formatClassNameForMessage(contextObject instanceof Class ? ((Class) contextObject) - : contextObject.getClass())); - } - - private MethodExecutor getCachedExecutor(TypeDescriptor target, - List argumentTypes) { - if (this.cachedExecutor == null || !this.cachedExecutor.isSuitable(target, argumentTypes)) { - this.cachedExecutor = null; - return null; - } - return this.cachedExecutor.get(); - } private class MethodValueRef implements ValueRef { - private final ExpressionState state; - private final EvaluationContext evaluationContext; + private EvaluationContext evaluationContext; - private final Object target; + private Object value; private TypeDescriptor targetType; - private final Object[] arguments; - - private List argumentTypes; + private Object[] arguments; - MethodValueRef(ExpressionState state, EvaluationContext evaluationContext, - Object object, Object[] arguments) { - this.state = state; - this.evaluationContext = evaluationContext; - this.target = object; - this.targetType = TypeDescriptor.valueOf(target.getClass()); - this.arguments = arguments; - this.argumentTypes = getTypes(this.arguments); + MethodValueRef(ExpressionState state) { + this.evaluationContext = state.getEvaluationContext(); + this.value = state.getActiveContextObject().getValue(); + this.targetType = state.getActiveContextObject().getTypeDescriptor(); + this.arguments = getArguments(state); } @Override public TypedValue getValue() { - MethodExecutor executorToUse = getCachedExecutor(this.targetType, - this.argumentTypes); - if (executorToUse != null) { - try { - return executorToUse.execute(this.evaluationContext, this.target, this.arguments); - } - catch (AccessException ae) { - // Two reasons this can occur: - // 1. the method invoked actually threw a real exception - // 2. the method invoked was not passed the arguments it expected and has become 'stale' - - // In the first case we should not retry, in the second case we should see if there is a - // better suited method. - - // To determine which situation it is, the AccessException will contain a cause. - // If the cause is an InvocationTargetException, a user exception was thrown inside the method. - // Otherwise the method could not be invoked. - throwSimpleExceptionIfPossible(this.state, ae); - - // at this point we know it wasn't a user problem so worth a retry if a better candidate can be found - MethodReference.this.cachedExecutor = null; - } - } - - // either there was no accessor or it no longer existed - executorToUse = findAccessorForMethod(MethodReference.this.name, argumentTypes, this.target, this.evaluationContext); - MethodReference.this.cachedExecutor = new CachedMethodExecutor(executorToUse, this.targetType, this.argumentTypes); - try { - return executorToUse.execute(this.evaluationContext, this.target, this.arguments); - } - catch (AccessException ex) { - // Same unwrapping exception handling as above in above catch block - throwSimpleExceptionIfPossible(this.state, ex); - throw new SpelEvaluationException(getStartPosition(), ex, - SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, - MethodReference.this.name, this.state.getActiveContextObject().getValue().getClass().getName(), - ex.getMessage()); - } + return MethodReference.this.getValueInternal(this.evaluationContext, + this.value, this.arguments, this.targetType); } @Override @@ -316,6 +274,7 @@ public class MethodReference extends SpelNodeImpl { } } + private static class CachedMethodExecutor { private final MethodExecutor methodExecutor; @@ -324,6 +283,7 @@ public class MethodReference extends SpelNodeImpl { private final List argumentTypes; + public CachedMethodExecutor(MethodExecutor methodExecutor, TypeDescriptor target, List argumentTypes) { this.methodExecutor = methodExecutor; @@ -331,7 +291,9 @@ public class MethodReference extends SpelNodeImpl { this.argumentTypes = argumentTypes; } - public boolean isSuitable(TypeDescriptor target, List argumentTypes) { + + public boolean isSuitable(TypeDescriptor target, + List argumentTypes) { return (this.methodExecutor != null && this.target != null && this.target.equals(target) && this.argumentTypes.equals(argumentTypes)); } From 638ba3fa9792fe2d31b87c0ae2e2f79664bb665c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 1 Jul 2013 07:08:17 -0700 Subject: [PATCH 3/6] Revert inadvertent change to DefaultDocumentLoader Revert changes to DefaultDocumentLoader accidentally committed in 60532cbd1eb4e1b5778653fec315407531e949a9. --- .../springframework/beans/factory/xml/DefaultDocumentLoader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java index 0cca9e8be0..cf15a81132 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java @@ -91,7 +91,6 @@ public class DefaultDocumentLoader implements DocumentLoader { factory.setNamespaceAware(namespaceAware); if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) { - factory.setFeature("http://apache.org/xml/features/validation/schema", false); factory.setValidating(true); if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) { // Enforce namespace aware for XSD... From 9dba73dfc90978a98e6d8d214fec107ff71018f4 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 1 Jul 2013 13:07:51 -0700 Subject: [PATCH 4/6] Add ConversionService support for ByteBuffers Add ByteBufferConverter that is registered by default with the DefaultConversionService. Allows conversion from/to a ByteBuffer and byte[] or to any type that can be converted via a byte[]. Issue: SPR-10712 --- .../convert/support/ByteBufferConverter.java | 114 ++++++++++++++++++ .../support/DefaultConversionService.java | 6 + .../support/ByteBufferConverterTests.java | 109 +++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java create mode 100644 spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java new file mode 100644 index 0000000000..d58a8f9bc4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java @@ -0,0 +1,114 @@ +/* + * 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.core.convert.support; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a {@link ByteBuffer} directly to and from {@code byte[]}s and indirectly to + * any type that the {@link ConversionService} support via {@code byte[]}. + * + * @author Phillip Webb + */ +public class ByteBufferConverter implements ConditionalGenericConverter { + + private static final TypeDescriptor BYTE_BUFFER_TYPE = TypeDescriptor.valueOf(ByteBuffer.class); + + private static final TypeDescriptor BYTE_ARRAY_TYPE = TypeDescriptor.valueOf(byte[].class); + + private static final Set CONVERTIBLE_PAIRS; + static { + Set convertiblePairs = new HashSet(); + convertiblePairs.add(new ConvertiblePair(ByteBuffer.class, Object.class)); + convertiblePairs.add(new ConvertiblePair(Object.class, ByteBuffer.class)); + CONVERTIBLE_PAIRS = Collections.unmodifiableSet(convertiblePairs); + } + + + private ConversionService conversionService; + + + public ByteBufferConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (sourceType.isAssignableTo(BYTE_BUFFER_TYPE)) { + return matchesFromByteBuffer(targetType); + } + if (targetType.isAssignableTo(BYTE_BUFFER_TYPE)) { + return matchesToByteBuffer(sourceType); + } + return false; + } + + private boolean matchesFromByteBuffer(TypeDescriptor targetType) { + return (targetType.isAssignableTo(BYTE_ARRAY_TYPE) || this.conversionService.canConvert( + BYTE_ARRAY_TYPE, targetType)); + } + + private boolean matchesToByteBuffer(TypeDescriptor sourceType) { + return (sourceType.isAssignableTo(BYTE_ARRAY_TYPE) || this.conversionService.canConvert( + sourceType, BYTE_ARRAY_TYPE)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (sourceType.isAssignableTo(BYTE_BUFFER_TYPE)) { + return convertFromByteBuffer((ByteBuffer) source, targetType); + } + if (targetType.isAssignableTo(BYTE_BUFFER_TYPE)) { + return convertToByteBuffer(source, sourceType); + } + // Should not happen + throw new IllegalStateException("Unexpected source/target types"); + } + + private Object convertFromByteBuffer(ByteBuffer source, TypeDescriptor targetType) { + byte[] bytes = new byte[source.remaining()]; + source.get(bytes); + if (targetType.isAssignableTo(BYTE_ARRAY_TYPE)) { + return bytes; + } + return this.conversionService.convert(bytes, BYTE_ARRAY_TYPE, targetType); + } + + private Object convertToByteBuffer(Object source, TypeDescriptor sourceType) { + byte[] bytes = (byte[]) (source instanceof byte[] ? source + : this.conversionService.convert(source, sourceType, BYTE_ARRAY_TYPE)); + ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); + byteBuffer.put(bytes); + byteBuffer.rewind(); + return byteBuffer; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java index 86e580983b..8ef74e18fc 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -53,6 +53,7 @@ public class DefaultConversionService extends GenericConversionService { public static void addDefaultConverters(ConverterRegistry converterRegistry) { addScalarConverters(converterRegistry); addCollectionConverters(converterRegistry); + addBinaryConverters(converterRegistry); addFallbackConverters(converterRegistry); } @@ -109,6 +110,11 @@ public class DefaultConversionService extends GenericConversionService { converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService)); } + private static void addBinaryConverters(ConverterRegistry converterRegistry) { + ConversionService conversionService = (ConversionService) converterRegistry; + converterRegistry.addConverter(new ByteBufferConverter(conversionService)); + } + private static void addFallbackConverters(ConverterRegistry converterRegistry) { ConversionService conversionService = (ConversionService) converterRegistry; converterRegistry.addConverter(new ObjectToObjectConverter()); diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java new file mode 100644 index 0000000000..87b3db4dcc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java @@ -0,0 +1,109 @@ +/* + * 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.core.convert.support; + +import java.nio.ByteBuffer; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.convert.converter.Converter; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests for {@link ByteBufferConverter}. + * + * @author Phillip Webb + */ +public class ByteBufferConverterTests { + + private GenericConversionService conversionService; + + @Before + public void setup() { + this.conversionService = new GenericConversionService(); + this.conversionService.addConverter(new ByteBufferConverter(conversionService)); + this.conversionService.addConverter(new ByteArrayToOtherTypeConverter()); + this.conversionService.addConverter(new OtherTypeToByteArrayConverter()); + } + + @Test + public void byteArrayToByteBuffer() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer convert = this.conversionService.convert(bytes, ByteBuffer.class); + assertThat(bytes, not(sameInstance(convert.array()))); + assertThat(bytes, equalTo(convert.array())); + } + + @Test + public void byteBufferToByteArray() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + byte[] convert = this.conversionService.convert(byteBuffer, byte[].class); + assertThat(bytes, not(sameInstance(convert))); + assertThat(bytes, equalTo(convert)); + } + + @Test + public void byteBufferToOtherType() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + OtherType convert = this.conversionService.convert(byteBuffer, OtherType.class); + assertThat(bytes, not(sameInstance(convert.bytes))); + assertThat(bytes, equalTo(convert.bytes)); + } + + @Test + public void otherTypeToByteBuffer() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + OtherType otherType = new OtherType(bytes); + ByteBuffer convert = this.conversionService.convert(otherType, ByteBuffer.class); + assertThat(bytes, not(sameInstance(convert.array()))); + assertThat(bytes, equalTo(convert.array())); + } + + private static class OtherType { + + private byte[] bytes; + + public OtherType(byte[] bytes) { + this.bytes = bytes; + } + + } + + private static class ByteArrayToOtherTypeConverter implements + Converter { + + @Override + public OtherType convert(byte[] source) { + return new OtherType(source); + } + } + + private static class OtherTypeToByteArrayConverter implements + Converter { + + @Override + public byte[] convert(OtherType source) { + return source.bytes; + } + + } + +} From 8d6fd1ed636ce1a1ad135dfdbe6b3c9cb7d07e11 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 1 Jul 2013 13:11:20 -0700 Subject: [PATCH 5/6] ConversionService JSR-356 Encoder/Decoder adapters Develop support class allowing JSR-356 Encoder and Decoder interfaces to delegate to Spring's ConversionService. Issue: SPR-10694 --- .../ConvertingEncoderDecoderSupport.java | 247 ++++++++++++++ .../web/socket/ContextLoaderTestUtils.java | 58 ++++ .../ConvertingEncoderDecoderSupportTests.java | 317 ++++++++++++++++++ 3 files changed, 622 insertions(+) create mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java new file mode 100644 index 0000000000..5158eeac7e --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupport.java @@ -0,0 +1,247 @@ +/* + * 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.socket.adapter; + +import java.nio.ByteBuffer; + +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EncodeException; +import javax.websocket.Encoder; +import javax.websocket.EndpointConfig; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.Assert; +import org.springframework.web.context.ContextLoader; + +/** + * Base class that can be used to implement a standard {@link javax.websocket.Encoder} + * and/or {@link javax.websocket.Decoder}. It provides encode and decode method + * implementations that delegate to a Spring {@link ConversionService}. + * + *

By default, this class looks up a {@link ConversionService} registered in the + * {@link #getApplicationContext() active ApplicationContext} under + * the name {@code 'webSocketConversionService'}. This works fine for both client + * and server endpoints, in a Servlet container environment. If not running in a + * Servlet container, subclasses will need to override the + * {@link #getConversionService()} method to provide an alternative lookup strategy. + * + *

Subclasses can extend this class and should also implement one or + * both of {@link javax.websocket.Encoder} and {@link javax.websocket.Decoder}. + * For convenience {@link ConvertingEncoderDecoderSupport.BinaryEncoder}, + * {@link ConvertingEncoderDecoderSupport.BinaryDecoder}, + * {@link ConvertingEncoderDecoderSupport.TextEncoder} and + * {@link ConvertingEncoderDecoderSupport.TextDecoder} subclasses are provided. + * + *

Since JSR-356 only allows Encoder/Decoder to be registered by type, instances + * of this class are therefore managed by the WebSocket runtime, and do not need to + * be registered as Spring Beans. They can, however, by injected with Spring-managed + * dependencies via {@link Autowired @Autowire}. + * + *

Converters to convert between the {@link #getType() type} and {@code String} or + * {@code ByteBuffer} should be registered. + * + * @author Phillip Webb + * @since 4.0 + * + * @param The type being converted to (for Encoder) or from (for Decoder) + * @param The WebSocket message type ({@link String} or {@link ByteBuffer}) + * + * @see ConvertingEncoderDecoderSupport.BinaryEncoder + * @see ConvertingEncoderDecoderSupport.BinaryDecoder + * @see ConvertingEncoderDecoderSupport.TextEncoder + * @see ConvertingEncoderDecoderSupport.TextDecoder + */ +public abstract class ConvertingEncoderDecoderSupport { + + private static final String CONVERSION_SERVICE_BEAN_NAME = "webSocketConversionService"; + + + /** + * @see javax.websocket.Encoder#init(EndpointConfig) + * @see javax.websocket.Decoder#init(EndpointConfig) + */ + public void init(EndpointConfig config) { + ApplicationContext applicationContext = getApplicationContext(); + if (applicationContext != null && applicationContext instanceof ConfigurableApplicationContext) { + ConfigurableListableBeanFactory beanFactory = + ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); + beanFactory.autowireBean(this); + } + } + + /** + * @see javax.websocket.Encoder#destroy() + * @see javax.websocket.Decoder#destroy() + */ + public void destroy() { + } + + /** + * Strategy method used to obtain the {@link ConversionService}. By default this + * method expects a bean named {@code 'webSocketConversionService'} in the + * {@link #getApplicationContext() active ApplicationContext}. + * @return the {@link ConversionService} (never null) + */ + protected ConversionService getConversionService() { + ApplicationContext applicationContext = getApplicationContext(); + Assert.state(applicationContext != null, + "Unable to locate the Spring ApplicationContext"); + try { + return applicationContext.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); + } + catch (BeansException ex) { + throw new IllegalStateException( + "Unable to find ConversionService, please configure a '" + + CONVERSION_SERVICE_BEAN_NAME + "' or override getConversionService()", ex); + } + } + + /** + * Returns the active {@link ApplicationContext}. Be default this method obtains + * the context via {@link ContextLoader#getCurrentWebApplicationContext()}, which + * finds the ApplicationContext loaded via {@link ContextLoader} typically in a + * Servlet container environment. When not running in a Servlet container and + * not using {@link ContextLoader}, this method should be overridden. + * @return the {@link ApplicationContext} or {@code null} + */ + protected ApplicationContext getApplicationContext() { + return ContextLoader.getCurrentWebApplicationContext(); + } + + /** + * Returns the type being converted. By default the type is resolved using + * the generic arguments of the class. + */ + protected TypeDescriptor getType() { + return TypeDescriptor.valueOf(resolveTypeArguments()[0]); + } + + /** + * Returns the websocket message type. By default the type is resolved using + * the generic arguments of the class. + */ + protected TypeDescriptor getMessageType() { + return TypeDescriptor.valueOf(resolveTypeArguments()[1]); + } + + private Class[] resolveTypeArguments() { + return GenericTypeResolver.resolveTypeArguments(getClass(), + ConvertingEncoderDecoderSupport.class); + } + + /** + * @see javax.websocket.Encoder.Text#encode(Object) + * @see javax.websocket.Encoder.Binary#encode(Object) + */ + @SuppressWarnings("unchecked") + public M encode(T object) throws EncodeException { + try { + return (M) getConversionService().convert(object, getType(), getMessageType()); + } + catch (ConversionException ex) { + throw new EncodeException(object, "Unable to encode websocket message using ConversionService", ex); + } + } + + /** + * @see javax.websocket.Decoder.Text#willDecode(String) + * @see javax.websocket.Decoder.Binary#willDecode(ByteBuffer) + */ + public boolean willDecode(M bytes) { + return getConversionService().canConvert(getType(), getMessageType()); + } + + /** + * @see javax.websocket.Decoder.Text#decode(String) + * @see javax.websocket.Decoder.Binary#decode(ByteBuffer) + */ + @SuppressWarnings("unchecked") + public T decode(M message) throws DecodeException { + try { + return (T) getConversionService().convert(message, getMessageType(), getType()); + } + catch (ConversionException ex) { + if (message instanceof String) { + throw new DecodeException((String) message, "Unable to decode " + + "websocket message using ConversionService", ex); + } + if (message instanceof ByteBuffer) { + throw new DecodeException((ByteBuffer) message, "Unable to decode " + + "websocket message using ConversionService", ex); + } + throw ex; + } + } + + + /** + * A Binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that + * delegates to Spring's conversion service. See + * {@link ConvertingEncoderDecoderSupport} for details. + * + * @param The type that this Encoder can convert to. + */ + public static abstract class BinaryEncoder extends + ConvertingEncoderDecoderSupport implements Encoder.Binary { + } + + + /** + * A Binary {@link javax.websocket.Encoder.Binary javax.websocket.Encoder} that delegates + * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for + * details. + * + * @param The type that this Decoder can convert from. + */ + public static abstract class BinaryDecoder extends + ConvertingEncoderDecoderSupport implements Decoder.Binary { + } + + + /** + * A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates + * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for + * details. + * + * @param The type that this Encoder can convert to. + */ + public static abstract class TextEncoder extends + ConvertingEncoderDecoderSupport implements Encoder.Text { + } + + + /** + * A Text {@link javax.websocket.Encoder.Text javax.websocket.Encoder} that delegates + * to Spring's conversion service. See {@link ConvertingEncoderDecoderSupport} for + * details. + * + * @param The type that this Decoder can convert from. + */ + public static abstract class TextDecoder extends + ConvertingEncoderDecoderSupport implements Decoder.Text { + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java b/spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java new file mode 100644 index 0000000000..d0024dbbb1 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/ContextLoaderTestUtils.java @@ -0,0 +1,58 @@ +/* + * 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.socket; + +import java.lang.reflect.Field; +import java.util.Map; + +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.WebApplicationContext; + +/** + * General test utilities for manipulating the {@link ContextLoader}. + * + * @author Phillip Webb + */ +public class ContextLoaderTestUtils { + + private static Map currentContextPerThread = getCurrentContextPerThreadFromContextLoader(); + + public static void setCurrentWebApplicationContext(WebApplicationContext applicationContext) { + setCurrentWebApplicationContext(Thread.currentThread().getContextClassLoader(), applicationContext); + } + + public static void setCurrentWebApplicationContext(ClassLoader classLoader, WebApplicationContext applicationContext) { + if(applicationContext != null) { + currentContextPerThread.put(classLoader, applicationContext); + } else { + currentContextPerThread.remove(classLoader); + } + } + + @SuppressWarnings("unchecked") + private static Map getCurrentContextPerThreadFromContextLoader() { + try { + Field field = ContextLoader.class.getDeclaredField("currentContextPerThread"); + field.setAccessible(true); + return (Map) field.get(null); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java new file mode 100644 index 0000000000..1ed49a81e7 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/ConvertingEncoderDecoderSupportTests.java @@ -0,0 +1,317 @@ +/* + * 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.socket.adapter; + +import java.nio.ByteBuffer; + +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EncodeException; +import javax.websocket.Encoder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.ByteBufferConverter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.socket.ContextLoaderTestUtils; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Test for {@link ConvertingEncoderDecoderSupport}. + * + * @author Phillip Webb + */ +public class ConvertingEncoderDecoderSupportTests { + + private static final String CONVERTED_TEXT = "_test"; + + private static final ByteBuffer CONVERTED_BYTES = ByteBuffer.wrap("~test".getBytes()); + + + @Rule + public ExpectedException thown = ExpectedException.none(); + + private WebApplicationContext applicationContext; + + private MyType myType = new MyType("test"); + + + @Before + public void setup() { + setup(Config.class); + } + + @After + public void teardown() { + ContextLoaderTestUtils.setCurrentWebApplicationContext(null); + } + + private void setup(Class configurationClass) { + AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); + applicationContext.register(configurationClass); + applicationContext.refresh(); + this.applicationContext = applicationContext; + ContextLoaderTestUtils.setCurrentWebApplicationContext(this.applicationContext); + } + + @Test + public void encodeToText() throws Exception { + assertThat(new MyTextEncoder().encode(myType), equalTo(CONVERTED_TEXT)); + } + + @Test + public void encodeToTextCannotConvert() throws Exception { + setup(NoConvertersConfig.class); + thown.expect(EncodeException.class); + thown.expectCause(isA(ConverterNotFoundException.class)); + new MyTextEncoder().encode(myType); + } + + @Test + public void encodeToBinary() throws Exception { + assertThat(new MyBinaryEncoder().encode(myType).array(), + equalTo(CONVERTED_BYTES.array())); + } + + @Test + public void encodeToBinaryCannotConvert() throws Exception { + setup(NoConvertersConfig.class); + thown.expect(EncodeException.class); + thown.expectCause(isA(ConverterNotFoundException.class)); + new MyBinaryEncoder().encode(myType); + } + + @Test + public void decodeFromText() throws Exception { + Decoder.Text decoder = new MyTextDecoder(); + assertThat(decoder.willDecode(CONVERTED_TEXT), is(true)); + assertThat(decoder.decode(CONVERTED_TEXT), equalTo(myType)); + } + + @Test + public void decodeFromTextCannotConvert() throws Exception { + setup(NoConvertersConfig.class); + Decoder.Text decoder = new MyTextDecoder(); + assertThat(decoder.willDecode(CONVERTED_TEXT), is(false)); + thown.expect(DecodeException.class); + thown.expectCause(isA(ConverterNotFoundException.class)); + decoder.decode(CONVERTED_TEXT); + } + + @Test + public void decodeFromBinary() throws Exception { + Decoder.Binary decoder = new MyBinaryDecoder(); + assertThat(decoder.willDecode(CONVERTED_BYTES), is(true)); + assertThat(decoder.decode(CONVERTED_BYTES), equalTo(myType)); + } + + @Test + public void decodeFromBinaryCannotConvert() throws Exception { + setup(NoConvertersConfig.class); + Decoder.Binary decoder = new MyBinaryDecoder(); + assertThat(decoder.willDecode(CONVERTED_BYTES), is(false)); + thown.expect(DecodeException.class); + thown.expectCause(isA(ConverterNotFoundException.class)); + decoder.decode(CONVERTED_BYTES); + } + + @Test + public void encodeAndDecodeText() throws Exception { + MyTextEncoderDecoder encoderDecoder = new MyTextEncoderDecoder(); + String encoded = encoderDecoder.encode(myType); + assertThat(encoderDecoder.decode(encoded), equalTo(myType)); + } + + @Test + public void encodeAndDecodeBytes() throws Exception { + MyBinaryEncoderDecoder encoderDecoder = new MyBinaryEncoderDecoder(); + ByteBuffer encoded = encoderDecoder.encode(myType); + assertThat(encoderDecoder.decode(encoded), equalTo(myType)); + } + + @Test + public void autowiresIntoEncoder() throws Exception { + WithAutowire withAutowire = new WithAutowire(); + withAutowire.init(null); + assertThat(withAutowire.config, equalTo(applicationContext.getBean(Config.class))); + } + + @Test + public void cannotFindApplicationContext() throws Exception { + ContextLoaderTestUtils.setCurrentWebApplicationContext(null); + WithAutowire encoder = new WithAutowire(); + encoder.init(null); + thown.expect(IllegalStateException.class); + thown.expectMessage("Unable to locate the Spring ApplicationContext"); + encoder.encode(myType); + } + + @Test + public void cannotFindConversionService() throws Exception { + setup(NoConfig.class); + MyBinaryEncoder encoder = new MyBinaryEncoder(); + encoder.init(null); + thown.expect(IllegalStateException.class); + thown.expectMessage("Unable to find ConversionService"); + encoder.encode(myType); + } + + @Configuration + public static class Config { + + @Bean + public ConversionService webSocketConversionService() { + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(new ByteBufferConverter(conversionService)); + conversionService.addConverter(new MyTypeToStringConverter()); + conversionService.addConverter(new MyTypeToBytesConverter()); + conversionService.addConverter(new StringToMyTypeConverter()); + conversionService.addConverter(new BytesToMyTypeConverter()); + return conversionService; + } + + } + + @Configuration + public static class NoConvertersConfig { + + @Bean + public ConversionService webSocketConversionService() { + return new GenericConversionService(); + } + + } + + + @Configuration + public static class NoConfig { + } + + + public static class MyType { + + private String value; + + public MyType(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if(obj instanceof MyType) { + return ((MyType)obj).value.equals(value); + } + return false; + } + } + + + private static class MyTypeToStringConverter implements Converter { + @Override + public String convert(MyType source) { + return "_" + source.toString(); + } + } + + + private static class MyTypeToBytesConverter implements Converter { + @Override + public byte[] convert(MyType source) { + return ("~" + source.toString()).getBytes(); + } + } + + + private static class StringToMyTypeConverter implements Converter { + @Override + public MyType convert(String source) { + return new MyType(source.substring(1)); + } + } + + + private static class BytesToMyTypeConverter implements Converter { + @Override + public MyType convert(byte[] source) { + return new MyType(new String(source).substring(1)); + } + } + + + public static class MyTextEncoder extends + ConvertingEncoderDecoderSupport.TextEncoder { + } + + + public static class MyBinaryEncoder extends + ConvertingEncoderDecoderSupport.BinaryEncoder { + } + + + public static class MyTextDecoder extends + ConvertingEncoderDecoderSupport.TextDecoder { + } + + + public static class MyBinaryDecoder extends + ConvertingEncoderDecoderSupport.BinaryDecoder { + } + + + public static class MyTextEncoderDecoder extends + ConvertingEncoderDecoderSupport implements Encoder.Text, + Decoder.Text { + } + + + public static class MyBinaryEncoderDecoder extends + ConvertingEncoderDecoderSupport implements Encoder.Binary, + Decoder.Binary { + } + + + public static class WithAutowire extends ConvertingEncoderDecoderSupport.TextDecoder { + + @Autowired + private Config config; + + } + +} From 2a559028ad91cfad0202d728726bffaef9eaa96f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 27 Jun 2013 18:00:49 -0400 Subject: [PATCH 6/6] Upgrade spring-websocket to Jetty 9.0.4 --- build.gradle | 6 +++--- .../socket/server/support/JettyRequestUpgradeStrategy.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index e570418757..c5a7937c4a 100644 --- a/build.gradle +++ b/build.gradle @@ -480,11 +480,11 @@ project("spring-websocket") { } optional("org.glassfish.tyrus:tyrus-websocket-core:1.0") optional("org.glassfish.tyrus:tyrus-container-servlet:1.0") - optional("org.eclipse.jetty:jetty-webapp:9.0.3.v20130506") { + optional("org.eclipse.jetty:jetty-webapp:9.0.4.v20130625") { exclude group: "org.eclipse.jetty.orbit", module: "javax.servlet" } - optional("org.eclipse.jetty.websocket:websocket-server:9.0.3.v20130506") - optional("org.eclipse.jetty.websocket:websocket-client:9.0.3.v20130506") + optional("org.eclipse.jetty.websocket:websocket-server:9.0.4.v20130625") + optional("org.eclipse.jetty.websocket:websocket-client:9.0.4.v20130625") optional("com.fasterxml.jackson.core:jackson-databind:2.2.0") // required for SockJS support currently } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/JettyRequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/JettyRequestUpgradeStrategy.java index 6239cc98da..63cd67a60a 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/JettyRequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/JettyRequestUpgradeStrategy.java @@ -24,8 +24,8 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.server.HandshakeRFC6455; -import org.eclipse.jetty.websocket.server.ServletWebSocketRequest; import org.eclipse.jetty.websocket.server.WebSocketServerFactory; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.WebSocketCreator; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; @@ -67,8 +67,8 @@ public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy { this.factory.setCreator(new WebSocketCreator() { @Override public Object createWebSocket(UpgradeRequest request, UpgradeResponse response) { - Assert.isInstanceOf(ServletWebSocketRequest.class, request); - return ((ServletWebSocketRequest) request).getServletAttributes().get(WEBSOCKET_LISTENER_ATTR_NAME); + Assert.isInstanceOf(ServletUpgradeRequest.class, request); + return ((ServletUpgradeRequest) request).getServletAttributes().get(WEBSOCKET_LISTENER_ATTR_NAME); } }); try {