A client for the Havalo K,V store RESTful API.
Makes aggressive use of kolich-httpclient4-closure backed by the Apache Commons HttpClient 4.x. Also, uses Google's GSON library for all JSON related "stuph" under-the-hood.
Written in Java 7, but can be cross compiled against Java 6 if desired.
The latest stable version of this library is 1.4.
If you wish to use this artifact, you can easily add it to your existing Maven or SBT project using my GitHub hosted Maven2 repository.
resolvers += "Kolich repo" at "http://markkolich.github.com/repo"
val havaloClient = "com.kolich" % "havalo-kvs-client" % "1.4" % "compile"
<repository>
<id>Kolichrepo</id>
<name>Kolich repo</name>
<url>http://markkolich.github.io/repo/</url>
<layout>default</layout>
</repository>
<dependency>
<groupId>com.kolich</groupId>
<artifactId>havalo-kvs-client</artifactId>
<version>1.4</version>
<scope>compile</scope>
</dependency>
Before you can make API requests, you'll need to instantiate a HavaloClient
backed by an HttpClient
instance.
You will pass your Havalo API access key and secret to the HavaloClient
via its constructor. The HavaloClient
instance will handle all authentication with the API on your behalf. Further, you'll need to provide the Havalo API endpoint URL.
Note, the API key and secret are created by the Havalo API when a new repository is created.
import com.kolich.havalo.client.service.HavaloClient;
import java.util.UUID;
// Your Havalo API key
final UUID key = ...;
// Your Havalo API secret
final String secret = ...;
// Your Havalo API endpoint URL, usually something like
// http://localhost:8080/havalo/api
final String apiUrl = ...;
final HavaloClient client = new HavaloClient(key, secret, apiUrl);
If you have a configured HttpClient
that you'd like to use instead of the default, you can of course use a slightly different constructor and pass your own.
import org.apache.http.client.HttpClient;
final HttpClient httpClient = ...; // Your own HttpClient instance
final HavaloClient client = new HavaloClient(httpClient, key, secret, apiUrl);
Finally, if you're using Spring, your web-application can also instantiate a HavaloClient
bean.
<!-- An HttpClient instance, either created by the KolichHttpClientFactory or on your own. -->
<bean id="YourHttpClient"
class="com.kolich.http.blocking.KolichDefaultHttpClient.KolichHttpClientFactory"
factory-method="getNewInstanceWithProxySelector">
<constructor-arg><value>Some kewl user-agent</value></constructor-arg>
</bean>
<bean id="HavaloClient"
class="com.kolich.havalo.client.service.HavaloClient">
<constructor-arg index="0" ref="YourHttpClient" />
<constructor-arg index="1"><value>6fe8e625-ec21-4890-a685-a7db4346cceb</value></constructor-arg>
<constructor-arg index="2"><value>Crb7s5coXNb...EnQIYr-9cxNqShozksHitLg</value></constructor-arg>
<constructor-arg index="3"><value>http://localhost:8080/havalo/api</value></constructor-arg>
</bean>
That's it, you're ready to make API requests.
All HavaloClient
methods return an com.kolich.common.either.Either<F,S>
— this return type represents either a left type F
indicating failure, or a right type S
indicating success. For more details on this return type and how to use it, please refer to the Functional Concepts overview in my kolich-httpclient4-closure library.
Verify your Havalo API authentication credentials.
Does nothing other than verifies your API key and secret. This is most useful on application startup when you want to verify connectivity/access to the Havalo API before attempting to do actual work.
final Either<HttpFailure,KeyPair> auth =
client.authenticate();
if(auth.success()) {
// Success
} else {
// Failed
}
Create a new repository.
Note that only administrator level API users can create new repositories.
final Either<HttpFailure,KeyPair> repo =
client.createRepository();
final KeyPair kp;
if((kp = repo.right()) != null) {
// Success
System.out.println("Your new API key: " + kp.getKey());
System.out.println("Your new API secret: " + kp.getSecret());
}
Delete repository by UUID toDelete
.
Note that only administrator level API users can delete repositories.
final UUID toDelete = ...; // The UUID of the repository to delete.
final Either<HttpFailure,Integer> del =
client.deleteRepository(toDelete);
if(del.success()) {
System.out.println("Deleted repository successfully, status code: " +
del.right());
}
List objects in repository, or list all objects in repository that start with a given prefix.
Note the prefix
argument is optional — if omitted all objects will be returned.
// List all objects whose key starts with "foobar/baz"
final Either<HttpFailure,ObjectList> list =
client.listObjects("foobar", "baz");
if(list.success()) {
final ObjectList objs = list.right();
System.out.println("Found " + objs.size() + " objects.");
}
Get an object with the given path
and write it out to the provided outputStream
.
final OutputStream os = ...; // An existing and open OutputStream.
// Get the object whose key is "foobar/baz/0.json" and stream its
// bytes to the provided OutputStream.
final Either<HttpFailure,List<Header>> get =
client.getObject(os, "foobar", "baz", "0.json");
if(get.success()) {
// Success, object data was copied to provided OutputStream.
// Here are the HTTP headers on the response.
final List<Header> headers = get.right();
for(final Header h : headers) {
System.out.println(h.getName() + ": " + h.getValue());
}
} else {
// Object not found.
}
Or, pass a CustomEntityConverter
provided by the kolich-httpclient4-closure to stream the object "elsewhere" on success, or handle a failure on error. This allows you to extract meta-data about the object from Havalo before streaming the object out to a consumer — properties like the HTTP Content-Length
or Content-Type
of the object are only available on a GET
operation if you use a CustomEntityConverter
.
import com.kolich.http.blocking.helpers.definitions.CustomEntityConverter;
import org.apache.commons.io.IOUtils;
final ServletResponse response = ...; // From your Servlet container
client.getObject(new CustomEntityConverter<HttpFailure,Long>() {
@Override
public Long success(final HttpSuccess success) throws Exception {
// Send down the HTTP Content-Type as fetched in the response
// from the Havalo K,V store.
final String contentType;
if((contentType = success.getContentType()) != null) {
response.setContentType(contentType);
}
// Send down the HTTP Content-Length as fetched in the response
// from the Havalo K,V store.
final String contentLength;
if((contentLength = success.getContentLength()) != null) {
response.setContentLength(Integer.parseInt(contentLength));
}
// Actually stream the bytes out to the caller.
final ServletOutputStream os = response.getOutputStream();
return IOUtils.copyLarge(success.getContent(), os);
}
@Override
public HttpFailure failure(final HttpFailure failure) {
// Handle failure, write out error response, do whatever is
// appropriate on error.
// ...
return failure;
}
}, "foobar", "baz", "0.json");
Or, even further, you can pass a CustomSuccessEntityConverter<S>
and a CustomFailureEntityConverter<F>
to define separate units of work to be "called" on either success or failure.
import com.kolich.http.blocking.helpers.definitions.CustomFailureEntityConverter;
import com.kolich.http.blocking.helpers.definitions.CustomSuccessEntityConverter;
client.getObject(
// Success converter
new CustomSuccessEntityConverter<S>() {
@Override
public S success(final HttpSuccess success) throws Exception {
// Do something on success, return type 'S'
}
},
// Failure converter
new CustomFailureEntityConverter<F>() {
@Override
public F failure(final HttpFailure failure) {
// Do something on failure, return type 'F'
}
},
// Path to object key
"foobar", "baz", "0.json"
);
The intention of CustomSuccessEntityConverter<S>
and CustomFailureEntityConverter<F>
is to let you define reusable units of work — reusable implementations that define how to convert a response entity into something useful specific to your application, outside of an inline anonymous class.
Get the meta data associated with the object at the given path
.
Any HTTP headers sent with the object during a PUT
are returned. Note that the API generated SHA-1 hash that was computed on upload is also returned in the ETag
HTTP response header.
// Get the meta data for the object whose key is "foobar/baz/1.xml"
final Either<HttpFailure,List<Header>> meta =
client.getObjectMetaData("foobar", "baz", "1.xml");
if(meta.success()) {
final List<Header> headers = meta.right();
for(final Header h : headers) {
System.out.println(h.getName() + ": " + h.getValue());
}
}
Upload (PUT
) an object to the given path
that is contentLength
bytes long, using the provided inputStream
. Send any additional meta data represented by headers
with the request too.
import static org.apache.http.HttpHeaders.CONTENT_TYPE;
import static org.apache.http.HttpHeaders.ETAG;
final InputStream is = ...; // An existing and open InputStream.
// Upload an object to path "baz/foobar.jpg" .. since it's a JPG
// image, also send the relevant Content-Type with the request.
final Either<HttpFailure,FileObject> upload =
client.putObject(is,
// The number of bytes in this object.
1024L,
// A Content-Type to be saved with the object. This header is
// sent back with the object when it's retrieved.
new Header[]{new BasicHeader(CONTENT_TYPE, "image/jpeg")},
// The path to the object.
"baz", "foobar.jpg");
if(upload.success()) {
// Success
// The SHA-1 hash of the uploaded object can be found in the
// ETag HTTP response header.
final String hash = upload.right().getFirstHeader(ETAG);
System.out.println("Uploaded object SHA-1 hash is: " + hash);
}
TIP: For a conditional PUT
, you can also send an If-Match
HTTP request header with your request. If the SHA-1 hash sent with the If-Match
header matches the current SHA-1 hash of the object, the object will be replaced. If the SHA-1 hash sent with the If-Match
header does not match the current hash of the object, the PUT
will fail with a 409 Conflict
.
Upload (PUT
) an object to the given path
using the provided byte[]
array.
final byte[] data = ...; // Some byte[] array of data.
// Upload an object to path "cat"
final Either<HttpFailure,FileObject> upload =
client.putObject(data, "cat");
if(upload.success()) {
// Success
// The SHA-1 hash of the uploaded object can be found in the
// ETag HTTP response header.
final String hash = upload.right().getFirstHeader("ETag");
System.out.println("Uploaded object SHA-1 hash is: " + hash);
}
Delete an object at the given path
only if the SHA-1 hash of that object matches the SHA-1 hash sent with the If-Match
HTTP request header.
import static org.apache.http.HttpHeaders.IF_MATCH;
// An SHA-1 hash for the version of the object you want to delete.
final String myHash = "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3";
// Delete the object at path "foobar/cat" only if that object's hash
// equals "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3".
final Either<HttpFailure,Integer> delete =
client.deleteObject(
new Header[]{new BasicHeader(IF_MATCH, myHash)},
"foobar", "cat");
if(delete.success()) {
// Success
} else {
// Failed, get the resulting HTTP status code so
// we can see what happened.
switch(delete.left().getStatusCode()) {
case 404:
// 404 Not Found
// Object at provided path didn't exist.
// ...
break;
case 409:
// 409 Conflict
// Object conflict, the sent SHA-1 hash did not match.
// ...
break;
default:
// Hmm, something else went wrong.
// ...
break;
}
}
Delete an object at the given path
, ignoring the object's current hash.
// Delete the object at path "foobar/cat".
final Either<HttpFailure,Integer> delete =
client.deleteObject("foobar", "cat");
if(delete.success()) {
// Success
} else {
// Failed
System.out.println("Oops, delete failed with status: " +
delete.left().getStatusCode());
}
This Java library and its dependencies are built and managed using SBT.
To clone and build havalo-kvs-client, you must have SBT installed and configured on your computer.
The havalo-kvs-client SBT Build.scala file is highly customized to build and package this Java artifact. It's written to manage all dependencies and versioning.
To build, clone the repository.
#~> git clone git://github.com/markkolich/havalo-kvs-client.git
Run SBT from within havalo-kvs-client.
#~> cd havalo-kvs-client
#~/havalo-kvs-client> sbt
...
havalo-kvs-client:1.4>
You will see a havalo-kvs-client
SBT prompt once all dependencies are resolved and the project is loaded.
In SBT, run package
to compile and package the JAR.
havalo-kvs-client:1.4> package
[info] Compiling 12 Java sources to ~/havalo-kvs-client/target/classes...
[info] Packaging ~/havalo-kvs-client/dist/havalo-kvs-client-1.4.jar ...
[info] Done packaging.
[success] Total time: 4 s, completed
Note the resulting JAR is placed into the havalo-kvs-client/dist directory.
To create an Eclipse Java project for havalo-kvs-client, run eclipse
in SBT.
havalo-kvs-client:1.4> eclipse
...
[info] Successfully created Eclipse project files for project(s):
[info] havalo-kvs-client
You'll now have a real Eclipse .project file worthy of an Eclipse import.
Note your new .classpath file as well — all source JAR's are fetched and injected into the Eclipse project automatically.
Copyright (c) 2014 Mark S. Kolich
All code in this artifact is freely available for use and redistribution under the MIT License.
See LICENSE for details.