URL Shortener #4: The Xtend Service Implementation
How do we go about implementing the service’s REST interface and components in Xtend? This series of articles contrasts the Java and Xtend languages around a very simple URL shortening REST service. Xtend is a JVM language that compiles into readable Java and is fully compatible with all Java frameworks, libraries and tools. If you know Java you already know most of Xtend! A presentation is also available
The Java UrlShortener
Implementation
Let’s start our comparison revisiting the overallJava UrlShortener
implementation:
// The workhorse of our service
public class UrlShortener {
// Immutable dependency on Shortener
private final Shortener shortener;
// Immutable dependency on StorageProvider
private final StorageProvider storageProvider;
// Non-property, mutable local variable
private Map<String, String> storage;
// All-property constructor
// and associated builder elided...
// Start the Spark server and set routes
public void start() {
// Get fresh storage map each time
storage = storageProvider.openStorage();
if (storage == null) {
throw new NullPointerException("Null storage");
}
// The first route defined via get,
// post, put, delete, head or option
// implicitly starts the Spark server
// on port 4567
// Shorten REST endpoint
post("/api/shorten", (req, res) -> {
// POST creating shortUrl goes here
});
// Redirect REST endpoint
get("/:hash", (req, res) -> {
// GET redirecting from short URL
// goes here
});
// Delete REST endpoint
delete("/:hash", (req, res) -> {
// DELETE removing short URL
// goes here
});
}
// Stop the Spark server and
// clean up after ourselves
public void stop() {
// Stop Spark server
spark.Spark.stop();
// Persistent stores require closing!
if (storage instanceof Closeable) {
try {
((Closeable) storage).close();
} catch (IOException e) {
}
}
}
}
The Xtend UrlShortener
Implementation
The Xtend version of the above is:
// The @Buildable active annotation
// generates a full-blown builder
// at compile time!
@Buildable
// Class access defaults to public
class UrlShortener {
// Field access defaults to private
// Immutable dependency on Shortener
val Shortener shortener
// Immutable dependency on Shortener
val StorageProvider storageProvider
// Non-property local variable
Map<String, String> storage
// All-property constructor elided...
// Start Spark server, config routes
def start() {
// Get a fresh storage on each start
storage = storageProvider.openStorage
if(storage === null) {
throw new NullPointerException('Null storage')
}
// The first route defined via
// get/post/put/delete/head/option
// will implicitly start the Spark
// server on port 4567
// Shorten REST endpoint
post('/api/shorten', [req, res |
// Short URL creation goes here
])
// Redirect REST endpoint
get('/:hash', [req, res |
// Short URL redirection goes here
])
// Delete REST endpoint
delete('/:hash', [req, res |
// Short URL removal goes here
])
}
def stop() {
// Stop Spark server
Spark.stop
// Clean up after ourselves
if(storage instanceof Closeable) {
try {
// No need to cast to Closeable!
storage.close
} catch(Exception e) {
}
}
}
}
Note the use of the @Buildable
active annotation.
An Xtend active annotation is an annotation whose processor is invoked at compile time as part of the translation to Java pipeline. Active annotations can generate additional code (such as constructors and builders in this case) that can be freely referenced as if they’d been written by hand!
Posting a Long URL to Obtain a Short One
The Xtend POST
route returning a short URL from a long one looks like:
post('/api/shorten', [req, res |
// Long URL comes in the request body
val longUrl = req.body
try {
// Try to build URI from long URL
val uri = new URI(longUrl)
// We only do HTTP/HTTPS
if(uri.scheme !== null &&
uri.scheme.startsWith('http')) {
// String appears to have
// a "shorten" method...
val hash = longUrl.shorten
// Map appears to have
// a "+=" operator...
storage += hash -> longUrl
// Request appears to have
// a "shortUrlFrom" method
val shortUrl =
req.shortUrlFrom(hash)
// Response appears to have
// a "respondWith" method
res.respondWith(SC_CREATED, shortUrl)
} else {
res.respondWith(
SC_BAD_REQUEST,
'''Not an HTTP URL: «longUrl»''')
}
} catch(Exception e) {
res.respondWith(
SC_BAD_REQUEST,
'''Malformed long URL: «longUrl»''')
}
])
The overall Xtend logic is not that different from its Java counterpart but… what on Earth are those comments stating “… appears to have a method…”?
Xtend has this powerful concept called extension methods where any class can be augmented so as to expose additional methods not present in its original implementation.
Thus, for example, we can have String
expose a shorten
method that looks intuitive in:
val String longUrl = req.body
val String hash = longUrl.shorten
For this to be valid, an extension method must be in scope such that its first argument is of type String
. In the above case, our extension method must look like:
def shorten(String longUrl) {
shortener.shorten(longUrl)
}
Note above that the
return
keyword is optional in Xtend. The last expression in the method is the return value.
The +=
operator attached to Map
is an extension method implemented as follows:
def operator_add(
Map<String, String> storage,
Pair<String, String> pair) {
storage.put(pair.key, pair.value)
}
The
->
operator is syntactic sugar for classPair
. Thuskey -> value
is equivalent tonew Pair<K, V>(key, value)
When we write
val shortUrl = req.shortUrlFrom(hash)
we’re actually using the following extension method:
def shortUrlFrom(Request request,
String hash) {
new URI(request.url).
resolve(redirectPath + hash).toString
}
Lastly, when we write:
res.respondWith(SC_CREATED, shortUrl)
we’re using the following extension method:
def respondWith(Response response,
int status,
String body) {
response.status(status)
body
}
where we set the response’s HTTP status and return whatever body
was passed to the method.
Requesting a Short URL that Redirects to the Long One
The Xtend implementation of redirecting to the long URL given the short URL is:
get('/:hash', [req, res |
// Get the short URL's hash
val hash = req.params(':hash')
// Do we have it in our key/value store?
if(storage.containsKey(hash)) {
// Yes: retrieve the long URL...
val longUrl = storage.get(hash)
// ... and redirect the client there
res.redirect(longUrl)
// Return no content
''
} else {
// No such short URL: 404
res.respondWith(
SC_NOT_FOUND,
'''No such short URL: «req.url»''')
}
])
Deleting a Short URL
The Xtend implementation for the DELETE
endpoint is:
delete('/:hash', [req, res |
// Get the short URL's hash
val hash = req.params(':hash')
// Do we have it in our key/value store?
if(storage.containsKey(hash)) {
// Yes: remove it
storage -= hash
// Signal success w/no content
res.respondWith(SC_NO_CONTENT)
} else {
// No such short URL: 404
res.respondWith(SC_NOT_FOUND)
}
])
Note here that we’re using the respondWith
method with only an HTTP status but no response body. The underlying extension method is just a shortand:
def respondWith(Response response,
int status) {
respondWith(response, status, "")
}
We’re also attributing a -=
operator to Map
. The underlying extension method is:
def operator_remove(
Map<String, String> storage,
String hash) {
storage.remove(hash)
}
Stopping the URL Shortener
In order to stop our server we need to stop the Spark server as such and also close the storage
map:
def stop() {
Spark.stop
tryAndClose(storage)
}
where tryAndClose
is implemented as:
static def tryAndClose(Object obj) {
if (obj instanceof Closeable) {
try {
// No cast to Closeable!
obj.close
} catch(Exception e) {
// Ignore closing errors
}
}
}
As an added bonus, when Xtend sees an instanceof
condition it generates an implicit cast with the same variable name. Neat!
Implementing the Shortener
Interface
We’ve chosen Guava’s Hashing utility class to provide us with hashing algorithms suitable for string shortening due to their very probability of collision.
Two appropriate algorithms are: mumur3 and sipHash. Google’s Guava implements both algorithms in its Hashing utility class.
Implementing our Shortener
interfaces based on Guava’s Hashing
is a breeze:
// File GuavaHashingShortening.xtend
/**
* Murmur3-based shortener.
* This class only supports UTF-8.
*/
class Murmur3Shortener
implements Shortener {
override shorten(String string) {
checkNotNull(
string,
'String to be shortened cannot be null')
Hashing.murmur3_32().
hashString(
string, StandardCharsets.UTF_8).
toString()
}
}
/**
* SipHash-based shortener.
* This class only supports UTF-8.
*/
class SipHashShortener
implements Shortener {
override shorten(String string) {
checkNotNull(
string,
'String to be shortened cannot be null')
Hashing.sipHash24().
hashString(
string, StandardCharsets.UTF_8).
toString
}
}
Unlike Java, Xtend allows multiple type declarations in the same file. Thus, the implementations above reside in the same file (properly baptized as
GuavaHashingShortening.xtend
.
Note above that an interface-implementing method is not prefixed with def
but with override
Implementing the StorageProvider
Interface
For key/value persistent stores we’ve selected ChronicleMap. This embeddable key/store store is very fast and easy to use.
The StorageProvider
implementation is straight-forward but requires a little bit of configuration:
@Buildable // Give us a builder
class ChronicleMapStorageProvider
implements StorageProvider {
val String name
val String filename
val int entries
val double averageKeySize
val double averageValueSize
// Constructors elided...
override openStorage() {
ChronicleMapBuilder.
of(String, String).
name(this.name).
entries(this.entries).
averageKeySize(this.averageKeySize).
averageValueSize(this.averageValueSize).
createPersistedTo(new File(filename))
}
}
In addition to the “real” ChronicalMap-based implementation we also define a volatile, in-memory implementation of StorageProvider
useful for testing. This trivial implementation accompanies the interface declaration in the same source file:
// File StorageProvier.xtend
interface StorageProvider {
def Map<String, String> getStorage()
}
class InMemoryStorageProvider
implements StorageProvider {
override openStorage() {
new ConcurrentHashMap<String, String>
}
}
Notes about Annotations, Constructors and Instantiation
For both UrlShortener
and ChronicleMapStorageProvider
we’ve used the @Buildable
active annotation to give us a named-parameter way of instantiating classes having an all-property constructor.
Thanks to @Buildable
, for example, we can instantiate ChronicleMapStorageProvider
like:
val provider =
ChronicleMapStorageProvider.builder.
name('test').
filename(/var/storage/url-shortener.dat).
entries(1048576).
averageKeySize(32).
averageValueSize(64).
build
or, alternatively,
val builder =
ChronicleMapStorageProvider.builder => [
name = 'test'
filename = '/var/storage/url-shortener.dat'
entries = 1048576
averageKeySize = 32
averageValueSize = 64
]
val provider = builder.build
@Buildable
goes along especially well the @Data
active annotation.
@Data
is most appropriate for data-only, immutable classes. All fields are deemed final (val
) and an all-field constructor and getters are generated. Thus, if we have a data class like:
@Data
class PersonName {
String firstName
String middleName
String lastName
}
then the augmented Xtend class would be:
class PersonName {
val String firstName
val String middleName
val String lastName
new(String firstName,
String middleName,
String lastName) {
this.firstName = firstName
this.middleName = middleName
this.lastName = lastName
}
String getFirstName() { firstName }
String getMiddleName() { middleName }
String getLastNameName() { lastName }
}
Note, incidentally, that Xtend constructors do not replicate the class name. Instead they’re always called
new
.
The generated Java class would be:
public class PersonName {
private final String firstName;
private final String middleName;
private final String middleName;
public PersonName(String firstName,
String middleName,
String middleName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName
}
public String getFirstName() {
return firstName;
}
public String middleName() {
return middleName;
}
public String lastName() {
return lastName;
}
}
When the data class has too many fields invoking its all-property constructor can become cumbersome and error-prone:
val personName =
new PersonName('Johnathan',
'Christopher',
'Aguillard')
@Buildable
comes to the rescue by generating a builder where instantiation looks like:
val personName = PersonName.builder.
firstName('Johnathan').
middleName('Christopher').
lastName('Aguillard').
build
Named-parameter style, much more readable and intuitive!
If someone has no middle name, then instead of passing null
for the middleName
property we simply omit it in the “with” block:
val personName = PersonName.builder.
firstName('John').
lastName('Doe').
build
Final Notes about Private Fields and Constructors and SnakeYAML
As a rule of thumb, we don’t want our interface-implementing classes to have publicly visible state. We also want them to be immutable. Consequently, we don’t want getter/setter accessors that make the classes mutable and their internals visible.
We achieve this by defining an all-property constructor accompanied by a builder providing named parameter-style instantiation.
This approach, however, is inconsistent with the default approach followed by SnakeYAML which is based on the Java Beans specification. This spec requires an empty constructor and getters and/or setters for each bean property.
Fortunately, SnakeYAML has a BeanAccess.FIELD
mode that works with empty private constructors as well as with private fields corresponding to each property.
This style allows us to write Yam files like:
shortener: !!net.xrrocha.urlshortener.shortening.Murmur3Shortener []
storageProvider: !!net.xrrocha.urlshortener.storage.ChronicleMapStorageProvider
name: url-shortener
filename: target/url-shortener.dat
entries: 1048576
averageKeySize: 32
averageValueSize: 64
which makes it look as if we were using regular bean properties but, behind the scenes, uses reflection wizardry to instantiate classes through their private constructors and populate their private fields.
Xtend Shortcomings
Xtend currently has a few limitations that make it necessary to implement certain constructs in Java
Enumerations, for one, only allow for declaring their constants. Xtend enumerations cannot implement interfaces or have fields and methods of their own:
enum Color {
GREEN,
BLUE,
RED
}
If you need you enumeration to implement interfaces or have fields or methods you have to resort to good ole’ Java.
Also, inner classes can only be static:
class MyClass {
static class NestedClass {}
}
This can be alleviated by adding the enclosing this
as a field in the inner class but that is grunt work.
Finally, Xtend has no notion of methods references. However, its own lambda syntax is so intuitive and concise that this is not really a problem.
While in java we write:
Runtime.getRuntime().
addShutdownHook(urlShortener::stop)
in Xtend we write:
Runtime.runtime.addShutdownHook(
new Thread[urlShortener.stop])
Conclusion
This concludes our tour of Xtend around a simple but meaningful example.
Since Xtend compiles to (readable) Java, it has exactly the same semantics as Java. This means Xtend is fully compatible with all Java frameworks and libraries out there.
If you want to try Xtend in your real-world projects, testing is a good start. Collections literals, for instance, make populating fixtures a breeze and even allow for improvising test data on-the-fly inside test methods.
Java programmers can write working Xtend code after a few hours hours of study. Mastering the finer details of the language comes very quickly because it could be seen, in the end, as Java sàns the cruft: Xtend is all about removing unnecessary verbosity and maximizing clarity of intent.
Also, Xtend is an Eclipse project, very likely to last, evolve and be supported for the times ahead.
Code for the Java and Xtend implementations of our URL shortener reside under a single Bitbucket repo. The implementations in the repo are a bit more elaborate than the somewhat simplified versions shown in this post series. In general, though, the snippets presented here do reflect the actual code in the repo. The repo contains unit and integration tests illustrating how to leverage Xtend syntax and conciseness for testing Java code.