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.concurrent.internal;
20  
21  import static com.google.common.base.Preconditions.checkState;
22  
23  import java.lang.reflect.InvocationHandler;
24  import java.lang.reflect.Method;
25  import java.lang.reflect.Proxy;
26  import java.util.Arrays;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.concurrent.ExecutionException;
30  import java.util.concurrent.TimeUnit;
31  
32  import javax.inject.Inject;
33  import javax.inject.Named;
34  
35  import com.google.common.collect.Iterables;
36  import com.google.common.collect.Multimap;
37  import org.jclouds.concurrent.Timeout;
38  import org.jclouds.internal.ClassMethodArgs;
39  import org.jclouds.rest.annotations.Delegate;
40  import org.jclouds.util.Throwables2;
41  
42  import com.google.common.cache.Cache;
43  import com.google.common.collect.ImmutableSet;
44  import com.google.common.collect.Maps;
45  import com.google.common.util.concurrent.ListenableFuture;
46  import com.google.inject.ProvisionException;
47  
48  /**
49   * Generates RESTful clients from appropriately annotated interfaces.
50   * 
51   * @author Adrian Cole
52   */
53  @SuppressWarnings("deprecation")
54  public class SyncProxy implements InvocationHandler {
55  
56     @SuppressWarnings("unchecked")
57     public static <T> T proxy(Class<T> clazz, Object async,
58           @Named("sync") Cache<ClassMethodArgs, Object> delegateMap,
59           Map<Class<?>, Class<?>> sync2Async, Map<String, Long> timeouts) throws IllegalArgumentException, SecurityException,
60           NoSuchMethodException {
61        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[] { clazz },
62                new SyncProxy(clazz, async, delegateMap, sync2Async, timeouts));
63     }
64  
65     private final Object delegate;
66     private final Class<?> declaring;
67     private final Map<Method, Method> methodMap;
68     private final Map<Method, Method> syncMethodMap;
69     private final Map<Method, Long> timeoutMap;
70     private final Cache<ClassMethodArgs, Object> delegateMap;
71     private final Map<Class<?>, Class<?>> sync2Async;
72     private static final Set<Method> objectMethods = ImmutableSet.of(Object.class.getMethods());
73  
74     @Inject
75     private SyncProxy(Class<?> declaring, Object async,
76           @Named("sync") Cache<ClassMethodArgs, Object> delegateMap, Map<Class<?>,
77             Class<?>> sync2Async, final Map<String, Long> timeouts)
78           throws SecurityException, NoSuchMethodException {
79        this.delegateMap = delegateMap;
80        this.delegate = async;
81        this.declaring = declaring;
82        this.sync2Async = sync2Async;
83        if (!declaring.isAnnotationPresent(Timeout.class)) {
84           throw new IllegalArgumentException(String.format("type %s does not specify a default @Timeout", declaring));
85        }
86        Timeout typeTimeout = declaring.getAnnotation(Timeout.class);
87        long typeNanos = convertToNanos(typeTimeout);
88  
89        methodMap = Maps.newHashMap();
90        syncMethodMap = Maps.newHashMap();
91        timeoutMap = Maps.newHashMap();
92        for (Method method : declaring.getMethods()) {
93           if (!objectMethods.contains(method)) {
94              Method delegatedMethod = delegate.getClass().getMethod(method.getName(), method.getParameterTypes());
95              if (!Arrays.equals(delegatedMethod.getExceptionTypes(), method.getExceptionTypes()))
96                 throw new IllegalArgumentException(String.format(
97                       "method %s has different typed exceptions than delegated method %s", method, delegatedMethod));
98              if (delegatedMethod.getReturnType().isAssignableFrom(ListenableFuture.class)) {
99                 timeoutMap.put(method, getTimeout(method, typeNanos, timeouts));
100                methodMap.put(method, delegatedMethod);
101             } else {
102                syncMethodMap.put(method, delegatedMethod);
103             }
104          }
105       }
106    }
107 
108    private Long getTimeout(Method method, long typeNanos, final Map<String,Long> timeouts) {
109       Long timeout = overrideTimeout(method, timeouts);
110       if (timeout == null && method.isAnnotationPresent(Timeout.class)) {
111          Timeout methodTimeout = method.getAnnotation(Timeout.class);
112          timeout = convertToNanos(methodTimeout);
113       }
114       return timeout != null ? timeout : typeNanos;
115 
116    }
117 
118    static long convertToNanos(Timeout timeout) {
119       long methodNanos = TimeUnit.NANOSECONDS.convert(timeout.duration(), timeout.timeUnit());
120       return methodNanos;
121    }
122 
123    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
124       if (method.getName().equals("equals")) {
125          return this.equals(o);
126       } else if (method.getName().equals("hashCode")) {
127          return this.hashCode();
128       } else if (method.getName().equals("toString")) {
129          return this.toString();
130       } else if (method.isAnnotationPresent(Delegate.class)) {
131          Class<?> asyncClass = sync2Async.get(method.getReturnType());
132          checkState(asyncClass != null, "please configure corresponding async class for " + method.getReturnType()
133                + " in your RestClientModule");
134          Object returnVal = delegateMap.get(new ClassMethodArgs(asyncClass, method, args));
135          return returnVal;
136       } else if (syncMethodMap.containsKey(method)) {
137          return syncMethodMap.get(method).invoke(delegate, args);
138       } else {
139          try {
140             return ((ListenableFuture<?>) methodMap.get(method).invoke(delegate, args)).get(timeoutMap.get(method),
141                   TimeUnit.NANOSECONDS);
142          } catch (ProvisionException e) {
143             throw Throwables2.returnFirstExceptionIfInListOrThrowStandardExceptionOrCause(method.getExceptionTypes(), e);
144          } catch (ExecutionException e) {
145             throw Throwables2.returnFirstExceptionIfInListOrThrowStandardExceptionOrCause(method.getExceptionTypes(), e);
146          } catch (Exception e) {
147             throw Throwables2.returnFirstExceptionIfInListOrThrowStandardExceptionOrCause(method.getExceptionTypes(), e);
148          }
149       }
150    }
151 
152    // override timeout by values configured in properties(in ms)
153    private Long overrideTimeout(final Method method, final Map<String, Long> timeouts) {
154       if (timeouts == null) {
155          return null;
156       }
157       final String className = declaring.getSimpleName();
158       Long timeout = timeouts.get(className + "." + method.getName());
159       if (timeout == null) {
160          timeout = timeouts.get(className);
161       }
162       return timeout != null ? TimeUnit.MILLISECONDS.toNanos(timeout) : null;
163    }
164 
165    @Override
166    public boolean equals(Object obj) {
167       if (obj == null || !(obj instanceof SyncProxy))
168          return false;
169       SyncProxy other = (SyncProxy) obj;
170       if (other == this)
171          return true;
172       if (other.declaring != this.declaring)
173          return false;
174       return super.equals(obj);
175    }
176 
177    @Override
178    public int hashCode() {
179       return declaring.hashCode();
180    }
181 
182    public String toString() {
183       return "Sync Proxy for: " + delegate.getClass().getSimpleName();
184    }
185 }