diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/adapter/DefaultMethodEndpointAdapter.java b/core/src/main/java/org/springframework/ws/server/endpoint/adapter/DefaultMethodEndpointAdapter.java index 6584966d..3f70f1d4 100644 --- a/core/src/main/java/org/springframework/ws/server/endpoint/adapter/DefaultMethodEndpointAdapter.java +++ b/core/src/main/java/org/springframework/ws/server/endpoint/adapter/DefaultMethodEndpointAdapter.java @@ -33,6 +33,7 @@ import org.springframework.ws.server.endpoint.adapter.method.MethodArgumentResol import org.springframework.ws.server.endpoint.adapter.method.MethodReturnValueHandler; import org.springframework.ws.server.endpoint.adapter.method.SourcePayloadMethodProcessor; import org.springframework.ws.server.endpoint.adapter.method.StaxPayloadMethodArgumentResolver; +import org.springframework.ws.server.endpoint.adapter.method.XPathParamMethodArgumentResolver; import org.springframework.ws.server.endpoint.adapter.method.dom.Dom4jPayloadMethodProcessor; import org.springframework.ws.server.endpoint.adapter.method.dom.DomPayloadMethodProcessor; import org.springframework.ws.server.endpoint.adapter.method.dom.JDomPayloadMethodProcessor; @@ -114,6 +115,7 @@ public class DefaultMethodEndpointAdapter extends AbstractMethodEndpointAdapter methodArgumentResolvers.add(new DomPayloadMethodProcessor()); methodArgumentResolvers.add(new MessageContextMethodArgumentResolver()); methodArgumentResolvers.add(new SourcePayloadMethodProcessor()); + methodArgumentResolvers.add(new XPathParamMethodArgumentResolver()); try { Class soapMethodArgumentResolverClass = (Class) ClassUtils diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/adapter/method/XPathParamMethodArgumentResolver.java b/core/src/main/java/org/springframework/ws/server/endpoint/adapter/method/XPathParamMethodArgumentResolver.java new file mode 100644 index 00000000..fa60f9ed --- /dev/null +++ b/core/src/main/java/org/springframework/ws/server/endpoint/adapter/method/XPathParamMethodArgumentResolver.java @@ -0,0 +1,150 @@ +/* + * Copyright 2005-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.ws.server.endpoint.adapter.method; + +import javax.xml.namespace.QName; +import javax.xml.transform.Source; +import javax.xml.transform.TransformerException; +import javax.xml.transform.dom.DOMResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.server.endpoint.annotation.XPathParam; +import org.springframework.ws.server.endpoint.support.NamespaceUtils; +import org.springframework.xml.transform.TransformerObjectSupport; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Implementation of {@link MethodArgumentResolver} that supports the {@link XPathParam @XPathParam} annotation. + *

+ * This resolver supports parameters annotated with {@link XPathParam @XPathParam} that specifies the XPath expression + * that should be bound to that parameter. The parameter can either a "natively supported" XPath type ({@link Boolean + * boolean}, {@link Double double}, {@link String}, {@link Node}, or {@link NodeList}), or a type that is {@linkplain + * ConversionService#canConvert(Class, Class) supported} by the {@link ConversionService}. + * + * @author Arjen Poutsma + * @since 2.0 + */ +public class XPathParamMethodArgumentResolver extends TransformerObjectSupport implements MethodArgumentResolver { + + private final XPathFactory xpathFactory = createXPathFactory(); + + private ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + + /** + * Sets the conversion service to use. + *

+ * Defaults to the {@linkplain ConversionServiceFactory#createDefaultConversionService() default conversion + * service}. + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public boolean supportsParameter(MethodParameter parameter) { + if (parameter.getParameterAnnotation(XPathParam.class) == null) { + return false; + } + Class parameterType = parameter.getParameterType(); + if (Boolean.class.equals(parameterType) || Boolean.TYPE.equals(parameterType) || + Double.class.equals(parameterType) || Double.TYPE.equals(parameterType) || + Node.class.isAssignableFrom(parameterType) || NodeList.class.isAssignableFrom(parameterType) || + String.class.isAssignableFrom(parameterType)) { + return true; + } + else { + return conversionService.canConvert(String.class, parameterType); + } + } + + public Object resolveArgument(MessageContext messageContext, MethodParameter parameter) + throws TransformerException, XPathExpressionException { + Class parameterType = parameter.getParameterType(); + QName evaluationReturnType = getReturnType(parameterType); + boolean useConversionService = false; + if (evaluationReturnType == null) { + evaluationReturnType = XPathConstants.STRING; + useConversionService = true; + } + + XPath xpath = createXPath(); + xpath.setNamespaceContext(NamespaceUtils.getNamespaceContext(parameter.getMethod())); + + Element rootElement = getRootElement(messageContext.getRequest().getPayloadSource()); + String expression = parameter.getParameterAnnotation(XPathParam.class).value(); + Object result = xpath.evaluate(expression, rootElement, evaluationReturnType); + return useConversionService ? conversionService.convert(result, parameterType) : result; + } + + private QName getReturnType(Class parameterType) { + if (Boolean.class.equals(parameterType) || Boolean.TYPE.equals(parameterType)) { + return XPathConstants.BOOLEAN; + } + else if (Double.class.equals(parameterType) || Double.TYPE.equals(parameterType)) { + return XPathConstants.NUMBER; + } + else if (Node.class.equals(parameterType)) { + return XPathConstants.NODE; + } + else if (NodeList.class.equals(parameterType)) { + return XPathConstants.NODESET; + } + else if (String.class.equals(parameterType)) { + return XPathConstants.STRING; + } + else { + return null; + } + } + + private XPath createXPath() { + synchronized (xpathFactory) { + return xpathFactory.newXPath(); + } + } + + private Element getRootElement(Source source) throws TransformerException { + DOMResult domResult = new DOMResult(); + transform(source, domResult); + Document document = (Document) domResult.getNode(); + return document.getDocumentElement(); + } + + /** + * Create a {@code XPathFactory} that this resolver will use to create {@link XPath} objects. + *

+ * Can be overridden in subclasses, adding further initialization of the factory. The resulting factory is cached, + * so this method will only be called once. + * + * @return the created factory + */ + protected XPathFactory createXPathFactory() { + return XPathFactory.newInstance(); + } + + +} diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/annotation/Namespace.java b/core/src/main/java/org/springframework/ws/server/endpoint/annotation/Namespace.java new file mode 100644 index 00000000..375786aa --- /dev/null +++ b/core/src/main/java/org/springframework/ws/server/endpoint/annotation/Namespace.java @@ -0,0 +1,55 @@ +/* + * Copyright 2005-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.ws.server.endpoint.annotation; + +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; +import javax.xml.XMLConstants; + +/** + * Sets up a namespace to be used in an {@link Endpoint @Endpoint} method, class, or package. + *

+ * Typically used in combination with {@link XPathParam @XPathParam}, or {@link PayloadRoot @PayloadRoot}. + * + * @author Arjen Poutsma + * @see XPathParam + * @see PayloadRoot + * @since 2.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD}) +public @interface Namespace { + + /** + * Signifies the prefix of the namespace. + * + * @see #uri() + */ + String prefix() default XMLConstants.DEFAULT_NS_PREFIX; + + /** + * Signifies the URI of the namespace. + * + * @see #prefix() + */ + String uri(); + +} diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/annotation/Namespaces.java b/core/src/main/java/org/springframework/ws/server/endpoint/annotation/Namespaces.java new file mode 100644 index 00000000..9ba9d7f0 --- /dev/null +++ b/core/src/main/java/org/springframework/ws/server/endpoint/annotation/Namespaces.java @@ -0,0 +1,38 @@ +/* + * Copyright 2005-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.ws.server.endpoint.annotation; + +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; + +/** + * Sets the namespaces to be used in an {@link Endpoint @Endpoint} method, class, or package. + * + * @author Arjen Poutsma + * @see Namespace + * @since 2.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD}) +public @interface Namespaces { + + Namespace[] value(); +} diff --git a/core/src/main/java/org/springframework/ws/server/endpoint/support/NamespaceUtils.java b/core/src/main/java/org/springframework/ws/server/endpoint/support/NamespaceUtils.java new file mode 100644 index 00000000..b8c6b099 --- /dev/null +++ b/core/src/main/java/org/springframework/ws/server/endpoint/support/NamespaceUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright 2005-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.ws.server.endpoint.support; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import javax.xml.namespace.NamespaceContext; + +import org.springframework.util.Assert; +import org.springframework.ws.server.endpoint.annotation.Namespace; +import org.springframework.ws.server.endpoint.annotation.Namespaces; +import org.springframework.xml.namespace.SimpleNamespaceContext; + +/** + * Helper class for handling {@link Namespace @Namespace} annotations. + * + * @author Arjen Poutsma + * @since 2.0 + */ +public abstract class NamespaceUtils { + + private NamespaceUtils() { + } + + /** + * Creates a {@code NamespaceContext} for the specified method, based on {@link Namespaces @Namespaces} and {@link + * Namespace @Namespace} annotations. + *

+ * This method will search for {@link Namespaces @Namespaces} and {@link Namespace @Namespace} annotation in the + * given method, its class, and its package, in reverse order. That is: package-level annotations are overridden by + * class-level annotations, which again are overridden by method-level annotations. + * + * @param method the method to create the namespace context for + * @return the namespace context + */ + public static NamespaceContext getNamespaceContext(Method method) { + Assert.notNull(method, "'method' must not be null"); + SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext(); + Class endpointClass = method.getDeclaringClass(); + Package endpointPackage = endpointClass.getPackage(); + if (endpointPackage != null) { + addNamespaceAnnotations(endpointPackage, namespaceContext); + } + addNamespaceAnnotations(endpointClass, namespaceContext); + addNamespaceAnnotations(method, namespaceContext); + return namespaceContext; + } + + private static void addNamespaceAnnotations(AnnotatedElement annotatedElement, + SimpleNamespaceContext namespaceContext) { + if (annotatedElement.isAnnotationPresent(Namespaces.class)) { + Namespaces namespacesAnn = annotatedElement.getAnnotation(Namespaces.class); + for (Namespace namespaceAnn : namespacesAnn.value()) { + namespaceContext.bindNamespaceUri(namespaceAnn.prefix(), namespaceAnn.uri()); + } + } + if (annotatedElement.isAnnotationPresent(Namespace.class)) { + Namespace namespaceAnn = annotatedElement.getAnnotation(Namespace.class); + namespaceContext.bindNamespaceUri(namespaceAnn.prefix(), namespaceAnn.uri()); + } + } + +} diff --git a/core/src/test/java/org/springframework/ws/server/endpoint/adapter/method/XPathParamMethodArgumentResolverTest.java b/core/src/test/java/org/springframework/ws/server/endpoint/adapter/method/XPathParamMethodArgumentResolverTest.java new file mode 100644 index 00000000..b5562731 --- /dev/null +++ b/core/src/test/java/org/springframework/ws/server/endpoint/adapter/method/XPathParamMethodArgumentResolverTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2005-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.ws.server.endpoint.adapter.method; + +import java.lang.reflect.Method; + +import org.springframework.core.MethodParameter; +import org.springframework.ws.MockWebServiceMessage; +import org.springframework.ws.MockWebServiceMessageFactory; +import org.springframework.ws.context.DefaultMessageContext; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.server.endpoint.annotation.Namespace; +import org.springframework.ws.server.endpoint.annotation.Namespaces; +import org.springframework.ws.server.endpoint.annotation.XPathParam; + +import org.junit.Before; +import org.junit.Test; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import static org.junit.Assert.*; + +@Namespaces(@Namespace(prefix = "tns", uri = "http://springframework.org/spring-ws")) +public class XPathParamMethodArgumentResolverTest { + + private static final String CONTENTS = "text42"; + + private XPathParamMethodArgumentResolver resolver; + + private MethodParameter booleanParameter; + + private MethodParameter doubleParameter; + + private MethodParameter nodeParameter; + + private MethodParameter nodeListParameter; + + private MethodParameter stringParameter; + + private MethodParameter convertedParameter; + + private MethodParameter unsupportedParameter; + + private MethodParameter namespaceMethodParameter; + + private MethodParameter namespaceClassParameter; + + @Before + public void setUp() throws Exception { + resolver = new XPathParamMethodArgumentResolver(); + Method supportedTypes = getClass() + .getMethod("supportedTypes", Boolean.TYPE, Double.TYPE, Node.class, NodeList.class, String.class); + booleanParameter = new MethodParameter(supportedTypes, 0); + doubleParameter = new MethodParameter(supportedTypes, 1); + nodeParameter = new MethodParameter(supportedTypes, 2); + nodeListParameter = new MethodParameter(supportedTypes, 3); + stringParameter = new MethodParameter(supportedTypes, 4); + convertedParameter = new MethodParameter(getClass().getMethod("convertedType", Integer.TYPE), 0); + unsupportedParameter = new MethodParameter(getClass().getMethod("unsupported", String.class), 0); + namespaceMethodParameter = new MethodParameter(getClass().getMethod("namespacesMethod", String.class), 0); + namespaceClassParameter = new MethodParameter(getClass().getMethod("namespacesClass", String.class), 0); + } + + @Test + public void supportsParameter() { + assertTrue("resolver does not support boolean parameter", resolver.supportsParameter(booleanParameter)); + assertTrue("resolver does not support double parameter", resolver.supportsParameter(doubleParameter)); + assertTrue("resolver does not support Node parameter", resolver.supportsParameter(nodeParameter)); + assertTrue("resolver does not support NodeList parameter", resolver.supportsParameter(nodeListParameter)); + assertTrue("resolver does not support String parameter", resolver.supportsParameter(stringParameter)); + assertTrue("resolver does not support String parameter", resolver.supportsParameter(convertedParameter)); + assertFalse("resolver supports parameter without @XPathParam", resolver.supportsParameter(unsupportedParameter)); + } + + @Test + public void resolveBoolean() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage(CONTENTS); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, booleanParameter); + + assertTrue("resolver does not return boolean", result instanceof Boolean); + Boolean b = (Boolean) result; + assertTrue("Invalid boolean value", b); + } + @Test + public void resolveDouble() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage(CONTENTS); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, doubleParameter); + + assertTrue("resolver does not return double", result instanceof Double); + Double d = (Double) result; + assertEquals("Invalid double value", 42D, d, 0D); + } + + @Test + public void resolveNode() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage(CONTENTS); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, nodeParameter); + + assertTrue("resolver does not return Node", result instanceof Node); + Node node = (Node) result; + assertEquals("Invalid node value", "child", node.getLocalName()); + } + + @Test + public void resolveNodeList() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage(CONTENTS); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, nodeListParameter); + + assertTrue("resolver does not return NodeList", result instanceof NodeList); + NodeList nodeList = (NodeList) result; + assertEquals("Invalid NodeList value", 1, nodeList.getLength()); + assertEquals("Invalid Node value", "child", nodeList.item(0).getLocalName()); + } + + @Test + public void resolveString() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage(CONTENTS); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, stringParameter); + + assertTrue("resolver does not return String", result instanceof String); + String s = (String) result; + assertEquals("Invalid string value", "text", s); + } + + @Test + public void resolveConvertedType() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage(CONTENTS); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, convertedParameter); + + assertTrue("resolver does not return String", result instanceof Integer); + Integer i = (Integer) result; + assertEquals("Invalid integer value", new Integer(42), i); + } + + @Test + public void resolveNamespacesMethod() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage( + "text"); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, namespaceMethodParameter); + + assertTrue("resolver does not return String", result instanceof String); + String s = (String) result; + assertEquals("Invalid string value", "text", s); + } + + @Test + public void resolveNamespacesClass() throws Exception { + MockWebServiceMessage request = new MockWebServiceMessage( + "text"); + MessageContext messageContext = new DefaultMessageContext(request, new MockWebServiceMessageFactory()); + + Object result = resolver.resolveArgument(messageContext, namespaceClassParameter); + + assertTrue("resolver does not return String", result instanceof String); + String s = (String) result; + assertEquals("Invalid string value", "text", s); + } + + public void unsupported(String s) { + } + + public void supportedTypes(@XPathParam("/root/child")boolean param1, + @XPathParam("/root/child/number")double param2, + @XPathParam("/root/child") Node param3, + @XPathParam("/root/*") NodeList param4, + @XPathParam("/root/child/text")String param5) { + } + + public void convertedType(@XPathParam("/root/child/number")int param) { + } + + @Namespaces(@Namespace(prefix = "tns", uri = "http://springframework.org/spring-ws")) + public void namespacesMethod(@XPathParam("/tns:root")String s) { + } + + public void namespacesClass(@XPathParam("/tns:root")String s) { + } + +} diff --git a/core/src/test/java/org/springframework/ws/server/endpoint/support/NamespaceUtilsTest.java b/core/src/test/java/org/springframework/ws/server/endpoint/support/NamespaceUtilsTest.java new file mode 100644 index 00000000..862b4dd8 --- /dev/null +++ b/core/src/test/java/org/springframework/ws/server/endpoint/support/NamespaceUtilsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2005-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.ws.server.endpoint.support; + +import java.lang.reflect.Method; +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; + +import org.springframework.ws.server.endpoint.annotation.Namespace; +import org.springframework.ws.server.endpoint.annotation.Namespaces; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +@Namespaces({@Namespace(prefix = "prefix1", uri = "class1"), @Namespace(uri = "class2")}) +public class NamespaceUtilsTest { + + @Test + public void getNamespaceContextMethod() throws NoSuchMethodException { + Method method = getClass().getMethod("method"); + NamespaceContext namespaceContext = NamespaceUtils.getNamespaceContext(method); + assertEquals("method1", namespaceContext.getNamespaceURI("prefix1")); + assertEquals("method2", namespaceContext.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX)); + + } + + @Test + public void getNamespaceContextClass() throws NoSuchMethodException { + Method method = getClass().getMethod("getNamespaceContextClass"); + NamespaceContext namespaceContext = NamespaceUtils.getNamespaceContext(method); + assertEquals("class1", namespaceContext.getNamespaceURI("prefix1")); + assertEquals("class2", namespaceContext.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX)); + + } + + @Namespaces({@Namespace(prefix = "prefix1", uri = "method1"), @Namespace(uri = "method2")}) + public void method() { + + } + +} diff --git a/xml/src/main/java/org/springframework/xml/namespace/SimpleNamespaceContext.java b/xml/src/main/java/org/springframework/xml/namespace/SimpleNamespaceContext.java index 91a6a932..6e0ec943 100644 --- a/xml/src/main/java/org/springframework/xml/namespace/SimpleNamespaceContext.java +++ b/xml/src/main/java/org/springframework/xml/namespace/SimpleNamespaceContext.java @@ -16,20 +16,17 @@ package org.springframework.xml.namespace; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * Simple javax.xml.namespace.NamespaceContext implementation. Follows the standard @@ -41,9 +38,9 @@ import org.springframework.util.MultiValueMap; */ public class SimpleNamespaceContext implements NamespaceContext { - private Map prefixToNamespaceUri = new HashMap(); + private Map prefixToNamespaceUri = new LinkedHashMap(); - private MultiValueMap namespaceUriToPrefixes = new LinkedMultiValueMap(); + private Map> namespaceUriToPrefixes = new LinkedHashMap>(); public String getNamespaceURI(String prefix) { Assert.notNull(prefix, "prefix is null"); @@ -60,12 +57,14 @@ public class SimpleNamespaceContext implements NamespaceContext { } public String getPrefix(String namespaceUri) { - List prefixes = getPrefixesInternal(namespaceUri); - return prefixes.isEmpty() ? null : prefixes.get(0); + Iterator iterator = getPrefixes(namespaceUri); + return iterator.hasNext() ? iterator.next() : null; } public Iterator getPrefixes(String namespaceUri) { - return getPrefixesInternal(namespaceUri).iterator(); + Set prefixes = getPrefixesInternal(namespaceUri); + prefixes = Collections.unmodifiableSet(prefixes); + return prefixes.iterator(); } /** @@ -125,23 +124,24 @@ public class SimpleNamespaceContext implements NamespaceContext { public Iterator getBoundPrefixes() { Set prefixes = new HashSet(prefixToNamespaceUri.keySet()); prefixes.remove(XMLConstants.DEFAULT_NS_PREFIX); + prefixes = Collections.unmodifiableSet(prefixes); return prefixes.iterator(); } - private List getPrefixesInternal(String namespaceUri) { + private Set getPrefixesInternal(String namespaceUri) { if (XMLConstants.XML_NS_URI.equals(namespaceUri)) { - return Collections.singletonList(XMLConstants.XML_NS_PREFIX); + return Collections.singleton(XMLConstants.XML_NS_PREFIX); } else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceUri)) { - return Collections.singletonList(XMLConstants.XMLNS_ATTRIBUTE); + return Collections.singleton(XMLConstants.XMLNS_ATTRIBUTE); } else { - List list = namespaceUriToPrefixes.get(namespaceUri); - if (list == null) { - list = new ArrayList(); - namespaceUriToPrefixes.put(namespaceUri, list); + Set set = namespaceUriToPrefixes.get(namespaceUri); + if (set == null) { + set = new LinkedHashSet(); + namespaceUriToPrefixes.put(namespaceUri, set); } - return list; + return set; } } @@ -152,7 +152,7 @@ public class SimpleNamespaceContext implements NamespaceContext { */ public void removeBinding(String prefix) { String namespaceUri = prefixToNamespaceUri.get(prefix); - List prefixes = getPrefixesInternal(namespaceUri); + Set prefixes = getPrefixesInternal(namespaceUri); prefixes.remove(prefix); } diff --git a/xml/src/test/java/org/springframework/xml/namespace/SimpleNamespaceContextTest.java b/xml/src/test/java/org/springframework/xml/namespace/SimpleNamespaceContextTest.java index 0981b28d..521f35a1 100644 --- a/xml/src/test/java/org/springframework/xml/namespace/SimpleNamespaceContextTest.java +++ b/xml/src/test/java/org/springframework/xml/namespace/SimpleNamespaceContextTest.java @@ -72,6 +72,17 @@ public class SimpleNamespaceContextTest { assertPrefixes(XMLConstants.XML_NS_URI, XMLConstants.XML_NS_PREFIX); assertPrefixes(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLConstants.XMLNS_ATTRIBUTE); } + + @Test(expected = UnsupportedOperationException.class) + public void unmodifiableGetPrefixes() { + String namespaceUri = "namespaceUri"; + context.bindNamespaceUri("prefix1", namespaceUri); + context.bindNamespaceUri("prefix2", namespaceUri); + + Iterator prefixes = context.getPrefixes(namespaceUri); + prefixes.next(); + prefixes.remove(); + } @Test public void testMultiplePrefixes() {