This is a thin Java client for Draftable's document comparison API.
It wraps the available endpoints, and handles authentication and signing for you.
The library is available on Maven
with artifact ID draftable-compare-api
(group ID com.draftable.api.client
).
See the full API documentation for an introduction to the API, usage notes, and other references.
-
Sign up for free at api.draftable.com to get your credentials.
-
Add
draftable-compare-api
to your project's dependencies. -
Instantiate the client:
import com.draftable.api.client.Comparisons; import com.draftable.api.client.Comparisons.Side; import com.draftable.api.client.Comparison; ... Comparisons comparisons = new Comparisons(<your account ID>, <your auth token>);
-
Start creating comparisons:
Comparison comparison = comparisons.createComparison( Side.create("https://api.draftable.com/static/test-documents/code-of-conduct/left.rtf", "rtf"), Side.create("https://api.draftable.com/static/test-documents/code-of-conduct/right.pdf", "pdf") ); String viewerURL = comparisons.signedViewerURL(comparison.identifier, Duration.ofMinutes(30), false); System.out.println("Comparison created: " + comparison); System.out.println("Viewer URL (expires in 30 min): " + viewerURL);
-
Your system administrator will provide you with the URL to complete setting up your account on your locally installed version of Draftable.
-
Once you log in, navigate to the API explorer - the URL will be something like https://draftable.your.com/api/api-explorer - and experiment with the API there.
-
To start making API calls, make a note of your account credentials from the API explorer page or the account credentials page, which have a URL like https://draftable.your.com/account/credientials
-
In particular, make a note of the base URL, which will be something like https://draftable.your.com/api/v1 with no trailing slash at the end.
The client depends on org.apache.httpcomponents
(in particular, httpclient
, httpasyncclient
, and httpmime
are used to make API requests).
The only other dependency is on org.json
.
All requests can be made synchronously or asynchronously (using the methods suffixed with Async
, e.g. getAllComparisonsAsync()
).
All asynchronous methods return CompletableFuture
instances that will either complete successfully,
or with one of the exceptions documented for the synchronous version.
As an example, in normal usage getAllComparisons()
will only ever throw an IOException
.
So getComparisonsAsync()
will only fail with an IOException
. Here's an example of asynchronously handling the result:
CompletableFuture<List<Comparison>> allComparisonsFuture = comparisons.getAllComparisonsAsync();
allComparisonsFuture.whenComplete((allComparisonsList, error) -> {
if (error != null) {
// CompletableFuture wraps errors in a CompletionException.
assert error instanceof CompletionException;
// The CompletionException's cause should be an IOException.
assert error.getCause() instanceof IOException;
// Handle the IOException here.
} else {
assert allComparisonsList != null;
// Handle the result here.
}
}
The API is designed such that requests should always succeed and comparisons should always succeed in production. This means:
- Exceptions when making requests will only occur upon network failure, or when you provide invalid credentials or data.
- Comparisons will only fail when the files are unreadable, or exceed your account's size limits.
As such, IOException
is the only checked exception that request methods throw.
Methods will also throw other unchecked exceptions, as documented in javadoc and below.
You may choose to handle these unchecked exceptions, but you should be able to write your application so that they're never thrown.
The API client class, Comparisons
, is completely thread-safe.
If you close()
it prematurely, further requests will reopen the underlying HTTP clients.
The package draftable-compare-api
provides a module, com.draftable.api.client
, which provides two classes, Comparisons
and Comparison
.
An instance of Comparisons
lets you create new comparisons, retrieve all or specific comparisons, and delete specific comparisons.
Instances of Comparison
are returned by methods, and provide metadata for a given comparison.
Accessing the Draftable cloud API requires only the account ID and auth token. Create an instance of Comparisons
like this:
import com.draftable.api.client.Comparisons;
import com.draftable.api.client.Comparison;
...
Comparisons comparisons = new Comparisons(<your account ID>, <your auth token>);
Enterprise self-hosted users need to provide the base URL as a third parameter, like this:
import com.draftable.api.client.Comparisons;
import com.draftable.api.client.Comparison;
...
Comparisons comparisons = new Comparisons(<your account ID>, <your auth token>, "http://draftable.your.com/api/v1");
The Comparisons
instance provides methods:
-
getAllComparisons()
returns aList<Comparison>
giving metadata for all your comparisons, ordered from newest to oldest. This is a potentially expensive operation. -
getComparison(String identifier)
returns a singleComparison
object, or raisesComparisons.ComparisonNotFoundException
if there isn't a comparison with that identifier.
Comparison
objects have the following properties:
identifier
: aString
giving the identifier.left
,right
:Comparison.Side
objects giving information about each side, with properties:fileType
: the file extension.sourceURL
(optional): if the file was specified as a URL, this will be a string with the URL. Otherwise,null
.displayName
(optional): the display name, if one was given. Otherwise,null
.
isPublic
: aboolean
giving whether the comparison is public, or requires authentication to view.creationTime
: anInstant
giving when the comparison was created.expiryTime
(optional): if the comparison will expire, anInstant
giving the expiry time. Otherwise,null
(indicating no expiry).ready
:boolean
indicating whether the comparison is ready for display.
If a Comparison
is ready
(i.e. it has been processed and is ready for display), it will have the following additional properties:
readyTime
: anInstant
giving the time the comparison became ready.failed
:Boolean
indicating whether the comparison succeeded or failed.errorMessage
(only present iffailed
): a string providing the developer with the reason the comparison failed.
The following snippet retrieves a specific comparison and prints key data to the console:
String identifier = "<identifier>";
try {
Comparison comparison = comparisons.getComparison(identifier);
assert comparison.getIdentifier().equals(identifier);
System.out.println(String.format(
"Comparison '%s' (%s) is %s.",
identifier,
comparison.getIsPublic() ? "public" : "private",
comparison.getReady() ? "ready" : "not ready"
));
if (comparison.getReady()) {
System.out.println(String.format(
"The comparison took %s seconds.",
comparison.getReadyTime().getEpochSecond() - comparison.getCreationTime().getEpochSecond()
));
if (comparison.getFailed()) {
System.out.println("The comparison failed. Error message:" + comparison.getErrorMessage());
}
}
} catch (Comparisons.ComparisonNotFoundException ex) {
System.out.println(String.format("Comparison '%s' doesn't exist.", identifier));
}
Comparisons
provides deleteComparison(String identifier)
, which attempts to delete the comparison with that identifier.
It has no return value, and raises Comparisons.ComparisonNotFoundException
if there isn't a comparison with that identifier.
List<Comparison> allComparisons = comparisons.getAllComparisons();
List<Comparison> oldestComparisons = allComparisons.subList(Math.max(allComparisons.size() - 10, 0), allComparisons.size());
System.out.println(String.format("Deleting oldest %s comparisons...", oldestComparisons.size()));
for (Comparison comparison : oldestComparisons) {
comparisons.deleteComparison(comparison.getIdentifier());
System.out.println(String.format("Deleted comparison '%s'.", comparison.getIdentifier()));
}
Comparisons
provides createComparison(left, right, [identifier, isPublic, expires])
, which returns a Comparison
object representing the newly created comparison.
For a complete, runnable example that creates a new comparison, see file NewComparison.java.
createComparison
accepts the following arguments:
left
,right
:Comparisons.Side
objects describing the left and right files. These are described below.identifier
(optional): the identifier to use for the comparison.- If specified, the identifier can't clash with an existing comparison. (If so, a
Comparisons.BadRequestException
is thrown.) - If left unspecified, the API will automatically generate one for you.
- If specified, the identifier can't clash with an existing comparison. (If so, a
public
(optional): whether the comparison is publicly accessible.- Defaults to
false
. Iftrue
, then the comparison viewer can be accessed by anyone, without authentication. - See the full API documentation for details.
- Defaults to
expires
(optional): anInstant
specifying when the comparison will be automatically deleted.- If given, must in the future.
- Defaults to
null
, meaning the comparison will never expire.
To specify left
and right
, create Comparisons.Side
instances using one of the static constructors.
The full set of overloads are documented in javadoc, but here are the main ones:
-
Comparisons.Side.create(url, fileType, [displayName])
- Specifies a file via a URL. You must give a fully qualified URL from which Draftable can download the file.
fileType
is required, given as the file extensiondisplayName
is an optional name for the file, to be shown in the comparison
-
Comparisons.Side.create(file, fileType, [displayName])
- Specifies a file to be uploaded in the request. You can provide a
File
, byte array, orInputStream
. fileType
anddisplayName
are as before.
- Specifies a file to be uploaded in the request. You can provide a
The following file types are supported:
- PDF:
pdf
- Word:
docx
,docm
,doc
,rtf
- PowerPoint:
pptx
,pptm
,ppt
If you try to create a Comparisons.Side
with an invalid fileType
or malformed url
, an IllegalArgumentException
will be thrown.
Exceptions are raised by createComparison
in the following cases:
- If a parameter is invalid (e.g.
expires
is set to a time in the past), it will throw anIllegalArgumentException
.- All parameters should be validated client-side by this library. If not, a
Comparisons.BadRequestException
will be thrown. Please submit an issue on GitHub if you encounter such behaviour.
- All parameters should be validated client-side by this library. If not, a
- If
identifier
is already in use by another comparison,Comparisons.BadRequestException
is thrown.
File rightFile = new File(...);
Comparison comparison = comparisons.createComparison(
Comparisons.Side.create("https://domain.com/path/to/left.pdf", "pdf"),
Comparisons.Side.create(rightFile),
// identifier: not specified, so Draftable will generate one
null,
// isPublic: false, so that the comparison is private
false,
// expires: 30 minutes in the future, so the comparison will be automatically deleted then
Instant.now().plus(Duration.ofMinutes(30))
);
System.out.println("Created comparison: " + comparison);
// This generates a signed viewer URL that can be used to access the private comparison for the next 10 minutes.
String viewerURL = comparisons.signedViewerURL(
// identifier: The identifier of the comparison
comparison.identifier,
// validUntil: The amount of time before the link expires
Duration.ofMinutes(10),
// wait: Whether the viewer should wait for a comparison with the given identifier to exist.
// (This is simply `false` for normal usage.)
false
);
System.out.println("Viewer URL (expires in 10 min): " + viewerURL);
Comparisons are displayed using a viewer URL. See the section on displaying comparisons in the API documentation for details.
Viewer URLs are generated with the following methods:
-
comparisons.publicViewerURL(String identifier, [boolean wait])
- Viewer URL for a public comparison with the given
identifier
. wait
isfalse
by default, meaning the viewer will show an error if no such comparison exists.- If
wait
istrue
, the viewer will wait for a comparison with the givenidentifier
to exist (potentially displaying a loading animation forever).
- Viewer URL for a public comparison with the given
-
comparisons.signedViewerURL(String identifier, [Instant/Duration validUntil], [boolean wait])
- Gets a signed viewer URL for a comparison with the given
identifier
. (The signature is an HMAC based on your credentials.) validUntil
gives when the URL will expire. It's specified as anInstant
or aDuration
.- If
validUntil
isNone
, the URL defaults to expiring 30 minutes in the future (more than enough time to load the page).
- If
- Again, if
wait
istrue
, the viewer will wait for a comparison with the givenidentifier
to exist.
- Gets a signed viewer URL for a comparison with the given
In this example, we'll start creating a comparison in the background, but immediately direct our user to a viewer. The comparison viewer will display a loading animation, waiting for the comparison to be created and processed.
// This generates a unique identifier we can use.
String identifier = Comparisons.generateIdentifier();
CompletableFuture<Comparison> future = comparisons.createComparisonAsync(
Side.create("https://api.draftable.com/static/test-documents/code-of-conduct/left.rtf", "rtf"),
Side.create("https://api.draftable.com/static/test-documents/code-of-conduct/right.pdf", "pdf"),
// identifier: the identifier we just generated
identifier,
// isPublic: false, for a private comparison
false,
// expires: null, so the comparison will never expire
null
);
// At some point, we will have created the comparison.
// (The operation could take some time if we're uploading files...)
// In the mean time, we can immediately give the user a viewer URL, using `wait=true`:
String viewerURL = comparisons.signedViewerURL(identifier, Duration.ofMinutes(30), true);
// This URL is valid for 30 minutes, and will show a loading screen until the comparison is ready.
System.out.println("Comparison is being created. View it here: " + viewerURL);
// For the purposes of this example, we'll just block until the request finishes.
future.join();
Comparisons.generateIdentifier()
generates a random unique identifier for you to use.
The underlying httpclient
and httpasyncclient
objects are configured to respect system properties. The full list of options considered are given in the Apache HttpClient documentation. These options allow configuring the use of a proxy server, as well as other request parameters.
That's it! Please report issues you encounter, and we'll work quickly to resolve them. Contact us at [email protected] if you need assistance.