View Javadoc

1   /**
2    * Licensed to jclouds, Inc. (jclouds) under one or more
3    * contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  jclouds licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.jclouds.rest.internal;
20  
21  /**
22   * Generates RESTful clients from appropriately annotated interfaces.
23   * 
24   * @author Adrian Cole
25   */
26  import java.lang.annotation.Annotation;
27  import java.lang.reflect.InvocationHandler;
28  import java.lang.reflect.Method;
29  import java.util.NoSuchElementException;
30  import java.util.concurrent.ExecutionException;
31  
32  import javax.annotation.Resource;
33  import javax.inject.Named;
34  import javax.inject.Qualifier;
35  import javax.inject.Singleton;
36  
37  import org.jclouds.Constants;
38  import org.jclouds.concurrent.ExceptionParsingListenableFuture;
39  import org.jclouds.http.HttpRequest;
40  import org.jclouds.http.HttpResponse;
41  import org.jclouds.http.TransformingHttpCommand;
42  import org.jclouds.internal.ClassMethodArgs;
43  import org.jclouds.logging.Logger;
44  import org.jclouds.rest.AuthorizationException;
45  import org.jclouds.rest.InvocationContext;
46  import org.jclouds.rest.annotations.Delegate;
47  import org.jclouds.util.Throwables2;
48  
49  import com.google.common.base.Function;
50  import com.google.common.base.Predicate;
51  import com.google.common.cache.Cache;
52  import com.google.common.collect.ImmutableList;
53  import com.google.common.collect.Iterables;
54  import com.google.common.util.concurrent.Futures;
55  import com.google.common.util.concurrent.ListenableFuture;
56  import com.google.inject.Inject;
57  import com.google.inject.Injector;
58  import com.google.inject.Key;
59  import com.google.inject.Provides;
60  import com.google.inject.ProvisionException;
61  import com.google.inject.TypeLiteral;
62  
63  @Singleton
64  public class AsyncRestClientProxy<T> implements InvocationHandler {
65     private final Injector injector;
66     private final RestAnnotationProcessor<T> annotationProcessor;
67     private final Class<T> declaring;
68     private final Factory commandFactory;
69  
70     /**
71      * maximum duration of an unwrapped http Request
72      */
73     @Inject(optional = true)
74     @Named(Constants.PROPERTY_REQUEST_TIMEOUT)
75     protected long requestTimeoutMilliseconds = 30000;
76  
77     @Resource
78     protected Logger logger = Logger.NULL;
79     private final Cache<ClassMethodArgs, Object> delegateMap;
80  
81     @SuppressWarnings("unchecked")
82     @Inject
83     public AsyncRestClientProxy(Injector injector, Factory factory, RestAnnotationProcessor<T> util,
84              TypeLiteral<T> typeLiteral, @Named("async") Cache<ClassMethodArgs, Object> delegateMap) {
85        this.injector = injector;
86        this.annotationProcessor = util;
87        this.declaring = (Class<T>) typeLiteral.getRawType();
88        this.commandFactory = factory;
89        this.delegateMap = delegateMap;
90     }
91  
92     private static final Predicate<Annotation> isQualifierPresent = new Predicate<Annotation>() {
93  
94        @Override
95        public boolean apply(Annotation input) {
96           return input.annotationType().isAnnotationPresent(Qualifier.class);
97        }
98  
99     };
100 
101    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
102       if (method.getName().equals("equals")) {
103          return this.equals(o);
104       } else if (method.getName().equals("toString")) {
105          return this.toString();
106       } else if (method.getName().equals("hashCode")) {
107          return this.hashCode();
108       } else if (method.isAnnotationPresent(Provides.class)) {
109          try {
110             try {
111                Annotation qualifier = Iterables.find(ImmutableList.copyOf(method.getAnnotations()), isQualifierPresent);
112                return injector.getInstance(Key.get(method.getGenericReturnType(), qualifier));
113             } catch (NoSuchElementException e) {
114                return injector.getInstance(Key.get(method.getGenericReturnType()));
115             }
116          } catch (ProvisionException e) {
117             AuthorizationException aex = Throwables2.getFirstThrowableOfType(e, AuthorizationException.class);
118             if (aex != null)
119                throw aex;
120             throw e;
121          }
122       } else if (method.isAnnotationPresent(Delegate.class)) {
123          return delegateMap.get(new ClassMethodArgs(method.getReturnType(), method, args));
124       } else if (annotationProcessor.getDelegateOrNull(method) != null
125                && ListenableFuture.class.isAssignableFrom(method.getReturnType())) {
126          return createListenableFuture(method, args);
127       } else {
128          throw new RuntimeException("method is intended solely to set constants: " + method);
129       }
130    }
131 
132    @SuppressWarnings( { "unchecked", "rawtypes" })
133    private ListenableFuture<?> createListenableFuture(Method method, Object[] args) throws ExecutionException {
134       method = annotationProcessor.getDelegateOrNull(method);
135       logger.trace("Converting %s.%s", declaring.getSimpleName(), method.getName());
136       Function<Exception, ?> exceptionParser = annotationProcessor
137                .createExceptionParserOrThrowResourceNotFoundOn404IfNoAnnotation(method);
138       // in case there is an exception creating the request, we should at least
139       // pass in args
140       if (exceptionParser instanceof InvocationContext) {
141          ((InvocationContext) exceptionParser).setContext((HttpRequest) null);
142       }
143       ListenableFuture<?> result;
144       try {
145          GeneratedHttpRequest<T> request = annotationProcessor.createRequest(method, args);
146          if (exceptionParser instanceof InvocationContext) {
147             ((InvocationContext) exceptionParser).setContext(request);
148          }
149          logger.trace("Converted %s.%s to %s", declaring.getSimpleName(), method.getName(), request.getRequestLine());
150 
151          Function<HttpResponse, ?> transformer = annotationProcessor.createResponseParser(method, request);
152          logger.trace("Response from %s.%s is parsed by %s", declaring.getSimpleName(), method.getName(), transformer
153                   .getClass().getSimpleName());
154 
155          logger.debug("Invoking %s.%s", declaring.getSimpleName(), method.getName());
156          result = commandFactory.create(request, transformer).execute();
157 
158       } catch (RuntimeException e) {
159          AuthorizationException aex = Throwables2.getFirstThrowableOfType(e, AuthorizationException.class);
160          if (aex != null)
161             e = aex;
162          if (exceptionParser != null) {
163             try {
164                return Futures.immediateFuture(exceptionParser.apply(e));
165             } catch (Exception ex) {
166                return Futures.immediateFailedFuture(ex);
167             }
168          }
169          return Futures.immediateFailedFuture(e);
170       }
171 
172       if (exceptionParser != null) {
173          logger.trace("Exceptions from %s.%s are parsed by %s", declaring.getSimpleName(), method.getName(),
174                   exceptionParser.getClass().getSimpleName());
175          result = new ExceptionParsingListenableFuture(result, exceptionParser);
176       }
177       return result;
178    }
179 
180    public static interface Factory {
181       public TransformingHttpCommand<?> create(HttpRequest request, Function<HttpResponse, ?> transformer);
182    }
183 
184    @Override
185    public boolean equals(Object obj) {
186       if (obj == null || !(obj instanceof AsyncRestClientProxy<?>))
187          return false;
188       AsyncRestClientProxy<?> other = (AsyncRestClientProxy<?>) obj;
189       if (other == this)
190          return true;
191       if (other.declaring != this.declaring)
192          return false;
193       return super.equals(obj);
194    }
195 
196    @Override
197    public int hashCode() {
198       return declaring.hashCode();
199    }
200 
201    public String toString() {
202       return "Client Proxy for :" + declaring.getName();
203    }
204 }