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