There is already much you can do while in Golo land using functions, closures, structs, augmentations and dynamic objects.
Yet, the JVM is a wider ecosystem and you will soon be tempted to integrate existing Java libraries into your code. Calling Java libraries from Golo is quite easy, but what happens when you need to subclass classes or provide objects that implement specific interfaces?
As you can easily guess, this is all what adapters are about: they allow the definition of objects at runtime that can extend and inherit Java types.
Let us get started with a simple example of a web application based on the nice Spark micro-framework [1].
Spark requires route handlers to extend an abstract base class called spark.Route
. The following
code snippet does just that:
module sparky
import spark
import spark.Spark
function main = |args| {
let conf = map[ # (1)
["extends", "spark.Route"], # (2)
["implements", map[ # (3)
["handle", |this, request, response| { # (4)
return "Golo, world!"
}]
]]
]
let fabric = AdapterFabric() # (5)
let routeMaker = fabric: maker(conf) # (6)
let route = routeMaker: newInstance("/hello") # (7)
get(route) # (8)
}
-
An adapter configuration is provided by a map object.
-
The
extends
key allows specifying the name of the parent class (java.lang.Object
by default). -
The
implements
provides a map of method implementations. -
The implementation is given by a closure whose signature matches the parent class definition, and where the first argument is the receiver object that is going to be the adapter instance.
-
An adapter fabric provides context for creating adapters. It manages its own class loader.
-
An adapter maker creates instances based on a configuration.
-
The
newInstance()
method calls the right constructor based on the parent class constructors and provided argument types. -
The
spark.Spark.get()
static is method is happy as we feed it a subclass ofspark.Route
.
Note
|
Adapter objects implement the gololang.GoloAdapter marker interface, so you can do type
checks on them a in: (foo oftype gololang.GoloAdapter.class) .
|
This is as easy as providing a java.lang.Iterable
as part of the configuration:
link:{tests-dir}/for-execution/adapters.golo[role=include]
-
As you may guess, this changes the
result
array values to[11, 12, 13]
.
Implementations are great, but what happens if you need to call the parent class implementation of a
method? In Java, you would use a super
reference, but Golo does not provide that.
Instead, you can override methods, and have the parent class implementation given to you as a method handle parameter:
link:{tests-dir}/for-execution/adapters.golo[role=include]
println(val: toString()) # (1)
-
This prints something like:
>>> $Golo$Adapter$0@12fc7ceb
.
Note
|
You can mix both implementations and overrides in an adapter configuration. |
You can pass *
as a name for implementations or overrides. In such cases, the provided closure
become the dispatch targets for all methods that do not have an implementation or override. Note
that providing both a star implementation and a star override is an error.
Let us see a concrete example:
link:{tests-dir}/for-execution/adapters.golo[role=include]
-
We create an empty list, more on that later.
-
A star override takes 3 parameters: the parent class implementation, the method name and the arguments into an array (the element at index 0 is the receiver).
-
We copy into
carbonCopy
. -
Same here, but we dispatch to a different method
-
We just call the parent class implementation of whatever method it is. Note that
spread
allows to dispatch a closure call with an array of arguments. -
At this point
carbonCopy
contains["foo", "bar", "baz"]
(and so doeslist
, too).
The case of star implementation is similar, except that the closure takes only 2 parameters:
|name, args|
.
The AdapterFabric
constructor can also take a class loader as a parameter. When none is provided,
the current thread context class loader is being used as a parent for an AdapterFabric
-internal
classloader. There is also a static method withParentClassLoader(classloader)
to obtain a fabric
whose class loader is based on a provided parent.
As it is often the case for dynamic languages on the JVM, overloaded methods with the same name but
different methods are painful. In such cases, we suggest that you take advantage of
star-implementations or star-overrides as illustrated above on a ArrayList
subclass where the 2
add(obj)
and add(index, obj)
methods are being intercepted.
Finally we do not encourage you to use adapters as part of Golo code outside of providing bridges to third-party APIs.
This is another way to use the adapters. You can see that as a kind of DSL for the Golo adapters. Let’s see how to re-write the examples in the previous paragraph
Let us get started (again) with a simple example of a web application based on the Spark micro-framework.
module sparky
import gololang.Adapters
import spark
import spark.Spark
function main = |args| {
let sparkRouteAdapter = Adapter() # (1)
: extends("spark.Route") # (2)
: implements("handle", |this, request, response| { # (3)
return "Golo, world!"
})
let route = sparkRouteAdapter: newInstance("/hello") # (4)
get(route) # (5)
}
-
An adapter factory.
-
The
extends
method specifies the name of the parent class (java.lang.Object
by default). -
The
implements
method specifies the method implementations. The implementation is given by a closure whose signature matches the parent class definition, and where the first argument is the receiver object that is going to be the adapter instance. -
The
newInstance()
method calls the right constructor based on the parent class constructors and provided argument types. -
The
spark.Spark.get()
static is method is happy as we feed it a subclass ofspark.Route
.
link:{tests-dir}/for-execution/adapters-helper.golo[role=include]
-
As you may guess, this changes the
result
array values to[11, 12, 13]
.
link:{tests-dir}/for-execution/adapters-helper.golo[role=include]
println(val) # (1)
-
This prints something like:
>>> $Golo$Adapter$0@12fc7ceb
.