001/*
002 * Copyright 2002-2013 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.remoting.jaxws;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.net.MalformedURLException;
022import java.net.URL;
023import java.util.HashMap;
024import java.util.Map;
025import javax.jws.WebService;
026import javax.xml.namespace.QName;
027import javax.xml.ws.BindingProvider;
028import javax.xml.ws.ProtocolException;
029import javax.xml.ws.Service;
030import javax.xml.ws.WebServiceException;
031import javax.xml.ws.WebServiceFeature;
032import javax.xml.ws.soap.SOAPFaultException;
033
034import org.aopalliance.intercept.MethodInterceptor;
035import org.aopalliance.intercept.MethodInvocation;
036
037import org.springframework.aop.support.AopUtils;
038import org.springframework.beans.BeanUtils;
039import org.springframework.beans.factory.BeanClassLoaderAware;
040import org.springframework.beans.factory.InitializingBean;
041import org.springframework.remoting.RemoteAccessException;
042import org.springframework.remoting.RemoteConnectFailureException;
043import org.springframework.remoting.RemoteLookupFailureException;
044import org.springframework.remoting.RemoteProxyFailureException;
045import org.springframework.util.Assert;
046import org.springframework.util.ClassUtils;
047import org.springframework.util.StringUtils;
048
049/**
050 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing a
051 * specific port of a JAX-WS service. Compatible with JAX-WS 2.1 and 2.2,
052 * as included in JDK 6 update 4+ and Java 7/8.
053 *
054 * <p>Uses either {@link LocalJaxWsServiceFactory}'s facilities underneath,
055 * or takes an explicit reference to an existing JAX-WS Service instance
056 * (e.g. obtained via {@link org.springframework.jndi.JndiObjectFactoryBean}).
057 *
058 * @author Juergen Hoeller
059 * @since 2.5
060 * @see #setPortName
061 * @see #setServiceInterface
062 * @see javax.xml.ws.Service#getPort
063 * @see org.springframework.remoting.RemoteAccessException
064 * @see org.springframework.jndi.JndiObjectFactoryBean
065 */
066public class JaxWsPortClientInterceptor extends LocalJaxWsServiceFactory
067                implements MethodInterceptor, BeanClassLoaderAware, InitializingBean {
068
069        private Service jaxWsService;
070
071        private String portName;
072
073        private String username;
074
075        private String password;
076
077        private String endpointAddress;
078
079        private boolean maintainSession;
080
081        private boolean useSoapAction;
082
083        private String soapActionUri;
084
085        private Map<String, Object> customProperties;
086
087        private WebServiceFeature[] portFeatures;
088
089        private Object[] webServiceFeatures;
090
091        private Class<?> serviceInterface;
092
093        private boolean lookupServiceOnStartup = true;
094
095        private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
096
097        private QName portQName;
098
099        private Object portStub;
100
101        private final Object preparationMonitor = new Object();
102
103
104        /**
105         * Set a reference to an existing JAX-WS Service instance,
106         * for example obtained via {@link org.springframework.jndi.JndiObjectFactoryBean}.
107         * If not set, {@link LocalJaxWsServiceFactory}'s properties have to be specified.
108         * @see #setWsdlDocumentUrl
109         * @see #setNamespaceUri
110         * @see #setServiceName
111         * @see org.springframework.jndi.JndiObjectFactoryBean
112         */
113        public void setJaxWsService(Service jaxWsService) {
114                this.jaxWsService = jaxWsService;
115        }
116
117        /**
118         * Return a reference to an existing JAX-WS Service instance, if any.
119         */
120        public Service getJaxWsService() {
121                return this.jaxWsService;
122        }
123
124        /**
125         * Set the name of the port.
126         * Corresponds to the "wsdl:port" name.
127         */
128        public void setPortName(String portName) {
129                this.portName = portName;
130        }
131
132        /**
133         * Return the name of the port.
134         */
135        public String getPortName() {
136                return this.portName;
137        }
138
139        /**
140         * Set the username to specify on the stub.
141         * @see javax.xml.ws.BindingProvider#USERNAME_PROPERTY
142         */
143        public void setUsername(String username) {
144                this.username = username;
145        }
146
147        /**
148         * Return the username to specify on the stub.
149         */
150        public String getUsername() {
151                return this.username;
152        }
153
154        /**
155         * Set the password to specify on the stub.
156         * @see javax.xml.ws.BindingProvider#PASSWORD_PROPERTY
157         */
158        public void setPassword(String password) {
159                this.password = password;
160        }
161
162        /**
163         * Return the password to specify on the stub.
164         */
165        public String getPassword() {
166                return this.password;
167        }
168
169        /**
170         * Set the endpoint address to specify on the stub.
171         * @see javax.xml.ws.BindingProvider#ENDPOINT_ADDRESS_PROPERTY
172         */
173        public void setEndpointAddress(String endpointAddress) {
174                this.endpointAddress = endpointAddress;
175        }
176
177        /**
178         * Return the endpoint address to specify on the stub.
179         */
180        public String getEndpointAddress() {
181                return this.endpointAddress;
182        }
183
184        /**
185         * Set the "session.maintain" flag to specify on the stub.
186         * @see javax.xml.ws.BindingProvider#SESSION_MAINTAIN_PROPERTY
187         */
188        public void setMaintainSession(boolean maintainSession) {
189                this.maintainSession = maintainSession;
190        }
191
192        /**
193         * Return the "session.maintain" flag to specify on the stub.
194         */
195        public boolean isMaintainSession() {
196                return this.maintainSession;
197        }
198
199        /**
200         * Set the "soapaction.use" flag to specify on the stub.
201         * @see javax.xml.ws.BindingProvider#SOAPACTION_USE_PROPERTY
202         */
203        public void setUseSoapAction(boolean useSoapAction) {
204                this.useSoapAction = useSoapAction;
205        }
206
207        /**
208         * Return the "soapaction.use" flag to specify on the stub.
209         */
210        public boolean isUseSoapAction() {
211                return this.useSoapAction;
212        }
213
214        /**
215         * Set the SOAP action URI to specify on the stub.
216         * @see javax.xml.ws.BindingProvider#SOAPACTION_URI_PROPERTY
217         */
218        public void setSoapActionUri(String soapActionUri) {
219                this.soapActionUri = soapActionUri;
220        }
221
222        /**
223         * Return the SOAP action URI to specify on the stub.
224         */
225        public String getSoapActionUri() {
226                return this.soapActionUri;
227        }
228
229        /**
230         * Set custom properties to be set on the stub.
231         * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
232         * or a "props" element in XML bean definitions.
233         * @see javax.xml.ws.BindingProvider#getRequestContext()
234         */
235        public void setCustomProperties(Map<String, Object> customProperties) {
236                this.customProperties = customProperties;
237        }
238
239        /**
240         * Allow Map access to the custom properties to be set on the stub,
241         * with the option to add or override specific entries.
242         * <p>Useful for specifying entries directly, for example via
243         * "customProperties[myKey]". This is particularly useful for
244         * adding or overriding entries in child bean definitions.
245         */
246        public Map<String, Object> getCustomProperties() {
247                if (this.customProperties == null) {
248                        this.customProperties = new HashMap<String, Object>();
249                }
250                return this.customProperties;
251        }
252
253        /**
254         * Add a custom property to this JAX-WS BindingProvider.
255         * @param name the name of the attribute to expose
256         * @param value the attribute value to expose
257         * @see javax.xml.ws.BindingProvider#getRequestContext()
258         */
259        public void addCustomProperty(String name, Object value) {
260                getCustomProperties().put(name, value);
261        }
262
263        /**
264         * Specify WebServiceFeature objects (e.g. as inner bean definitions)
265         * to apply to JAX-WS port stub creation.
266         * @since 4.0
267         * @see Service#getPort(Class, javax.xml.ws.WebServiceFeature...)
268         * @see #setServiceFeatures
269         */
270        public void setPortFeatures(WebServiceFeature... features) {
271                this.portFeatures = features;
272        }
273
274        /**
275         * Specify WebServiceFeature specifications for the JAX-WS port stub:
276         * in the form of actual {@link javax.xml.ws.WebServiceFeature} objects,
277         * WebServiceFeature Class references, or WebServiceFeature class names.
278         * <p>As of Spring 4.0, this is effectively just an alternative way of
279         * specifying {@link #setPortFeatures "portFeatures"}. Do not specify
280         * both properties at the same time; prefer "portFeatures" moving forward.
281         * @deprecated as of Spring 4.0, in favor of the differentiated
282         * {@link #setServiceFeatures "serviceFeatures"} and
283         * {@link #setPortFeatures "portFeatures"} properties
284         */
285        @Deprecated
286        public void setWebServiceFeatures(Object[] webServiceFeatures) {
287                this.webServiceFeatures = webServiceFeatures;
288        }
289
290        /**
291         * Set the interface of the service that this factory should create a proxy for.
292         */
293        public void setServiceInterface(Class<?> serviceInterface) {
294                if (serviceInterface != null && !serviceInterface.isInterface()) {
295                        throw new IllegalArgumentException("'serviceInterface' must be an interface");
296                }
297                this.serviceInterface = serviceInterface;
298        }
299
300        /**
301         * Return the interface of the service that this factory should create a proxy for.
302         */
303        public Class<?> getServiceInterface() {
304                return this.serviceInterface;
305        }
306
307        /**
308         * Set whether to look up the JAX-WS service on startup.
309         * <p>Default is "true". Turn this flag off to allow for late start
310         * of the target server. In this case, the JAX-WS service will be
311         * lazily fetched on first access.
312         */
313        public void setLookupServiceOnStartup(boolean lookupServiceOnStartup) {
314                this.lookupServiceOnStartup = lookupServiceOnStartup;
315        }
316
317        /**
318         * Set the bean ClassLoader to use for this interceptor:
319         * for resolving WebServiceFeature class names as specified through
320         * {@link #setWebServiceFeatures}, and also for building a client
321         * proxy in the {@link JaxWsPortProxyFactoryBean} subclass.
322         */
323        @Override
324        public void setBeanClassLoader(ClassLoader classLoader) {
325                this.beanClassLoader = classLoader;
326        }
327
328        /**
329         * Return the bean ClassLoader to use for this interceptor.
330         */
331        protected ClassLoader getBeanClassLoader() {
332                return this.beanClassLoader;
333        }
334
335
336        @Override
337        public void afterPropertiesSet() {
338                if (this.lookupServiceOnStartup) {
339                        prepare();
340                }
341        }
342
343        /**
344         * Initialize the JAX-WS port for this interceptor.
345         */
346        public void prepare() {
347                Class<?> ifc = getServiceInterface();
348                if (ifc == null) {
349                        throw new IllegalArgumentException("Property 'serviceInterface' is required");
350                }
351                WebService ann = ifc.getAnnotation(WebService.class);
352                if (ann != null) {
353                        applyDefaultsFromAnnotation(ann);
354                }
355                Service serviceToUse = getJaxWsService();
356                if (serviceToUse == null) {
357                        serviceToUse = createJaxWsService();
358                }
359                this.portQName = getQName(getPortName() != null ? getPortName() : getServiceInterface().getName());
360                Object stub = getPortStub(serviceToUse, (getPortName() != null ? this.portQName : null));
361                preparePortStub(stub);
362                this.portStub = stub;
363        }
364
365        /**
366         * Initialize this client interceptor's properties from the given WebService annotation,
367         * if necessary and possible (i.e. if "wsdlDocumentUrl", "namespaceUri", "serviceName"
368         * and "portName" haven't been set but corresponding values are declared at the
369         * annotation level of the specified service interface).
370         * @param ann the WebService annotation found on the specified service interface
371         */
372        protected void applyDefaultsFromAnnotation(WebService ann) {
373                if (getWsdlDocumentUrl() == null) {
374                        String wsdl = ann.wsdlLocation();
375                        if (StringUtils.hasText(wsdl)) {
376                                try {
377                                        setWsdlDocumentUrl(new URL(wsdl));
378                                }
379                                catch (MalformedURLException ex) {
380                                        throw new IllegalStateException(
381                                                        "Encountered invalid @Service wsdlLocation value [" + wsdl + "]", ex);
382                                }
383                        }
384                }
385                if (getNamespaceUri() == null) {
386                        String ns = ann.targetNamespace();
387                        if (StringUtils.hasText(ns)) {
388                                setNamespaceUri(ns);
389                        }
390                }
391                if (getServiceName() == null) {
392                        String sn = ann.serviceName();
393                        if (StringUtils.hasText(sn)) {
394                                setServiceName(sn);
395                        }
396                }
397                if (getPortName() == null) {
398                        String pn = ann.portName();
399                        if (StringUtils.hasText(pn)) {
400                                setPortName(pn);
401                        }
402                }
403        }
404
405        /**
406         * Return whether this client interceptor has already been prepared,
407         * i.e. has already looked up the JAX-WS service and port.
408         */
409        protected boolean isPrepared() {
410                synchronized (this.preparationMonitor) {
411                        return (this.portStub != null);
412                }
413        }
414
415        /**
416         * Return the prepared QName for the port.
417         * @see #setPortName
418         * @see #getQName
419         */
420        protected final QName getPortQName() {
421                return this.portQName;
422        }
423
424        /**
425         * Obtain the port stub from the given JAX-WS Service.
426         * @param service the Service object to obtain the port from
427         * @param portQName the name of the desired port, if specified
428         * @return the corresponding port object as returned from
429         * {@code Service.getPort(...)}
430         */
431        protected Object getPortStub(Service service, QName portQName) {
432                if (this.portFeatures != null || this.webServiceFeatures != null) {
433                        WebServiceFeature[] portFeaturesToUse = this.portFeatures;
434                        if (portFeaturesToUse == null) {
435                                portFeaturesToUse = new WebServiceFeature[this.webServiceFeatures.length];
436                                for (int i = 0; i < this.webServiceFeatures.length; i++) {
437                                        portFeaturesToUse[i] = convertWebServiceFeature(this.webServiceFeatures[i]);
438                                }
439                        }
440                        return (portQName != null ? service.getPort(portQName, getServiceInterface(), portFeaturesToUse) :
441                                        service.getPort(getServiceInterface(), portFeaturesToUse));
442                }
443                else {
444                        return (portQName != null ? service.getPort(portQName, getServiceInterface()) :
445                                        service.getPort(getServiceInterface()));
446                }
447        }
448
449        /**
450         * Convert the given feature specification object to a WebServiceFeature instance
451         * @param feature the feature specification object, as passed into the
452         * {@link #setWebServiceFeatures "webServiceFeatures"} bean property
453         * @return the WebServiceFeature instance (never {@code null})
454         */
455        private WebServiceFeature convertWebServiceFeature(Object feature) {
456                Assert.notNull(feature, "WebServiceFeature specification object must not be null");
457                if (feature instanceof WebServiceFeature) {
458                        return (WebServiceFeature) feature;
459                }
460                else if (feature instanceof Class) {
461                        return (WebServiceFeature) BeanUtils.instantiate((Class<?>) feature);
462                }
463                else if (feature instanceof String) {
464                        try {
465                                Class<?> featureClass = getBeanClassLoader().loadClass((String) feature);
466                                return (WebServiceFeature) BeanUtils.instantiate(featureClass);
467                        }
468                        catch (ClassNotFoundException ex) {
469                                throw new IllegalArgumentException("Could not load WebServiceFeature class [" + feature + "]");
470                        }
471                }
472                else {
473                        throw new IllegalArgumentException("Unknown WebServiceFeature specification type: " + feature.getClass());
474                }
475        }
476
477        /**
478         * Prepare the given JAX-WS port stub, applying properties to it.
479         * Called by {@link #prepare}.
480         * @param stub the current JAX-WS port stub
481         * @see #setUsername
482         * @see #setPassword
483         * @see #setEndpointAddress
484         * @see #setMaintainSession
485         * @see #setCustomProperties
486         */
487        protected void preparePortStub(Object stub) {
488                Map<String, Object> stubProperties = new HashMap<String, Object>();
489                String username = getUsername();
490                if (username != null) {
491                        stubProperties.put(BindingProvider.USERNAME_PROPERTY, username);
492                }
493                String password = getPassword();
494                if (password != null) {
495                        stubProperties.put(BindingProvider.PASSWORD_PROPERTY, password);
496                }
497                String endpointAddress = getEndpointAddress();
498                if (endpointAddress != null) {
499                        stubProperties.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpointAddress);
500                }
501                if (isMaintainSession()) {
502                        stubProperties.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE);
503                }
504                if (isUseSoapAction()) {
505                        stubProperties.put(BindingProvider.SOAPACTION_USE_PROPERTY, Boolean.TRUE);
506                }
507                String soapActionUri = getSoapActionUri();
508                if (soapActionUri != null) {
509                        stubProperties.put(BindingProvider.SOAPACTION_URI_PROPERTY, soapActionUri);
510                }
511                stubProperties.putAll(getCustomProperties());
512                if (!stubProperties.isEmpty()) {
513                        if (!(stub instanceof BindingProvider)) {
514                                throw new RemoteLookupFailureException("Port stub of class [" + stub.getClass().getName() +
515                                                "] is not a customizable JAX-WS stub: it does not implement interface [javax.xml.ws.BindingProvider]");
516                        }
517                        ((BindingProvider) stub).getRequestContext().putAll(stubProperties);
518                }
519        }
520
521        /**
522         * Return the underlying JAX-WS port stub that this interceptor delegates to
523         * for each method invocation on the proxy.
524         */
525        protected Object getPortStub() {
526                return this.portStub;
527        }
528
529
530        @Override
531        public Object invoke(MethodInvocation invocation) throws Throwable {
532                if (AopUtils.isToStringMethod(invocation.getMethod())) {
533                        return "JAX-WS proxy for port [" + getPortName() + "] of service [" + getServiceName() + "]";
534                }
535                // Lazily prepare service and stub if necessary.
536                synchronized (this.preparationMonitor) {
537                        if (!isPrepared()) {
538                                prepare();
539                        }
540                }
541                return doInvoke(invocation);
542        }
543
544        /**
545         * Perform a JAX-WS service invocation based on the given method invocation.
546         * @param invocation the AOP method invocation
547         * @return the invocation result, if any
548         * @throws Throwable in case of invocation failure
549         * @see #getPortStub()
550         * @see #doInvoke(org.aopalliance.intercept.MethodInvocation, Object)
551         */
552        protected Object doInvoke(MethodInvocation invocation) throws Throwable {
553                try {
554                        return doInvoke(invocation, getPortStub());
555                }
556                catch (SOAPFaultException ex) {
557                        throw new JaxWsSoapFaultException(ex);
558                }
559                catch (ProtocolException ex) {
560                        throw new RemoteConnectFailureException(
561                                        "Could not connect to remote service [" + getEndpointAddress() + "]", ex);
562                }
563                catch (WebServiceException ex) {
564                        throw new RemoteAccessException(
565                                        "Could not access remote service at [" + getEndpointAddress() + "]", ex);
566                }
567        }
568
569        /**
570         * Perform a JAX-WS service invocation on the given port stub.
571         * @param invocation the AOP method invocation
572         * @param portStub the RMI port stub to invoke
573         * @return the invocation result, if any
574         * @throws Throwable in case of invocation failure
575         * @see #getPortStub()
576         */
577        protected Object doInvoke(MethodInvocation invocation, Object portStub) throws Throwable {
578                Method method = invocation.getMethod();
579                try {
580                        return method.invoke(portStub, invocation.getArguments());
581                }
582                catch (InvocationTargetException ex) {
583                        throw ex.getTargetException();
584                }
585                catch (Throwable ex) {
586                        throw new RemoteProxyFailureException("Invocation of stub method failed: " + method, ex);
587                }
588        }
589
590}