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}