Introduction
This article is the Java part of a series whose main article is How to build a language binding for a web API which you should read before this one to get general background information about implementing a language binding.
If you are interested in the .NET implementation you can refer to the dedicated article: How to build a .Net binding in C# for a web API.
This article will give you more concrete information, mainly source code, if you need to implement a web API binding in Java.
When applicable each section of this article references a section of the main article to give you more context before diving into the Java implementation details.
The different sections are not directly related so you can read them in a random order, and particularly if you’re an experienced .Net developer some sections, e.g. about object-oriented programming, may bore you, so feel free to skip them completely.
The open source code of the project is available on GitHub at CometDocs.
Design
For an overview of the design of the Cometdocs binding see the “Design” section of the main article.
The client class
For an overview of the design of the client class see the “The client class” section of the main article.
First here is the Java interface that reifies the functional interface of a Cometdocs client:
public interface Client
{
public AuthenticationToken authenticate(String username, String password, String key) throws Exception;
public AuthenticationToken authenticate(String username, String password, String key, Integer validity) throws Exception;
public FileInfo convertFile(AuthenticationToken token, FileInfo file, ConversionType conversion) throws Exception;
public FileInfo convertFile(AuthenticationToken token, FileInfo file,
ConversionType conversionType, Integer timeout) throws Exception;
public void createAccount(String name, String email, String password) throws Exception;
public FolderInfo createFolder(AuthenticationToken token, FolderInfo parent, String name) throws Exception;
public void deleteFile(AuthenticationToken token, FileInfo file) throws Exception;
public void deleteFile(AuthenticationToken token, FileInfo file, Boolean deleteRevisions) throws Exception;
public void deleteFolder(AuthenticationToken token, FolderInfo folder) throws Exception;
public File downloadFile(AuthenticationToken token, FileInfo file) throws Exception;
public Notification[] getNotifications(AuthenticationToken token) throws Exception;
public void invalidateToken(AuthenticationToken token) throws Exception;
public Category[] getCategories() throws Exception;
public Conversion[] getConversions(AuthenticationToken token) throws Exception;
public Conversion[] getConversions(AuthenticationToken token, FileInfo file) throws Exception;
public ConversionStatus getConversionStatus(AuthenticationToken token,
FileInfo file, ConversionType conversion) throws Exception;
public ConversionType[] getConversionTypes() throws Exception;
public Folder getFolder(AuthenticationToken token) throws Exception;
public Folder getFolder(AuthenticationToken token, FolderInfo folder) throws Exception;
public Folder getFolder(AuthenticationToken token, Boolean recursive) throws Exception;
public Folder getFolder(AuthenticationToken token, FolderInfo folder, Boolean recursive) throws Exception;
public String[] getMethods() throws Exception;
public FileInfo[] getPublicFiles(AuthenticationToken token) throws Exception;
public FileInfo[] getPublicFiles(AuthenticationToken token, Category category) throws Exception;
public FileInfo[] getSharedFiles(AuthenticationToken token) throws Exception;
public void refreshToken(AuthenticationToken token) throws Exception;
public void refreshToken(AuthenticationToken token, Integer validity) throws Exception;
public void sendFile(AuthenticationToken token, FileInfo file, String[] recipients) throws Exception;
public void sendFile(AuthenticationToken token, FileInfo file, String[] recipients, String message) throws Exception;
public void sendFile(AuthenticationToken token, FileInfo file,
String[] recipients, String sender, String message) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, InputStream file, String name) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, InputStream file, String name, Long folderId) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, String file) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, String file, Long folderId) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, File file) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, File file, Long folderId) throws Exception;
public FileInfo uploadFile(AuthenticationToken token, File file, FolderInfo folder) throws Exception;
public FileInfo uploadFileFromUrl(AuthenticationToken token, String url) throws Exception;
public FileInfo uploadFileFromUrl(AuthenticationToken token, String url, String name) throws Exception;
public FileInfo uploadFileFromUrl(AuthenticationToken token, String url, Long folderId) throws Exception;
public FileInfo uploadFileFromUrl(AuthenticationToken token, String url, FolderInfo folder) throws Exception;
public FileInfo uploadFileFromUrl(AuthenticationToken token, String url, String name, Long folderId) throws Exception;
}
Quite a bunch of methods to implement!
Most of the overloads are needed to map the Cometdocs API optional parameters; as an example when you authenticate you can provide a validity period for the generated session token.
The client factory is as simple as it can be:
public class ClientFactory
{
public static Client getClient()
{
return new ClientImpl();
}
}
The authentication token class
Here is the wrapper used to represent the authentication token:
public class AuthenticationToken
{
private String value;
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
public AuthenticationToken(String value)
{
this.value = value;
}
}
This is a minimal illustration of object-oriented abstraction.
The files and folders types
And finally here are the different classes that carry the basic info about a file FileInfo
:
public class FileInfo
{
private long id;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
private String extension;
public String getExtension() { return extension; }
public void setExtension(String extension) { this.extension = extension; }
private long size;
public long getSize() { return size; }
public void setSize(long size) { this.size = size; }
private Boolean hasConversions;
public Boolean hasConversions() { return hasConversions; }
public void hasConversions(Boolean hasConversions) { this.hasConversions = hasConversions; }
public FileInfo()
{
this(null);
}
public FileInfo(String nameWithExtension)
{
if (nameWithExtension != null)
{
String[] tokens = nameWithExtension.split("\\.");
String name;
if (tokens.length >= 2)
{
name = tokens[0];
for (int i = 1; i < tokens.length - 1; ++i)
{
name += "." + tokens[i];
}
setExtension(tokens[tokens.length - 1]);
}
else
{
name = tokens[0];
}
setName(name);
}
}
@Override
public int hashCode()
{
return (int)id;
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof FileInfo)) return false;
FileInfo other = (FileInfo)obj;
return other != null && other.id == this.id;
}
}
and about a folder, FolderInfo
:
public class FolderInfo
{
private long id;
public long getID() { return id; }
public void setID(long id) { this.id = id; }
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
For FileInfo
I’ve implemented
hashCode
and
equals
to be able to compare two instances based on their internal Cometdocs ID.
File
is the class that represents a file with some content:
public class File extends FileInfo
{{
private byte[] content;
public byte[] getContent() { return content; }
public void setContent(byte[] content)
{
if (content == null)
throw new NullPointerException(
"Content cannot be null; for empty files provide " +
"an empty array or do not provide anything!");
this.content = content;
}
public File()
{
super();
content = new byte[0];
}
public File(String nameWithExtension)
{
super(nameWithExtension);
content = new byte[0];
}
}
And Folder
a folder with some files and sub-folders:
public class Folder extends FolderInfo
{
private Folder[] folders;
public Folder[] getFolders() { return folders; }
public void setFolders(Folder[] folders) { this.folders = folders; }
private FileInfo[] files;
public FileInfo[] getFiles() { return files; }
public void setFiles(FileInfo[] files) { this.files = files; }
}
The web/HTTP client
For general information about what is an HTTP client and how it fits into the building of a web API binding have a look at the dedicated section in the main article.
The Apache HttpClient
There is a bad and a good news…
The bad news is Java does not come with a native HTTP client, so you have to use a third-party component.
The good news is there exist at least two good implementations: the Apache HttpClient, a part of the HttpComponents project and the Google HTTP Client
Library for Java. I’ve chosen the first one because the Apache foundation projects are high-quality (from my own experience), sometimes serve as the anteroom for the standard Java implementation and I’ve already use it a little.
The Apache HttpClient is relatively well documented but you have to take care of referring to the correct version of the documentation as there is more than one version of this component.
The abstraction it uses is a set of requests and responses that both carry some “entity”, i.e. the payload: for the Cometdocs API we exclusively use HTTP/POST request whose entity is more often than not a set of parameters, and you get back a response whose entity is a bunch of JSON.
A typical example is the authentication, whose method source code is:
public AuthenticationToken authenticate(String username,
String password, String key, Integer validity) throws Exception
{
List<NameValuePair> params = new ArrayList<NameValuePair>(4);
params.add(new BasicNameValuePair("username", username));
params.add(new BasicNameValuePair("password", password));
params.add(new BasicNameValuePair("clientKey", key));
if (validity != null)
{
params.add(new BasicNameValuePair("validity", validity.toString()));
}
HttpPost post = new HttpPost(APIRoot + "authenticate");
post.setEntity(new UrlEncodedFormEntity(params));
HttpResponse httpResponse = httpClient.execute(post);
String json = EntityUtils.toString(httpResponse.getEntity());
AuthenticateResponse response = gson.fromJson(json, AuthenticateResponse.class);
checkAndThrow(response);
return new AuthenticationToken(response.getToken());
}
File upload
The upload of files is special because you need to send a composite request: the first parts are made of the parameters, as for the other requests, but the last part is made of the binary content of the file.
The good news is that on the Java side the HttpClient does most of the work and all you have to do is give it the sub-payloads and it will take care of building the multipart HTTP request.
Here is the Java source code for the uploadFile
method:
public FileInfo uploadFile(AuthenticationToken token,
InputStream file, String name, Long folderId) throws Exception
{
byte[] fileContent = getBytes(file);
HttpPost post = new HttpPost(APIRoot + "uploadFile");
MultipartEntity entity = new MultipartEntity();
entity.addPart("token", new StringBody(token.getValue()));
if (folderId != null)
{
entity.addPart("folderId", new StringBody(folderId.toString()));
}
entity.addPart("file", new ByteArrayBody(fileContent, name));
post.setEntity(entity);
HttpResponse httpResponse = httpClient.execute(post);
String json = EntityUtils.toString(httpResponse.getEntity());
UploadFileResponse response = gson.fromJson(json, UploadFileResponse.class);
checkAndThrow(response);
return response.getFile();
}
Thanks to the abstraction provided by the HttpClient the code is really similar to the simpler form POST case.
JSON data binding
For a quick introduction to data binding and how it is applied to the Cometdocs binding see the “Data binding” section of the main article.
Gson
In the java world there is more than one good JSON libraries like Jackson or Gson.
I’ve chosen GSON for the same reason I’ve chosen the HttpClient: a strong trust in Google stuff.
And I was not disappointed: it’s almost as good as my preferred JSON library, Json.NET, it just requires a little more work, like to convert numeric booleans expressed with 0 and 1.
Here is an illustration of its typical usage in our Cometdocs binding: when we authenticate to the Cometdocs API we get back a JSON response similar to this:
{"token":"b687500d-3cff-44b7-ad14-a241a7edabf1","status":0,"message":"OK"}
Gson maps this JSON representation to a Java object:
Deserialized AuthenticateResponse
whose class is AuthenticateResponse
:
class Response
{
private Status status;
public Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
private String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
private String error;
public String getError() { return error; }
public void setError(String error) { this.error = error; }
}
class AuthenticateResponse extends Response
{
private String token;
public String getToken() { return token; }
}
This is all good for most of the mapping, but sometimes you have to help Gson be it to map numerical booleans, enums values or files conversions.
For a complete introduction to Gson you can read Java/JSON mapping with Gson.
Mapping numerical booleans
The Cometdocs API uses integers, 0 and 1, to represent boolean values, but Gson is not natively able to map this representation to a Java boolean type.
As we only convert JSON to Java we only need to write a JsonDeserializer:
class BooleanTypeAdapter implements JsonDeserializer<Boolean>
{
public Boolean deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException
{
int code = json.getAsInt();
return code == 0 ? false :
code == 1 ? true :
null;
}
}
We make Gson aware of this by registering the deserializer:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Boolean.class, new BooleanTypeAdapter());
gson = builder.create();
From now on each time GSON encounters a field in the JSON document mapped to a Boolean, it will call our deserializer for doing the job.
Mapping statuses
The Cometdocs API responses carry a status code that indicates if the request is successful (code 0) or has failed (code > 0).
As an example if you send invalid credentials the response will have status 6,
BadCredentials
. From the Java side the list of statuses is represented using an enum named
Status
:
enum Status
{
OK,
InternalError,
UnsupportedAPIMethod,
MethodInvocationError,
HttpsRequired,
InvalidToken,
BadParameters
...
}
So to map between the numerical status codes and the Java enum we once again have to define a custom deserializer:
class StatusTypeAdapter implements JsonDeserializer<Status>
{
public Status deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException
{
int code = json.getAsInt();
Status[] values = Status.values();
return code < values.length ? values[ code] : null;
}
}
Mapping conversion types
The Cometdocs API represents the files conversions in a compacted text format: e.g. to represent a conversion from PDF to XLS instead of using a composite object like:
{from: "PDF", to: "XLS"}
it uses a simple string:
"PDF2XLS"
Here is the ConversionTypeAdapter
class, responsible for filling the gap between the Cometdocs
text representation and the Java ConversionType
class created for the binding:
class ConversionTypeAdapter implements JsonDeserializer<ConversionType>
{
public ConversionType deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException
{
String type = json.getAsString();
String[] tokens = type.split("2");
return new ConversionType(tokens[0], tokens[1]);
}
}
Security
For an overview of the security issues associated with the implementation of a web API binding have a look at the “Security” section in the main article.
The communication with the Cometdocs API is protected using HTTPS.
It means we have to register the Cometdocs security certificate in some way to allow the Apache HttpClient to interact with it.
In Java you have to do some work, even on Windows where the certification authority of the certificate should be trusted, because Java, more precisely the JRE, uses its own certificates stores, so you have to retrieve and provide the JRE a copy of the Cometdocs certificate.
Otherwise you’ll end up with an exception like:
javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
at sun.security.ssl.SSLSessionImpl.getPeerCertificates(Unknown Source)
...
See the main article “Security” section for the general procedure to export the certificate, from here I assume you have saved it, e.g. as a “CER” file, somewhere on your local file-system.
Here is the process to make your JRE aware of your trust:
- first you should double check the version of the JRE running your code to avoid working on a wrong store (yes I did it once ), e.g. by looking at the environment variable
JAVA_HOME
. - once you know which JRE is used locate its store, it should be in:
lib/security/cacerts
this path is relative to the location of your JRE.
- the default password for your store if you’ve never changed it is “changeit”
- add the Cometdocs certificates you downloaded to the store with the “importcert” command:
keytool -keystore "C:\opt\Java\jdk1.7.0_09\jre\lib\security\cacerts"
-importcert -alias cometdocs -file "C:\Users\pragmateek\Desktop\cometdocs.cer"
- to check that the certificate has correctly been added to the store you can use the “list” command:
keytool -list -keystore "C:\opt\Java\jdk1.7.0_09\jre\lib\security\cacerts"
this will print a bunch of text so you should filter it with a tool like “grep” to ease the check, here is an example using Cygwin:
$ keytool -list -keystore "C:\opt\Java\jdk1.7.0_09\jre\lib\security\cacerts" | grep -i cometdocs
Enter keystore password: changeit
cometdocs, 1 juin 2013, trustedCertEntry,
The keytool.exe tool is bundled with the JRE in the bin folder. If your Java setup is correct, your PATH environment variable is correctly set,
then you should be able to invoke keytool.exe directly as in the above command lines. If this is not the case you’ll have to use its absolute path,
e.g., C:\opt\Java\jdk1.7.0_09\jre\bin\keytool.exe.
Errors management
For general information on the way errors are managed see
Errors management in the main article.
Here is the Java class that is the representation of a Cometdocs API error:
public class CometDocsException extends Exception
{
private String message;
public String getMessage(){ return this.message; }
public void setStatus(String message){ this.message = message; }
private Status status;
public Status getStatus(){ return this.status; }
public void setStatus(Status status){ this.status = status; }
private String error;
public String getError(){ return this.error; }
public void setError(String error){ this.error = error; }
public CometDocsException(String message, Status status, String error)
{
super(String.format("%s (%s) [%s]", message, error, status));
this.message = message;
this.status = status;
this.error = error;
}
}
And here is a specialization (currently the sole) for the “invalid token” error:
public class InvalidTokenException extends CometDocsException
{
public InvalidTokenException(String message, Status status, String error)
{
super(message, status, error);
}
}
And finally the checkAndThrow
method that is called right after receiving a response from the Cometdocs API:
private void checkAndThrow(Response response) throws Exception
{
if (response.getStatus() != Status.OK)
{
if (response.getStatus() == Status.InvalidToken)
{
throw new InvalidTokenException(response.getMessage(),
response.getStatus(), response.getError());
}
throw new CometDocsException(response.getMessage(),
response.getStatus(), response.getError());
}
}
Testing
For more general information about the testing of the bindings have a look at the “Testing” section in the main article.
Tools
For testing I’ve used the vanilla solution: JUnit, and benefited from its perfect integration in the Eclipse IDE.
For those not familiar with JUnit, here is how you can create a set of unit-tests:
- create a class that will hold all the unit-tests, in our case “ClientTests“
- create a method for each unit-test and annotate it with the “Test” annotation
As an example here is the unit-test for checking that we can retrieve all the Cometdocs categories:
public class ClientTests
{
...
@Test
public void canGetCategories() throws Exception
{
Category[] categories = client.getCategories();
assertTrue(categories.length > 10);
boolean hasArt = false;
boolean hasBusiness = false;
boolean hasUnicorns = false;
for (Category cat : categories)
{
hasArt |= cat.getName().equals("Art");
hasBusiness |= cat.getName().equals("Business");
hasUnicorns |= cat.getName().equals("Unicorns");
}
assertTrue(hasArt);
assertTrue(hasBusiness);
assertFalse(hasUnicorns);
}
...
}
And here is what we get when all the tests are OK:
Unit tests OK
(No Photoshopping I promise
)
If you’re using NetBeans rather than Eclipse you can of course use JUnit too.
Test fixture setup
In JUnit you define a test fixture setup by marking a static method with the BeforeClass annotation.
Here is the Java source code of the fixture setup:
@BeforeClass
public static void testFixtureSetUp() throws Exception
{
readCredentials();
client = ClientFactory.getClient();
canAuthenticate();
Folder root = client.getFolder(authToken);
for (FolderInfo f : root.getFolders())
{
if (f.getName().equals(testFolderName))
{
testFolderInfo = f;
break;
}
}
if (testFolderInfo == null)
{
throw new Exception(String.format("Unable to find tests " +
"folder '%s'!\nPlease create it first.", testFolderName));
}
}
Reflection
In Java the entry point to the reflection API is the Class class, which represents a class.
It exposes all the methods necessary to explore the properties of a class like its methods using the
getDeclaredMethods
method which returns an array of Method, a class whose instances represent a single method; the Method class itself has a
getName
method which returns the name of the method it represents.
There is one subtlety: we don’t want to get all the methods of the Client type because we’re only interested in the public instance methods, not the implementation details which are private like the
checkAndThrow
method.
So we filter them based on their “modifiers” that are flags that indicate the characteristics of each method: instance or static, abstract or concrete, final or overridable, the visibility (public, package, protected, private).
We get the modifiers using dedicated methods like <a title="Modifier.isStatic Javadoc" href="http://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Modifier.html#isStatic(int)" target="_blank">Modifier.isStatic</a>
and
Modifier.isPublic
.
Moreover we only need the methods defined by the Client class itself, not those inherited from the
Object
base class like toString
.
It’s why we use the
getDeclaredMethods
method instead of
getMethods
.
Here is the complete code of the unit-test:
@Test
public void canGetMethods() throws Exception
{
String[] methods = client.getMethods();
Set<String> methodsName = new HashSet<String>();
for (String method : methods)
{
methodsName.add(method.split("\\(")[0]);
}
Set<String> clientMethods = new HashSet<String>();
for (Method method : Client.class.getDeclaredMethods())
{
if (!Modifier.isStatic(method.getModifiers()) &&
Modifier.isPublic(method.getModifiers()))
{
clientMethods.add(method.getName());
}
}
assertTrue(methodsName.size() >= 18);
assertTrue(clientMethods.equals(methodsName));
return;
}
HashSets are used for two reasons:
- they don’t store duplicates, so if there is more than one overload for a method, like
uploadFile
, the set will store the name once - two sets can be easily compared for equality based on their elements
Result
To illustrate how the language binding can be used to simply store and retrieve a file here is the JUnit
unit-test used to test the uploadFile
and downloadFile
methods:
@Test
public void canUploadAndDownloadAFile() throws Exception
{
assertClean();
String content = "The peanut, or groundnut (Arachis hypogaea), is a species in the " +
"legume or \"bean\" family (Fabaceae). The peanut was probably first domesticated " +
"and cultivated in the valleys of Paraguay.[1] It is an annual herbaceous plant growing" +
" 30 to 50 cm (1.0 to 1.6 ft) tall. The leaves are opposite, pinnate with four leaflets" +
" (two opposite pairs; no terminal leaflet), each leaflet is 1 to 7 cm (⅜ to 2¾ in)" +
" long and 1 to 3 cm (⅜ to 1 inch) broad.\n" +
"The flowers are a typical peaflower in shape, 2 to 4 cm (0.8 to 1.6 in)" +
" (¾ to 1½ in) across, yellow with reddish veining. Hypogaea means \"under" +
" the earth\"; after pollination, the flower stalk elongates causing it to bend" +
" until the ovary touches the ground. Continued stalk growth then pushes the ovary underground" +
" where the mature fruit develops into a legume pod, the peanut – a classical example" +
" of geocarpy. Pods are 3 to 7 cm (1.2 to 2.8 in) long, containing 1 to 4 seeds.[2]\n" +
"Peanuts are known by many other local names such as earthnuts, ground nuts, goober" +
" peas, monkey nuts, pygmy nuts and pig nuts.[3] Despite its name and appearance," +
" the peanut is not a nut, but rather a legume.";
byte[] contentBytes = content.getBytes(charset);
File inputFile = new File("Peanuts.txt");
inputFile.setContent(contentBytes);
FileInfo info = client.uploadFile(authToken, inputFile, testFolderInfo);
assertEquals(contentBytes.length, info.getSize());
File outputFile = client.downloadFile(authToken, info);
String outputContent = new String(outputFile.getContent(), charset);
assertEquals(content, outputContent);
}
If you drop all the assertions you can see that this non trivial task is made relatively straightforward.
I let you judge whether the result is worth the trouble…
Conclusion
As you see combining the Apache HTTP client, Gson, JUnit, JRE magic, reflection and some background on web development is enough to develop a Java binding for virtually any web API.
If you want more precision on one of the sections of this article or on a part of the Java source code not detailed in this article do not hesitate to ask by letting a comment, I’ll update the article accordingly.
If you have already developed a Java binding or expect to build one please share your experience and suggestions, I’d like to hear from you.