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}