001    package org.andromda.cartridges.support.webservice.client;
002    
003    import java.lang.reflect.Method;
004    import java.net.URL;
005    import java.util.ArrayList;
006    import java.util.Arrays;
007    import java.util.HashMap;
008    import java.util.Iterator;
009    import java.util.LinkedHashSet;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.Set;
013    import javax.wsdl.Definition;
014    import javax.wsdl.Port;
015    import javax.wsdl.Service;
016    import javax.wsdl.extensions.soap.SOAPAddress;
017    import javax.wsdl.factory.WSDLFactory;
018    import javax.wsdl.xml.WSDLReader;
019    import javax.xml.namespace.QName;
020    import org.apache.axiom.om.OMElement;
021    import org.apache.axis2.AxisFault;
022    import org.apache.axis2.addressing.EndpointReference;
023    import org.apache.axis2.client.ServiceClient;
024    import org.apache.axis2.client.async.Callback;
025    import org.apache.axis2.transport.http.HTTPConstants;
026    import org.apache.axis2.transport.http.HttpTransportProperties.Authenticator;
027    import org.apache.commons.lang.StringUtils;
028    import org.xml.sax.InputSource;
029    
030    /**
031     * A webservice client using Axis2 libraries that allows you to invoke operations on wrapped
032     * style services.
033     *
034     * @author Chad Brandon
035     */
036    public class WebServiceClient
037    {
038        /**
039         * The actual class to use for the webservice.
040         */
041        private Class serviceClass;
042    
043        /**
044         * The WSDL definition.
045         */
046        private Definition definition;
047    
048        /**
049         * The WSDL url.
050         */
051        private String wsdlUrl;
052    
053        /**
054         * The monitor used for synchronization of the definition (to make it thread-safe).
055         */
056        private final Object definitionMonitor = new Object();
057    
058        /**
059         * The underlying service client.
060         */
061        private ServiceClient serviceClient;
062    
063        /**
064         * The optional TypeMapper instance to set.
065         */
066        private TypeMapper typeMapper = new DefaultTypeMapper();
067    
068        /**
069         * Constructs a new client taking a WSDL and serviceClass
070         *
071         * @param wsdlUrl the URL to the WSDL for the service.
072         * @param serviceClass the class used for communicating with the service (i.e. can be a regular java object).
073         */
074        public WebServiceClient(
075            final String wsdlUrl,
076            final Class serviceClass)
077        {
078            this(wsdlUrl, serviceClass, null, null);
079        }
080    
081        /**
082         * Constructs a new client taking a WSDL, the serviceClass and username and password.
083         *
084         * @param wsdlUrl the URL to the WSDL for the service.
085         * @param serviceClass the class used for communicating with the service (i.e. can be a regular java object).
086         * @param username the username to access to the service (if its protected by basic auth).
087         * @param password the password to access the service (if its protected by basic auth).
088         */
089        public WebServiceClient(
090            final String wsdlUrl,
091            final Class serviceClass,
092            final String username,
093            final String password)
094        {
095            this(wsdlUrl, null, serviceClass, username, password);
096        }
097    
098        /**
099         * Constructs a new client taking a WSDL, endpointAddress, serviceClass and username and password.
100         *
101         * @param wsdlUrl the URL to the WSDL for the service.
102         * @param endpointAddress the "end point" or "port" address of the service (if null, then the one in the WSDL will be used).
103         * @param serviceClass the class used for communicating with the service (i.e. can be a regular java object).
104         * @param username the username to access to the service (if its protected by basic auth).
105         * @param password the password to access the service (if its protected by basic auth).
106         */
107        public WebServiceClient(
108            final String wsdlUrl,
109            final String endpointAddress,
110            final Class serviceClass,
111            final String username,
112            final String password)
113        {
114            try
115            {
116                this.serviceClient = new ServiceClient();
117                this.serviceClass = serviceClass;
118                this.wsdlUrl = wsdlUrl;
119                final WSDLReader reader = WSDLFactory.newInstance().newWSDLReader();
120                if (StringUtils.isNotBlank(username))
121                {
122                    final Authenticator authenticator = new Authenticator();
123                    final List<String> authorizationSchemes = new ArrayList<String>();
124                    authorizationSchemes.add(Authenticator.BASIC);
125                    authenticator.setAuthSchemes(authorizationSchemes);
126                    authenticator.setUsername(username);
127                    authenticator.setPassword(password);
128                    authenticator.setPreemptiveAuthentication(true);
129                    this.serviceClient.getOptions().setProperty(
130                        HTTPConstants.AUTHENTICATE,
131                        authenticator);
132                    synchronized (this.definitionMonitor)
133                    {
134                        this.definition =
135                            this.readProtectedWsdl(
136                                reader,
137                                wsdlUrl,
138                                username,
139                                password);
140                    }
141                }
142                else
143                {
144                    synchronized (this.definitionMonitor)
145                    {
146                        this.definition = reader.readWSDL(wsdlUrl);
147                    }
148                }
149                String portAddress;
150                if (StringUtils.isNotBlank(endpointAddress))
151                {
152                    portAddress = endpointAddress;
153                }
154                else
155                {
156                    portAddress = this.findEndPointAddress();
157                }
158                serviceClient.setTargetEPR(new EndpointReference(portAddress));
159            }
160            catch (Exception exception)
161            {
162                this.handleException(exception);
163            }
164        }
165    
166        /**
167         * Sets the optional object creator implementation.
168         *
169         * @param typeMapper the type mapper used for mapping types.
170         */
171        public void setTypeMapper(final TypeMapper typeMapper)
172        {
173            if (typeMapper == null)
174            {
175                throw new IllegalArgumentException("'typeMapper' can not be null");
176            }
177            this.typeMapper = typeMapper;
178        }
179    
180        /**
181         * Sets the timeout in seconds.
182         *
183         * @param seconds
184         */
185        public void setTimeout(long seconds)
186        {
187            this.serviceClient.getOptions().setTimeOutInMilliSeconds(seconds * 1000);
188        }
189    
190        /**
191         * Reads a WSDL protected by basic authentication.
192         *
193         * @param reader the WSDL reader to use for reading the WSDL.
194         * @param address the address of the WSDL.
195         * @param username the username to authenticate with.
196         * @param password the password to authenticate with.
197         * @return the WSDL definition
198         */
199        private Definition readProtectedWsdl(
200            final WSDLReader reader,
201            String address,
202            String username,
203            String password)
204        {
205            Definition definition = null;
206            try
207            {
208                final org.apache.commons.httpclient.HttpClient client = new org.apache.commons.httpclient.HttpClient();
209                final org.apache.commons.httpclient.params.HttpClientParams params =
210                    new org.apache.commons.httpclient.params.HttpClientParams();
211                params.setAuthenticationPreemptive(true);
212                client.setParams(params);
213    
214                final org.apache.commons.httpclient.Credentials credentials =
215                    new org.apache.commons.httpclient.UsernamePasswordCredentials(username, password);
216                final org.apache.commons.httpclient.auth.AuthScope scope =
217                    new org.apache.commons.httpclient.auth.AuthScope(
218                        new URL(address).getHost(),
219                        org.apache.commons.httpclient.auth.AuthScope.ANY_PORT,
220                        org.apache.commons.httpclient.auth.AuthScope.ANY_REALM);
221                client.getState().setCredentials(
222                    scope,
223                    credentials);
224    
225                final org.apache.commons.httpclient.methods.GetMethod get =
226                    new org.apache.commons.httpclient.methods.GetMethod(address);
227                get.setDoAuthentication(true);
228    
229                int status = client.executeMethod(get);
230                if (status == 404)
231                {
232                    throw new WebServiceClientException("WSDL could not be found at: '" + address + '\'');
233                }
234                InputSource inputSource = null;
235                boolean authenticated = status > 0 && status < 400;
236                if (authenticated)
237                {
238                    inputSource = new InputSource(get.getResponseBodyAsStream());
239                }
240                else
241                {
242                    throw new WebServiceClientException("Could not authenticate user: '" + username + "' to WSDL: '" +
243                        address + '\'');
244                }
245                definition =
246                    reader.readWSDL(
247                        address,
248                        inputSource);
249                get.releaseConnection();
250            }
251            catch (Exception exception)
252            {
253                this.handleException(exception);
254            }
255            return definition;
256        }
257    
258        /**
259         * Finds the end point address of the service.
260         *
261         * @return the service end point address.
262         */
263        private String findEndPointAddress()
264        {
265            String address = null;
266            Map services;
267            synchronized (this.definitionMonitor)
268            {
269                services = this.definition.getServices();
270            }
271            if (services != null)
272            {
273                for (final Iterator iterator = services.keySet().iterator(); iterator.hasNext();)
274                {
275                    final QName name = (QName)iterator.next();
276                    if (this.serviceClass.getSimpleName().equals(name.getLocalPart()))
277                    {
278                        final Service service = (Service)services.get(name);
279                        final Map ports = service.getPorts();
280                        for (final Iterator portIterator = ports.keySet().iterator(); portIterator.hasNext();)
281                        {
282                            final String portName = (String)portIterator.next();
283                            final Port port = (Port)ports.get(portName);
284                            for (final Iterator addressIterator = port.getExtensibilityElements().iterator();
285                                addressIterator.hasNext();)
286                            {
287                                final Object element = addressIterator.next();
288                                if (element instanceof SOAPAddress)
289                                {
290                                    address = ((SOAPAddress)element).getLocationURI();
291                                }
292                            }
293                        }
294                    }
295                }
296            }
297            return address;
298        }
299    
300        /**
301         * Invokes the operation identified by the given <code>operationName</code> with the
302         * given <code>arguments</code>.
303         *
304         * @param operationName the name of the operation to invoke.
305         * @param arguments the arguments of the operation.
306         * @return invoke result
307         * @see org.andromda.cartridges.support.webservice.client.Axis2ClientUtils#deserialize(OMElement, Class, TypeMapper)
308         */
309        public Object invokeBlocking(
310            String operationName,
311            Object[] arguments)
312        {
313            final Method method = this.getMethod(
314                    operationName,
315                    arguments);
316            OMElement omElement = this.getOperationElement(method, arguments);
317            Object result = null;
318            try
319            {
320                OMElement response = this.serviceClient.sendReceive(omElement);
321                if (method.getReturnType() != void.class)
322                {
323                    result =
324                        Axis2ClientUtils.deserialize(
325                            response.getFirstElement(),
326                            method.getReturnType(),
327                            this.typeMapper);
328                }
329                omElement = null;
330                response = null;
331            }
332            catch (Exception exception)
333            {
334                this.handleException(exception);
335            }
336            return result;
337        }
338    
339        /**
340         * Retrieves the operation element from the internal WSDL definition.  If it can't be found
341         * an exception is thrown indicating the name of the operation not found.
342         *
343         * @param method the method to invoke.
344         * @param arguments the arguments of the method
345         * @return the found operation element
346         */
347        private OMElement getOperationElement(final Method method, Object[] arguments)
348        {
349            OMElement element = null;
350            synchronized (this.definitionMonitor)
351            {
352                element =
353                    Axis2ClientUtils.getOperationOMElement(
354                        this.definition,
355                        method,
356                        arguments,
357                        this.typeMapper);
358            }
359            if (element == null)
360            {
361                throw new WebServiceClientException("No operation named '" + method.getName()
362                    + "' was found in WSDL: " + this.wsdlUrl);
363            }
364            return element;
365        }
366    
367        /**
368         * Invoke the nonblocking/Asynchronous call
369         *
370         * @param operationName
371         * @param arguments
372         * @param callback
373         */
374        public void invokeNonBlocking(
375            String operationName,
376            Object[] arguments,
377            Callback callback)
378        {
379            final Method method = this.getMethod(
380                    operationName,
381                    arguments);
382            OMElement omElement = this.getOperationElement(method, arguments);
383            try
384            {
385                this.serviceClient.sendReceiveNonBlocking(
386                    omElement,
387                    callback);
388            }
389            catch (AxisFault exception)
390            {
391                this.handleException(exception);
392            }
393            omElement = null;
394        }
395    
396        /**
397         * @param operationName
398         * @param arguments
399         */
400        public void invokeRobust(
401            String operationName,
402            Object[] arguments)
403        {
404            final Method method = this.getMethod(
405                    operationName,
406                    arguments);
407            OMElement omElement = this.getOperationElement(method, arguments);
408            try
409            {
410                this.serviceClient.sendRobust(omElement);
411            }
412            catch (AxisFault exception)
413            {
414                this.handleException(exception);
415            }
416            omElement = null;
417        }
418    
419        /**
420         * Reclaims any resources used by the client.
421         */
422        public void cleanup()
423        {
424            try
425            {
426                this.serviceClient.cleanup();
427            }
428            catch (AxisFault exception)
429            {
430                this.handleException(exception);
431            }
432        }
433    
434        /**
435         * Stores the methods found on the {@link #serviceClass}.
436         */
437        private Map<String, Method> methods = new HashMap<String, Method>();
438    
439        private Method getMethod(
440            final String name,
441            final Object[] arguments)
442        {
443            Method found = methods.get(name);
444            if (found == null)
445            {
446                for (final Iterator iterator = this.getAllMethods().iterator(); iterator.hasNext();)
447                {
448                    final Method method = (Method)iterator.next();
449                    if (method.getName().equals(name) && arguments.length == method.getParameterTypes().length)
450                    {
451                        found = method;
452                        this.methods.put(
453                            name,
454                            found);
455                        break;
456                    }
457                }
458            }
459            return found;
460        }
461    
462        /**
463         * Loads all methods from the given <code>clazz</code> (this includes
464         * all super class methods, public, private and protected).
465         *
466         * @param clazz the class to retrieve the methods.
467         * @return the loaded methods.
468         */
469        private List getAllMethods()
470        {
471            final Set<Method> methods = new LinkedHashSet<Method>();
472            loadMethods(
473                this.serviceClass,
474                methods);
475            return new ArrayList<Method>(methods);
476        }
477    
478        /**
479         * Loads all methods from the given <code>clazz</code> (this includes
480         * all super class methods).
481         *
482         * @param methods the list to load full of methods.
483         * @param clazz the class to retrieve the methods.
484         */
485        private void loadMethods(
486            final Class clazz,
487            final Set<Method> methods)
488        {
489            methods.addAll(Arrays.asList(clazz.getDeclaredMethods()));
490            if (clazz.getSuperclass() != null)
491            {
492                loadMethods(
493                    clazz.getSuperclass(),
494                    methods);
495            }
496        }
497    
498        /**
499         * Appropriate wraps or just re-throws the exception if already an instance
500         * of {@link WebServiceClientException}.
501         *
502         * @param exception the exception to wrap or re-throw as a WebServiceClientException
503         */
504        private void handleException(Exception exception)
505        {
506            if (!(exception instanceof WebServiceClientException))
507            {
508                exception = new WebServiceClientException(exception);
509            }
510            throw (WebServiceClientException)exception;
511        }
512    }