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