8784: Fix test for latest firefox.
[arvados.git] / sdk / java / src / main / java / org / arvados / sdk / Arvados.java
1 package org.arvados.sdk;
2
3 import com.google.api.client.http.javanet.*;
4 import com.google.api.client.http.ByteArrayContent;
5 import com.google.api.client.http.GenericUrl;
6 import com.google.api.client.http.HttpBackOffIOExceptionHandler;
7 import com.google.api.client.http.HttpContent;
8 import com.google.api.client.http.HttpRequest;
9 import com.google.api.client.http.HttpRequestFactory;
10 import com.google.api.client.http.HttpTransport;
11 import com.google.api.client.http.UriTemplate;
12 import com.google.api.client.json.JsonFactory;
13 import com.google.api.client.json.jackson2.JacksonFactory;
14 import com.google.api.client.util.ExponentialBackOff;
15 import com.google.api.client.util.Maps;
16 import com.google.api.services.discovery.Discovery;
17 import com.google.api.services.discovery.model.JsonSchema;
18 import com.google.api.services.discovery.model.RestDescription;
19 import com.google.api.services.discovery.model.RestMethod;
20 import com.google.api.services.discovery.model.RestMethod.Request;
21 import com.google.api.services.discovery.model.RestResource;
22
23 import java.math.BigDecimal;
24 import java.math.BigInteger;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31
32 import org.apache.log4j.Logger;
33 import org.json.simple.JSONArray;
34 import org.json.simple.JSONObject;
35
36 /**
37  * This class provides a java SDK interface to Arvados API server.
38  *
39  * Please refer to http://doc.arvados.org/api/ to learn about the
40  *  various resources and methods exposed by the API server.
41  *
42  * @author radhika
43  */
44 public class Arvados {
45   // HttpTransport and JsonFactory are thread-safe. So, use global instances.
46   private HttpTransport httpTransport;
47   private final JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
48
49   private String arvadosApiToken;
50   private String arvadosApiHost;
51   private boolean arvadosApiHostInsecure;
52
53   private String arvadosRootUrl;
54
55   private static final Logger logger = Logger.getLogger(Arvados.class);
56
57   // Get it once and reuse on the call requests
58   RestDescription restDescription = null;
59   String apiName = null;
60   String apiVersion = null;
61
62   public Arvados (String apiName, String apiVersion) throws Exception {
63     this (apiName, apiVersion, null, null, null);
64   }
65
66   public Arvados (String apiName, String apiVersion, String token,
67       String host, String hostInsecure) throws Exception {
68     this.apiName = apiName;
69     this.apiVersion = apiVersion;
70
71     // Read needed environmental variables if they are not passed
72     if (token != null) {
73       arvadosApiToken = token;
74     } else {
75       arvadosApiToken = System.getenv().get("ARVADOS_API_TOKEN");
76       if (arvadosApiToken == null) {
77         throw new Exception("Missing environment variable: ARVADOS_API_TOKEN");
78       }
79     }
80
81     if (host != null) {
82       arvadosApiHost = host;
83     } else {
84       arvadosApiHost = System.getenv().get("ARVADOS_API_HOST");
85       if (arvadosApiHost == null) {
86         throw new Exception("Missing environment variable: ARVADOS_API_HOST");
87       }
88     }
89     arvadosRootUrl = "https://" + arvadosApiHost;
90     arvadosRootUrl += (arvadosApiHost.endsWith("/")) ? "" : "/";
91
92     if (hostInsecure != null) {
93       arvadosApiHostInsecure = Boolean.valueOf(hostInsecure);
94     } else {
95       arvadosApiHostInsecure =
96           "true".equals(System.getenv().get("ARVADOS_API_HOST_INSECURE")) ? true : false;
97     }
98
99     // Create HTTP_TRANSPORT object
100     NetHttpTransport.Builder builder = new NetHttpTransport.Builder();
101     if (arvadosApiHostInsecure) {
102       builder.doNotValidateCertificate();
103     }
104     httpTransport = builder.build();
105
106     // initialize rest description
107     restDescription = loadArvadosApi();
108   }
109
110   /**
111    * Make a call to API server with the provide call information.
112    * @param resourceName
113    * @param methodName
114    * @param paramsMap
115    * @return Map
116    * @throws Exception
117    */
118   public Map call(String resourceName, String methodName,
119       Map<String, Object> paramsMap) throws Exception {
120     RestMethod method = getMatchingMethod(resourceName, methodName);
121
122     HashMap<String, Object> parameters = loadParameters(paramsMap, method);
123
124     GenericUrl url = new GenericUrl(UriTemplate.expand(
125         arvadosRootUrl + restDescription.getBasePath() + method.getPath(),
126         parameters, true));
127
128     try {
129       // construct the request
130       HttpRequestFactory requestFactory;
131       requestFactory = httpTransport.createRequestFactory();
132
133       // possibly required content
134       HttpContent content = null;
135
136       if (!method.getHttpMethod().equals("GET") &&
137           !method.getHttpMethod().equals("DELETE")) {
138         String objectName = resourceName.substring(0, resourceName.length()-1);
139         Object requestBody = paramsMap.get(objectName);
140         if (requestBody == null) {
141           error("POST method requires content object " + objectName);
142         }
143
144         content = new ByteArrayContent("application/json",((String)requestBody).getBytes());
145       }
146
147       HttpRequest request =
148           requestFactory.buildRequest(method.getHttpMethod(), url, content);
149
150       // Set read timeout to 120 seconds (up from default of 20 seconds)
151       request.setReadTimeout(120 * 1000);
152
153       // Add retry behavior
154       request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(new ExponentialBackOff()));
155
156       // make the request
157       List<String> authHeader = new ArrayList<String>();
158       authHeader.add("OAuth2 " + arvadosApiToken);
159       request.getHeaders().put("Authorization", authHeader);
160       String response = request.execute().parseAsString();
161
162       Map responseMap = jsonFactory.createJsonParser(response).parse(HashMap.class);
163
164       logger.debug(responseMap);
165
166       return responseMap;
167     } catch (Exception e) {
168       e.printStackTrace();
169       throw e;
170     }
171   }
172
173   /**
174    * Get all supported resources by the API
175    * @return Set
176    */
177   public Set<String> getAvailableResourses() {
178     return (restDescription.getResources().keySet());
179   }
180
181   /**
182    * Get all supported method names for the given resource
183    * @param resourceName
184    * @return Set
185    * @throws Exception
186    */
187   public Set<String> getAvailableMethodsForResourse(String resourceName)
188       throws Exception {
189     Map<String, RestMethod> methodMap = getMatchingMethodMap (resourceName);
190     return (methodMap.keySet());
191   }
192
193   /**
194    * Get the parameters for the method in the resource sought.
195    * @param resourceName
196    * @param methodName
197    * @return Set
198    * @throws Exception
199    */
200   public Map<String,List<String>> getAvailableParametersForMethod(String resourceName, String methodName)
201       throws Exception {
202     RestMethod method = getMatchingMethod(resourceName, methodName);
203     Map<String, List<String>> parameters = new HashMap<String, List<String>>();
204     List<String> requiredParameters = new ArrayList<String>();
205     List<String> optionalParameters = new ArrayList<String>();
206     parameters.put ("required", requiredParameters);
207     parameters.put("optional", optionalParameters);
208
209     try {
210       // get any request parameters
211       Request request = method.getRequest();
212       if (request != null) {
213         Object required = request.get("required");
214         Object requestProperties = request.get("properties");
215         if (requestProperties != null) {
216           if (requestProperties instanceof Map) {
217             Map properties = (Map)requestProperties;
218             Set<String> propertyKeys = properties.keySet();
219             for (String property : propertyKeys) {
220               if (Boolean.TRUE.equals(required)) {
221                 requiredParameters.add(property);
222               } else {
223                 optionalParameters.add(property);
224               }
225             }
226           }
227         }
228       }
229
230       // get other listed parameters
231       Map<String,JsonSchema> methodParameters = method.getParameters();
232       for (Map.Entry<String, JsonSchema> entry : methodParameters.entrySet()) {
233         if (Boolean.TRUE.equals(entry.getValue().getRequired())) {
234           requiredParameters.add(entry.getKey());
235         } else {
236           optionalParameters.add(entry.getKey());
237         }
238       }
239     } catch (Exception e){
240       logger.error(e);
241     }
242
243     return parameters;
244   }
245
246   private HashMap<String, Object> loadParameters(Map<String, Object> paramsMap,
247       RestMethod method) throws Exception {
248     HashMap<String, Object> parameters = Maps.newHashMap();
249
250     // required parameters
251     if (method.getParameterOrder() != null) {
252       for (String parameterName : method.getParameterOrder()) {
253         JsonSchema parameter = method.getParameters().get(parameterName);
254         if (Boolean.TRUE.equals(parameter.getRequired())) {
255           Object parameterValue = paramsMap.get(parameterName);
256           if (parameterValue == null) {
257             error("missing required parameter: " + parameter);
258           } else {
259             putParameter(null, parameters, parameterName, parameter, parameterValue);
260           }
261         }
262       }
263     }
264
265     for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
266       String parameterName = entry.getKey();
267       Object parameterValue = entry.getValue();
268
269       if (parameterName.equals("contentType")) {
270         if (method.getHttpMethod().equals("GET") || method.getHttpMethod().equals("DELETE")) {
271           error("HTTP content type cannot be specified for this method: " + parameterName);
272         }
273       } else {
274         JsonSchema parameter = null;
275         if (restDescription.getParameters() != null) {
276           parameter = restDescription.getParameters().get(parameterName);
277         }
278         if (parameter == null && method.getParameters() != null) {
279           parameter = method.getParameters().get(parameterName);
280         }
281         putParameter(parameterName, parameters, parameterName, parameter, parameterValue);
282       }
283     }
284
285     return parameters;
286   }
287
288   private RestMethod getMatchingMethod(String resourceName, String methodName)
289       throws Exception {
290     Map<String, RestMethod> methodMap = getMatchingMethodMap(resourceName);
291
292     if (methodName == null) {
293       error("missing method name");
294     }
295
296     RestMethod method =
297         methodMap == null ? null : methodMap.get(methodName);
298     if (method == null) {
299       error("method not found: ");
300     }
301
302     return method;
303   }
304
305   private Map<String, RestMethod> getMatchingMethodMap(String resourceName)
306       throws Exception {
307     if (resourceName == null) {
308       error("missing resource name");
309     }
310
311     Map<String, RestMethod> methodMap = null;
312     Map<String, RestResource> resources = restDescription.getResources();
313     RestResource resource = resources.get(resourceName);
314     if (resource == null) {
315       error("resource not found");
316     }
317     methodMap = resource.getMethods();
318     return methodMap;
319   }
320
321   /**
322    * Not thread-safe. So, create for each request.
323    * @param apiName
324    * @param apiVersion
325    * @return
326    * @throws Exception
327    */
328   private RestDescription loadArvadosApi()
329       throws Exception {
330     try {
331       Discovery discovery;
332
333       Discovery.Builder discoveryBuilder =
334           new Discovery.Builder(httpTransport, jsonFactory, null);
335
336       discoveryBuilder.setRootUrl(arvadosRootUrl);
337       discoveryBuilder.setApplicationName(apiName);
338
339       discovery = discoveryBuilder.build();
340
341       return discovery.apis().getRest(apiName, apiVersion).execute();
342     } catch (Exception e) {
343       e.printStackTrace();
344       throw e;
345     }
346   }
347
348   /**
349    * Convert the input parameter into its equivalent json string.
350    * Add this json string value to the parameters map to be sent to server.
351    * @param argName
352    * @param parameters
353    * @param parameterName
354    * @param parameter
355    * @param parameterValue
356    * @throws Exception
357    */
358   private void putParameter(String argName, Map<String, Object> parameters,
359       String parameterName, JsonSchema parameter, Object parameterValue)
360           throws Exception {
361     Object value = parameterValue;
362     if (parameter != null) {
363       if ("boolean".equals(parameter.getType())) {
364         value = Boolean.valueOf(parameterValue.toString());
365       } else if ("number".equals(parameter.getType())) {
366         value = new BigDecimal(parameterValue.toString());
367       } else if ("integer".equals(parameter.getType())) {
368         value = new BigInteger(parameterValue.toString());
369       } else if ("float".equals(parameter.getType())) {
370         value = new BigDecimal(parameterValue.toString());
371       } else if ("Java.util.Calendar".equals(parameter.getType())) {
372         value = new BigDecimal(parameterValue.toString());
373       } else if (("array".equals(parameter.getType())) ||
374           ("Array".equals(parameter.getType()))) {
375         if (parameterValue.getClass().isArray()){
376           value = getJsonValueFromArrayType(parameterValue);
377         } else if (List.class.isAssignableFrom(parameterValue.getClass())) {
378           value = getJsonValueFromListType(parameterValue);
379         }
380       } else if (("Hash".equals(parameter.getType())) ||
381           ("hash".equals(parameter.getType()))) {
382         value = getJsonValueFromMapType(parameterValue);
383       } else {
384         if (parameterValue.getClass().isArray()){
385           value = getJsonValueFromArrayType(parameterValue);
386         } else if (List.class.isAssignableFrom(parameterValue.getClass())) {
387           value = getJsonValueFromListType(parameterValue);
388         } else if (Map.class.isAssignableFrom(parameterValue.getClass())) {
389           value = getJsonValueFromMapType(parameterValue);
390         }
391       }
392     }
393
394     parameters.put(parameterName, value);
395   }
396
397   /**
398    * Convert the given input array into json string before sending to server.
399    * @param parameterValue
400    * @return
401    */
402   private String getJsonValueFromArrayType (Object parameterValue) {
403     String arrayStr = Arrays.deepToString((Object[])parameterValue);
404
405     // we can expect either an array of array objects or an array of objects
406     if (arrayStr.startsWith("[[") && arrayStr.endsWith("]]")) {
407       Object[][] array = new Object[1][];
408       arrayStr = arrayStr.substring(2, arrayStr.length()-2);
409       String jsonStr = getJsonStringForArrayStr(arrayStr);
410       String value = "[" + jsonStr + "]";
411       return value;
412     } else {
413       arrayStr = arrayStr.substring(1, arrayStr.length()-1);
414       return (getJsonStringForArrayStr(arrayStr));
415     }
416   }
417
418   private String getJsonStringForArrayStr(String arrayStr) {
419     Object[] array = arrayStr.split(",");
420     Object[] trimmedArray = new Object[array.length];
421     for (int i=0; i<array.length; i++){
422       trimmedArray[i] = array[i].toString().trim();
423     }
424     String value = JSONArray.toJSONString(Arrays.asList(trimmedArray));
425     return value;
426   }
427
428   /**
429    * Convert the given input List into json string before sending to server.
430    * @param parameterValue
431    * @return
432    */
433   private String getJsonValueFromListType (Object parameterValue) {
434     List paramList = (List)parameterValue;
435     Object[] array = new Object[paramList.size()];
436     Arrays.deepToString(paramList.toArray(array));
437     return (getJsonValueFromArrayType(array));
438   }
439
440   /**
441    * Convert the given input map into json string before sending to server.
442    * @param parameterValue
443    * @return
444    */
445   private String getJsonValueFromMapType (Object parameterValue) {
446     JSONObject json = new JSONObject((Map)parameterValue);
447     return json.toString();
448   }
449
450   private static void error(String detail) throws Exception {
451     String errorDetail = "ERROR: " + detail;
452
453     logger.debug(errorDetail);
454     throw new Exception(errorDetail);
455   }
456
457   public static void main(String[] args){
458     System.out.println("Welcome to Arvados Java SDK.");
459     System.out.println("Please refer to http://doc.arvados.org/sdk/java/index.html to get started with the the SDK.");
460   }
461
462 }