001/*
002 * Copyright 2002-2017 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;
025
026import javax.jws.WebService;
027import javax.xml.namespace.QName;
028import javax.xml.ws.BindingProvider;
029import javax.xml.ws.ProtocolException;
030import javax.xml.ws.Service;
031import javax.xml.ws.WebServiceException;
032import javax.xml.ws.WebServiceFeature;
033import javax.xml.ws.soap.SOAPFaultException;
034
035import org.aopalliance.intercept.MethodInterceptor;
036import org.aopalliance.intercept.MethodInvocation;
037
038import org.springframework.aop.support.AopUtils;
039import org.springframework.beans.factory.BeanClassLoaderAware;
040import org.springframework.beans.factory.InitializingBean;
041import org.springframework.lang.Nullable;
042import org.springframework.remoting.RemoteAccessException;
043import org.springframework.remoting.RemoteConnectFailureException;
044import org.springframework.remoting.RemoteLookupFailureException;
045import org.springframework.remoting.RemoteProxyFailureException;
046import org.springframework.util.Assert;
047import org.springframework.util.ClassUtils;
048import org.springframework.util.StringUtils;
049
050/**
051 * {@link org.aopalliance.intercept.MethodInterceptor} for accessing a
052 * specific port of a JAX-WS service.
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        @Nullable
070        private Service jaxWsService;
071
072        @Nullable
073        private String portName;
074
075        @Nullable
076        private String username;
077
078        @Nullable
079        private String password;
080
081        @Nullable
082        private String endpointAddress;
083
084        private boolean maintainSession;
085
086        private boolean useSoapAction;
087
088        @Nullable
089        private String soapActionUri;
090
091        @Nullable
092        private Map<String, Object> customProperties;
093
094        @Nullable
095        private WebServiceFeature[] portFeatures;
096
097        @Nullable
098        private Class<?> serviceInterface;
099
100        private boolean lookupServiceOnStartup = true;
101
102        @Nullable
103        private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
104
105        @Nullable
106        private QName portQName;
107
108        @Nullable
109        private Object portStub;
110
111        private final Object preparationMonitor = new Object();
112
113
114        /**
115         * Set a reference to an existing JAX-WS Service instance,
116         * for example obtained via {@link org.springframework.jndi.JndiObjectFactoryBean}.
117         * If not set, {@link LocalJaxWsServiceFactory}'s properties have to be specified.
118         * @see #setWsdlDocumentUrl
119         * @see #setNamespaceUri
120         * @see #setServiceName
121         * @see org.springframework.jndi.JndiObjectFactoryBean
122         */
123        public void setJaxWsService(@Nullable Service jaxWsService) {
124                this.jaxWsService = jaxWsService;
125        }
126
127        /**
128         * Return a reference to an existing JAX-WS Service instance, if any.
129         */
130        @Nullable
131        public Service getJaxWsService() {
132                return this.jaxWsService;
133        }
134
135        /**
136         * Set the name of the port.
137         * Corresponds to the "wsdl:port" name.
138         */
139        public void setPortName(@Nullable String portName) {
140                this.portName = portName;
141        }
142
143        /**
144         * Return the name of the port.
145         */
146        @Nullable
147        public String getPortName() {
148                return this.portName;
149        }
150
151        /**
152         * Set the username to specify on the stub.
153         * @see javax.xml.ws.BindingProvider#USERNAME_PROPERTY
154         */
155        public void setUsername(@Nullable String username) {
156                this.username = username;
157        }
158
159        /**
160         * Return the username to specify on the stub.
161         */
162        @Nullable
163        public String getUsername() {
164                return this.username;
165        }
166
167        /**
168         * Set the password to specify on the stub.
169         * @see javax.xml.ws.BindingProvider#PASSWORD_PROPERTY
170         */
171        public void setPassword(@Nullable String password) {
172                this.password = password;
173        }
174
175        /**
176         * Return the password to specify on the stub.
177         */
178        @Nullable
179        public String getPassword() {
180                return this.password;
181        }
182
183        /**
184         * Set the endpoint address to specify on the stub.
185         * @see javax.xml.ws.BindingProvider#ENDPOINT_ADDRESS_PROPERTY
186         */
187        public void setEndpointAddress(@Nullable String endpointAddress) {
188                this.endpointAddress = endpointAddress;
189        }
190
191        /**
192         * Return the endpoint address to specify on the stub.
193         */
194        @Nullable
195        public String getEndpointAddress() {
196                return this.endpointAddress;
197        }
198
199        /**
200         * Set the "session.maintain" flag to specify on the stub.
201         * @see javax.xml.ws.BindingProvider#SESSION_MAINTAIN_PROPERTY
202         */
203        public void setMaintainSession(boolean maintainSession) {
204                this.maintainSession = maintainSession;
205        }
206
207        /**
208         * Return the "session.maintain" flag to specify on the stub.
209         */
210        public boolean isMaintainSession() {
211                return this.maintainSession;
212        }
213
214        /**
215         * Set the "soapaction.use" flag to specify on the stub.
216         * @see javax.xml.ws.BindingProvider#SOAPACTION_USE_PROPERTY
217         */
218        public void setUseSoapAction(boolean useSoapAction) {
219                this.useSoapAction = useSoapAction;
220        }
221
222        /**
223         * Return the "soapaction.use" flag to specify on the stub.
224         */
225        public boolean isUseSoapAction() {
226                return this.useSoapAction;
227        }
228
229        /**
230         * Set the SOAP action URI to specify on the stub.
231         * @see javax.xml.ws.BindingProvider#SOAPACTION_URI_PROPERTY
232         */
233        public void setSoapActionUri(@Nullable String soapActionUri) {
234                this.soapActionUri = soapActionUri;
235        }
236
237        /**
238         * Return the SOAP action URI to specify on the stub.
239         */
240        @Nullable
241        public String getSoapActionUri() {
242                return this.soapActionUri;
243        }
244
245        /**
246         * Set custom properties to be set on the stub.
247         * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
248         * or a "props" element in XML bean definitions.
249         * @see javax.xml.ws.BindingProvider#getRequestContext()
250         */
251        public void setCustomProperties(Map<String, Object> customProperties) {
252                this.customProperties = customProperties;
253        }
254
255        /**
256         * Allow Map access to the custom properties to be set on the stub,
257         * with the option to add or override specific entries.
258         * <p>Useful for specifying entries directly, for example via
259         * "customProperties[myKey]". This is particularly useful for
260         * adding or overriding entries in child bean definitions.
261         */
262        public Map<String, Object> getCustomProperties() {
263                if (this.customProperties == null) {
264                        this.customProperties = new HashMap<>();
265                }
266                return this.customProperties;
267        }
268
269        /**
270         * Add a custom property to this JAX-WS BindingProvider.
271         * @param name the name of the attribute to expose
272         * @param value the attribute value to expose
273         * @see javax.xml.ws.BindingProvider#getRequestContext()
274         */
275        public void addCustomProperty(String name, Object value) {
276                getCustomProperties().put(name, value);
277        }
278
279        /**
280         * Specify WebServiceFeature objects (e.g. as inner bean definitions)
281         * to apply to JAX-WS port stub creation.
282         * @since 4.0
283         * @see Service#getPort(Class, javax.xml.ws.WebServiceFeature...)
284         * @see #setServiceFeatures
285         */
286        public void setPortFeatures(WebServiceFeature... features) {
287                this.portFeatures = features;
288        }
289
290        /**
291         * Set the interface of the service that this factory should create a proxy for.
292         */
293        public void setServiceInterface(@Nullable Class<?> serviceInterface) {
294                if (serviceInterface != null) {
295                        Assert.isTrue(serviceInterface.isInterface(), "'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        @Nullable
304        public Class<?> getServiceInterface() {
305                return this.serviceInterface;
306        }
307
308        /**
309         * Set whether to look up the JAX-WS service on startup.
310         * <p>Default is "true". Turn this flag off to allow for late start
311         * of the target server. In this case, the JAX-WS service will be
312         * lazily fetched on first access.
313         */
314        public void setLookupServiceOnStartup(boolean lookupServiceOnStartup) {
315                this.lookupServiceOnStartup = lookupServiceOnStartup;
316        }
317
318        /**
319         * Set the bean ClassLoader to use for this interceptor: primarily for
320         * building a client proxy in the {@link JaxWsPortProxyFactoryBean} subclass.
321         */
322        @Override
323        public void setBeanClassLoader(@Nullable ClassLoader classLoader) {
324                this.beanClassLoader = classLoader;
325        }
326
327        /**
328         * Return the bean ClassLoader to use for this interceptor.
329         */
330        @Nullable
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                Assert.notNull(ifc, "Property 'serviceInterface' is required");
349
350                WebService ann = ifc.getAnnotation(WebService.class);
351                if (ann != null) {
352                        applyDefaultsFromAnnotation(ann);
353                }
354
355                Service serviceToUse = getJaxWsService();
356                if (serviceToUse == null) {
357                        serviceToUse = createJaxWsService();
358                }
359
360                this.portQName = getQName(getPortName() != null ? getPortName() : ifc.getName());
361                Object stub = getPortStub(serviceToUse, (getPortName() != null ? this.portQName : null));
362                preparePortStub(stub);
363                this.portStub = stub;
364        }
365
366        /**
367         * Initialize this client interceptor's properties from the given WebService annotation,
368         * if necessary and possible (i.e. if "wsdlDocumentUrl", "namespaceUri", "serviceName"
369         * and "portName" haven't been set but corresponding values are declared at the
370         * annotation level of the specified service interface).
371         * @param ann the WebService annotation found on the specified service interface
372         */
373        protected void applyDefaultsFromAnnotation(WebService ann) {
374                if (getWsdlDocumentUrl() == null) {
375                        String wsdl = ann.wsdlLocation();
376                        if (StringUtils.hasText(wsdl)) {
377                                try {
378                                        setWsdlDocumentUrl(new URL(wsdl));
379                                }
380                                catch (MalformedURLException ex) {
381                                        throw new IllegalStateException(
382                                                        "Encountered invalid @Service wsdlLocation value [" + wsdl + "]", ex);
383                                }
384                        }
385                }
386                if (getNamespaceUri() == null) {
387                        String ns = ann.targetNamespace();
388                        if (StringUtils.hasText(ns)) {
389                                setNamespaceUri(ns);
390                        }
391                }
392                if (getServiceName() == null) {
393                        String sn = ann.serviceName();
394                        if (StringUtils.hasText(sn)) {
395                                setServiceName(sn);
396                        }
397                }
398                if (getPortName() == null) {
399                        String pn = ann.portName();
400                        if (StringUtils.hasText(pn)) {
401                                setPortName(pn);
402                        }
403                }
404        }
405
406        /**
407         * Return whether this client interceptor has already been prepared,
408         * i.e. has already looked up the JAX-WS service and port.
409         */
410        protected boolean isPrepared() {
411                synchronized (this.preparationMonitor) {
412                        return (this.portStub != null);
413                }
414        }
415
416        /**
417         * Return the prepared QName for the port.
418         * @see #setPortName
419         * @see #getQName
420         */
421        @Nullable
422        protected final QName getPortQName() {
423                return this.portQName;
424        }
425
426        /**
427         * Obtain the port stub from the given JAX-WS Service.
428         * @param service the Service object to obtain the port from
429         * @param portQName the name of the desired port, if specified
430         * @return the corresponding port object as returned from
431         * {@code Service.getPort(...)}
432         */
433        protected Object getPortStub(Service service, @Nullable QName portQName) {
434                if (this.portFeatures != null) {
435                        return (portQName != null ? service.getPort(portQName, getServiceInterface(), this.portFeatures) :
436                                        service.getPort(getServiceInterface(), this.portFeatures));
437                }
438                else {
439                        return (portQName != null ? service.getPort(portQName, getServiceInterface()) :
440                                        service.getPort(getServiceInterface()));
441                }
442        }
443
444        /**
445         * Prepare the given JAX-WS port stub, applying properties to it.
446         * Called by {@link #prepare}.
447         * @param stub the current JAX-WS port stub
448         * @see #setUsername
449         * @see #setPassword
450         * @see #setEndpointAddress
451         * @see #setMaintainSession
452         * @see #setCustomProperties
453         */
454        protected void preparePortStub(Object stub) {
455                Map<String, Object> stubProperties = new HashMap<>();
456                String username = getUsername();
457                if (username != null) {
458                        stubProperties.put(BindingProvider.USERNAME_PROPERTY, username);
459                }
460                String password = getPassword();
461                if (password != null) {
462                        stubProperties.put(BindingProvider.PASSWORD_PROPERTY, password);
463                }
464                String endpointAddress = getEndpointAddress();
465                if (endpointAddress != null) {
466                        stubProperties.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpointAddress);
467                }
468                if (isMaintainSession()) {
469                        stubProperties.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE);
470                }
471                if (isUseSoapAction()) {
472                        stubProperties.put(BindingProvider.SOAPACTION_USE_PROPERTY, Boolean.TRUE);
473                }
474                String soapActionUri = getSoapActionUri();
475                if (soapActionUri != null) {
476                        stubProperties.put(BindingProvider.SOAPACTION_URI_PROPERTY, soapActionUri);
477                }
478                stubProperties.putAll(getCustomProperties());
479                if (!stubProperties.isEmpty()) {
480                        if (!(stub instanceof BindingProvider)) {
481                                throw new RemoteLookupFailureException("Port stub of class [" + stub.getClass().getName() +
482                                                "] is not a customizable JAX-WS stub: it does not implement interface [javax.xml.ws.BindingProvider]");
483                        }
484                        ((BindingProvider) stub).getRequestContext().putAll(stubProperties);
485                }
486        }
487
488        /**
489         * Return the underlying JAX-WS port stub that this interceptor delegates to
490         * for each method invocation on the proxy.
491         */
492        @Nullable
493        protected Object getPortStub() {
494                return this.portStub;
495        }
496
497
498        @Override
499        @Nullable
500        public Object invoke(MethodInvocation invocation) throws Throwable {
501                if (AopUtils.isToStringMethod(invocation.getMethod())) {
502                        return "JAX-WS proxy for port [" + getPortName() + "] of service [" + getServiceName() + "]";
503                }
504                // Lazily prepare service and stub if necessary.
505                synchronized (this.preparationMonitor) {
506                        if (!isPrepared()) {
507                                prepare();
508                        }
509                }
510                return doInvoke(invocation);
511        }
512
513        /**
514         * Perform a JAX-WS service invocation based on the given method invocation.
515         * @param invocation the AOP method invocation
516         * @return the invocation result, if any
517         * @throws Throwable in case of invocation failure
518         * @see #getPortStub()
519         * @see #doInvoke(org.aopalliance.intercept.MethodInvocation, Object)
520         */
521        @Nullable
522        protected Object doInvoke(MethodInvocation invocation) throws Throwable {
523                try {
524                        return doInvoke(invocation, getPortStub());
525                }
526                catch (SOAPFaultException ex) {
527                        throw new JaxWsSoapFaultException(ex);
528                }
529                catch (ProtocolException ex) {
530                        throw new RemoteConnectFailureException(
531                                        "Could not connect to remote service [" + getEndpointAddress() + "]", ex);
532                }
533                catch (WebServiceException ex) {
534                        throw new RemoteAccessException(
535                                        "Could not access remote service at [" + getEndpointAddress() + "]", ex);
536                }
537        }
538
539        /**
540         * Perform a JAX-WS service invocation on the given port stub.
541         * @param invocation the AOP method invocation
542         * @param portStub the RMI port stub to invoke
543         * @return the invocation result, if any
544         * @throws Throwable in case of invocation failure
545         * @see #getPortStub()
546         */
547        @Nullable
548        protected Object doInvoke(MethodInvocation invocation, @Nullable Object portStub) throws Throwable {
549                Method method = invocation.getMethod();
550                try {
551                        return method.invoke(portStub, invocation.getArguments());
552                }
553                catch (InvocationTargetException ex) {
554                        throw ex.getTargetException();
555                }
556                catch (Throwable ex) {
557                        throw new RemoteProxyFailureException("Invocation of stub method failed: " + method, ex);
558                }
559        }
560
561}