Loklak server provides a large API to play with the data scraped by it. Methods in java can be implemented to use these API endpoints. A common approach of implementing the methods for using API endpoints is to create the request URL by taking the values passed to the method, and then send GET/POST request. Creating the request URL in every method can be tiresome and in the long run maintaining the library if implemented this way will require a lot of effort. For example, assume a method is to be implemented for suggest API endpoint, which has many parameters, for creating request URL a lot of conditionals needs to be written – whether a parameter is provided or not.
Well, the methods to call API endpoints can be implemented with lesser and easy to maintain code using Reflection in Java. The post ahead elaborates the problem, the approach to solve the problem and finally solution which is implemented in loklak_jlib_api.
Let’s say, the status API endpoint needs to be implemented, a simple approach can be:
public class LoklakAPI { public static String status(String baseUrl) { String requestUrl = baseUrl "/api/status.json"; // GET request using requestUrl } public static void main(String[] argv) { JSONObject result = status("https://api.loklak.org"); } }
This one is easy, isn’t it, as status API endpoint requires no parameters. But just imagine if a method implements an API endpoint that has a lot of parameters, and most of them are optional parameters. As a developer, you would like to provide methods that cover all the parameters of the API endpoint. For example, how a method would look like if it implements suggest API endpoint, the old SuggestClient implementation in loklak_jlib_api does that:
public static ResultList<QueryEntry> suggest( final String hostServerUrl, final String query, final String source, final int count, final String order, final String orderBy, final int timezoneOffset, final String since, final String until, final String selectBy, final int random) throws JSONException, IOException { ResultList<QueryEntry> resultList = new ResultList<>(); String suggestApiUrl = hostServerUrl + SUGGEST_API + URLEncoder.encode(query.replace(' ', '+'), ENCODING) + PARAM_TIMEZONE_OFFSET + timezoneOffset + PARAM_COUNT + count + PARAM_SOURCE + (source == null ? PARAM_SOURCE_VALUE : source) + (order == null ? "" : (PARAM_ORDER + order)) + (orderBy == null ? "" : (PARAM_ORDER_BY + orderBy)) + (since == null ? "" : (PARAM_SINCE + since)) + (until == null ? "" : (PARAM_UNTIL + until)) + (selectBy == null ? "" : (PARAM_SELECT_BY + selectBy)) + (random < 0 ? "" : (PARAM_RANDOM + random)) + PARAM_MINIFIED + PARAM_MINIFIED_VALUE; // GET request using suggestApiUrl } }
A lot of conditionals!!! The targeted users may also get irritated if they need to provide all the parameters every time even if they don’t need them. The obvious solution to that is overloading the methods. But, then again for each overloaded method, the same repetitive conditionals need to be written, a form of code duplication!! And what if you have to implement some 30 API endpoints and in future maintaining them, a single thought of it is scary and a nightmare for the developers.
Approach to the problem
To reduce the code size, one obvious thought that comes is to implement static methods that send GET/POST requests provided request URL and the required data. These methods are implemented in loklak_jlib_api by JsonIO and NetworkIO.
You might have noticed the method name i.e. status, suggest is also the part of the request URL. So, the common pattern that we get is:
base_url + "/api/" + methodName + ".json" + "?" + firstParameterName + "=" + firstParameterValue + "&" + secondParameterName + "=" + secondParameterValue ...
Reflection API to rescue
Using Reflection API inspection of classes, interfaces, methods, and variables can be done, yes you guessed it right if we can inspect we can get the method and parameter names. It is also possible to implement interfaces and create objects at runtime.
So, an interface is created and API endpoint methods are defined in it. Using reflection API the interface is implemented lazily at runtime. LoklakAPI interface defines all the API endpoint methods. Some of the defined methods are:
public interface LoklakAPI { @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface GET {} @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface POST {} @GET JSONObject search(String q); @GET JSONObject search(String q, int count); @GET JSONObject search(String q, int timezoneOffset, String since, String until); @GET JSONObject search(String q, int timezoneOffset, String since, String until, int count); @GET JSONObject peers(); @GET JSONObject hello(); @GET JSONObject status(); @POST JSONObject push(JSONObject data); }
Here GET and POST annotations are used to mark the methods which use GET and POST request respectively, so that appropriate method for GET and POST static method can be used JsonIO – loadJson for GET request and pushJson for POST request.
A private static inner class ApiInvocationHandler is created in APIGenerator, which implements InvocationHandler – used for implementing interfaces at runtime.
private static class ApiInvocationHandler implements InvocationHandler { private String mBaseUrl; public ApiInvocationHandler(String baseUrl) { this.mBaseUrl = baseUrl; } @Override public Object invoke(Object o, Method method, Object[] values) throws Throwable { Parameter[] params = method.getParameters(); Object[] paramValues = values; /* format of annotation name: @packageWhereAnnotationIsDeclared$AnnotationName() Example: @org.loklak.client.LoklakAPI$GET() */ Annotation annotation = method.getAnnotations()[0]; String annotationName = annotation.toString().toLowerCase(); String apiUrl = createGetRequestUrl(mBaseUrl, method.getName(), params, paramValues); if (annotationName.contains("get")) { // GET REQUEST return loadJson(apiUrl); } else { // POST REQUEST JSONObject jsonObjectToPush = (JSONObject) paramValues[0]; String postRequestUrl = createPostRequestUrl(mBaseUrl, method.getName()); return pushJson(postRequestUrl, params[0].getName(), jsonObjectToPush); } } }
The base URL is provided while creating the object, in the constructor. The invoke method is called whenever a defined method in the interface is called in actual code. This way an interface is implemented at runtime. The parameters of invoke method:
- Object o – the object on which to call the method.
- Method method – method which is called, parameters and annotations of the method are obtained using getParameters and getAnnotations methods respectively which are required to create our request url.
- Object[] values – an array of parameter values which are passed to the method when calling it.
createGetRequestUrl is used to create the request URL for sending GET requests.
private static String createGetRequestUrl( String baseUrl, String methodName, Parameter[] params, Object[] paramValues) { String apiEndpointUrl = baseUrl.replaceAll("/+$", "") + "/api/" + methodName + ".json"; StringBuilder url = new StringBuilder(apiEndpointUrl); if (params.length > 0) { String queryParamAndVal = "?" + params[0].getName() + "=" + paramValues[0]; url.append(queryParamAndVal); for (int i = 1; i < params.length; i++) { String paramAndVal = "&" + params[i].getName() + "=" + String.valueOf(paramValues[i]); url.append(paramAndVal); } } return url.toString(); }
Similarly, createPostRequestUrl is used to create request URL for sending POST requests.
private static String createPostRequestUrl(String baseUrl, String methodName) { baseUrl = baseUrl.replaceAll("/+$", ""); return baseUrl + "/api/" + methodName + ".json"; }
Finally, Proxy.newProxyInstance is used to implement the interface at runtime. An instance of ApiInvocationHandler class is passed to the newProxyInstance method. This is all done by static method createApiMethods.
public static <T> T createApiMethods(Class<T> service, final String baseUrl) { ApiInvocationHandler apiInvocationHandler = new ApiInvocationHandler(baseUrl); return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[]{service}, apiInvocationHandler); }
Where:
- Class<T> service – is the interface where API endpoint methods are defined, here LoklakAPI.class.
- String baseUrl – web address of the server where Loklak server is hosted.
NOTE: For all this to work the library must be build using “-parameters” flag, else parameter names can’t be obtained. Since loklak_jlib_api uses maven, “-parameters” is provided in pom.xml file.
A small example:
String baseUrl = "https://api.loklak.org"; LoklakAPI loklakAPI = APIGenerator.createApiMethods(LoklakAPI.class, baseUrl); JSONObject searchResult = loklakAPI.search("FOSSAsia");