Unboxing Day, Part II
Mockito Made Clear
Unboxing my ebook in Java; downloading it first.
In the first article in this series, I talked about the challenges of creating an unboxing video for an ebook, given the complication that there’s no box to open. The book in question is Mockito Made Clear, from the Pragmatic Bookshelf, also available as a Kindle version at Amazon.
Amazon recommends that you add a video to the book’s page on their site, as I did for my previous book, Help Your Boss Help You. Sadly, the video I added shows up only on the paperback version page and not on the Kindle version page, but I guess that makes sense.
In this waggish and frolicsome series, I demonstrate Java-based methods that download and open the ebook — all so I can simulate an unboxing process. I left off by using the following to open a file on the local file system with the application associated with the file extension:
public void openPdf(String fileName) {
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(new File(fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
The next question: how to download the file itself programmatically? That task will be complicated by the fact that Dropbox URLs have issues — which we’ll get to later.
Step 0 was just click to open the pdf file. Step 1 was mostly the openPdf
method just shown. Now let’s move on to Step 2.
Step 2: Downloading the PDF
There are a minimum of three different ways to download a file given its URL. I’ll show you all three just to mesh with my ultimate goal, which is to over-complicate the unboxing process to an absurd degree.
Method 1: HTTP Client
The first way to download a file is based on the HttpClient
API added to Java in version 11. I’ve been using that for a while, but never had the opportunity to use the HttpResponse.BodyHandlers.ofFile
method, until now.
The HTTP Client API is formally JEP 321, which replaces an Incubator version in JEP 110. The goals of the client were essentially to replace the existing HttpURLConnection
class, introduce an asynchronous mode, support HTTP/2 and WebSockets, and more. Like so many APIs these days, it’s all about factory methods and fluent interfaces.
Here’s how to use it to download a file at a link:
public long downloadFile(String url) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<Path> response =
client.sendAsync(request,
HttpResponse.BodyHandlers.ofFile(Paths.get(FILENAME)))
.join();
Path body = response.body();
return body.toFile().length();
}
Create a new HttpClient
from a factory method. Create a new HttpRequest
from a factory method, specifying the URL. Then execute the request using either the send
(synchronous, blocking) method or the sendAsync
(asynchronous) method.
The fun part is that both the send
and the sendAsync
methods take two arguments, the request and a body handler, whatever that is:
The send
method blocks, but then returns an HttpResponse
, while the sendAsync
method does not block, but returns a CompletableFuture
that wraps the output HttpResponse
. The HttpResponse.BodyHandler
inner interface is a functional interface that has several implementations in the HttpResponse.BodyHandlers
(plural) inner class. The Javadocs show a few implementations, including:
// Receives the response body as a String
HttpResponse<String> response = client
.send(request, BodyHandlers.ofString());
// Receives the response body as a file
HttpResponse<Path> response = client
.send(request, BodyHandlers.ofFile(Paths.get("example.html")));
The downloadFile
implementation demonstrates using the sendAsync
method to create a file from the URL, calling the join
method on the CompletableFuture
to wait for it to finish. Might as well do the networking part off the UI thread, even though there’s no actual UI in this case.
Finally, extract the body from the response, and the method returns the length of the file — useful in testing:
@Test
void downloadFile() {
long length = unboxing.downloadFile(dropBoxLink);
assertThat(length).isCloseTo(downloadedBookSize, within(10000L));
}
The test uses a few methods from the AssertJ
library of syntactic sugar that makes the tests easier to follow. There’s a bit of a complication with links from Dropbox, but we’ll get to that later.
This approach works, but feels overly complicated, even when complications are what we want. Isn’t there an easier way?
Method 2: Open a Stream and Copy
As it turns out, you can just open an InputStream
to the URL and use the Files.copy
method to save the returned bytes:
public long simplerDownloadFile(String url) {
try (InputStream in = URI.create(url).toURL().openStream()) {
return Files.copy(in, Paths.get(FILENAME));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Note the nice try-with-resources
construct, which not only closes the stream when you’re done but also gives you cool nerd credibility during the inevitable code review.
The copy
method in the Files
class copies all the incoming bytes into a File
and returns the total number of bytes read. Sweet.
But wait, there’s more. Before any of this mess came along, the Apache Commons IO project supplied lots of useful options for handling tasks like these. Can’t that help too?
Of course it can, or I wouldn’t have brought it up.
Method 3: Apache Commons IO FileUtils
The Apache Commons IO project is “a library of utilities to assist with developing IO functionality.” The project includes a whole series of classes to perform a wide range of functions.
The one we want is the FileUtils
class, from org.apache.commons.io
, naturally enough, which states:
The method that helps is called copyURLToFile
, which takes a URL source and a File
destination. Using that approach gives this implementation:
public long copyURLToFile(String url) {
try {
File file = new File(FILENAME);
FileUtils.copyURLToFile(URI.create(url).toURL(), file);
return file.length();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
The copyURLToFile
method returns void
, so it’s slightly more work to return the length of the file, but so be it.
Feel free to use any of these methods. They all do the job, meaning the tests in the GitHub repository pass for all three of them. If it weren’t for the specter of checked exceptions, they’d all be a bit simpler, but none are too bad.
The Dropbox Complication
We face another complication: Dropbox shared links aren’t direct, and that’s a problem when you access them programmatically. Normal Dropbox links end in ?dl=0
. In order for the links to work in Java, you need to change that ending to ?dl=1
. Look at the headers and you’ll see that the latter version returns a 302 redirect, but includes the actual path to the file in a header. The default path returns a 200, but that kept giving me a FileNotFoundException
, which was very annoying. Due to the magic of Stack Overflow and other sources, I was able to fix that by changing the links as stated.
Where’s Mockito?
You might be thinking, “This series is about downloading the awesome new book, Mockito Made Clear —isn’t there any way to incorporate Mockito into the code?”
No, not as we’ve shown so far, mostly because one of the lessons of Mockito is that you’re not supposed to mock classes you don’t own. I didn’t want to mock the library classes, so what else could I mock?
In the third and final article in this series, I’ll introduce another class to print the downloaded file. Then I can mock that class and show off Mockito — don’t touch that dial!
As before, all code for this nonsense is in this GitHub repository.