#!admin/users/id/4711/profile/activeTabsheet/address
This library provides a framework for mapping URI fragments as found in URIs, such as http://www.example.com/shop#!signin
, on custom action command objects. For example, the URI fragment signin
could be mapped on an action class GoToSignInPageAction
which will be executed when the user visits this address.
This is useful for single-page applications where the application state is typically encoded in the URI fragment. For example, the following URI might be used by an application to show a product detail page in some web shop.
Here the part !products/group/books/id/4711/view
forms the URI fragment that carries the application’s current state. As you can see, such a URI fragment consists of a directory-like path structure. This structure may additionally contain parameter values such as identifiers, categories, or geographic coordinates. Since visited URI fragments are pushed to the browser history, they allow the development of navigable web applications that support the browser back button and deep linking.
This document describes how the library uri-fragment-routing
is to be used.
Let’s first get an overview of the library’s most important classes and interfaces which you should know. The following classes and interfaces play a central role in the framework:
UriActionMapperTree
: The main class of the framework which manages the complete set of URI fragments used by an application. This class is responsible for interpreting any given URI fragment, and it is able to create a fully parameterized URI fragment to be used for hyperlinks.
UriPathSegmentActionMapper
: This interface defines an action mapper. These are classes which are responsible for handling exactly one particular path segment of any given URI fragment. For example, the URI fragment #!admin/users
contains two path segments admin
and users
which are each handled by one action mapper, respectively.
UriActionCommand
: Interface for defining action command objects. These command objects will be executed as a result of the URI fragment interpretation process. A URI fragment can be resolved to a UriActionCommand
object by the UriActionMapperTree
. This command object will eventually be executed after the URI fragment has been successfully interpreted.
UriActionCommandFactory
: A functional interface which creates new instances of action command objects of one specific type.
UriParameter
: A URI fragment can contain an arbitrary number of parameter values. These parameters are represented by classes implementing the interface UriParameter
.
ParameterValueConverter
: Each URI parameter has a specific type. Since parameters are represented in a URI fragment as a String value, a conversion needs to take place between a parameter’s type and this String representation. Classes implementing the ParameterValueConverter
interface take care of this.
ParameterValue
: When a URI fragment has been interpreted by the UriActionMapperTree
, all parameter values contained in the fragment have been extracted and converted into their respective type. ParameterValue
is a generic class which manages a converted parameter value in a type-safe way.
CapturedParameterValues
: The complete set of parameter values found in a URI fragment is managed by this class.
We will take a closer look at these classes and interfaces in the following sub-sections to get a better understanding of what they can do for you.
All URI fragments an application can handle have to be defined with an object of class UriActionMapperTree
. This class is then responsible for two main tasks:
Interpreting a concrete URI fragment visited by the user. The mapper tree tries to resolve any given URI fragment to a UriActionCommand
. If such a command object could be determined for a URI fragment, it is executed by the mapper tree. An instance of these action commands will be created using a UriActionCommandFactory
.
Assembling a valid URI fragment which can be later resolved to a particular action command by the process described above. A set of parameter values can be provided to be added to the assembled fragment so that current application state can be encoded in the URI fragment.
The UriActionMapperTree
is thread-safe. This means that only one instance of a mapper tree needs to be instantiated per application. You should try to avoid creating a mapper tree for every user session.
Creating and configuring a mapper tree can be done in two ways. You can either assemble all necessary parts by hand or you can use a builder class provided by UriActionMapperTree
. The latter option is recommended, since the builder infrastructure leads you through the process of assembling the hierarchical structure of a mapper tree with a fluent API. This fluent API at any time allows to use only those operations which make sense in the current context.
Both variants will be described in detail in the remainder of this manual.
When a URI fragment is interpreted, it is first disassembled into a set of distinct tokens. For example, the following URI fragment
#!admin/users/id/4711/profile/activeTabsheet/address
will be disassembled into the following list of tokens:
["admin", "users", "id", "4711", "profile", "activeTabsheet", "address"]
This list is then interpreted by the mapper tree. Parts of this list represent parameter key-value pairs (here ["id", "4711"] and ["activeTabsheet", "address]). The other parts make up the directory-like, hierarchical URI fragment structure (here ["admin", "users", "profile"]). For each of the directory parts (path segments), there is one instance of a UriPathSegmentActionMapper
which is responsible of handling this part. These action mappers have different tasks. The task of the inner segments is to pass the interpretation process on to the action mapper responsible for the next hierarchy level. For instance, the action mapper responsible for the admin
path segment will pass the interpretation process on to the action mapper for the users
path segment.
The second task of these action mappers is to provide a UriActionCommandFactory
object if there is no further sub-mapper available to which the interpretation process could be passed. In the example, the action mapper which is in charge for the profile
path segment would return an action command factory creating command objects which display the profile view in the user administration area when executed.
There are two basic implementations available for the UriPathSegmentActionMapper
interface:
A DispatchingUriPathSegmentActionMapper
is an action mapper which is responsible for the inner path segments. They will pass the interpretation process on to the next level. In the example, the action mappers for admin
and users
would be implemented with this class.
A SimpleUriPathSegmentActionMapper
is responsible for the leaf path segments which do not have any sub-mappers. In the example, the profile
path segment would be handled by this mapper type. The only task of SimpleUriPathSegmentActionMappers
is to provide a URI action command factory.
You can assign different parts of the URI fragment hierarchy a factory which creates instance of classes which implement the org.roklib.urifragmentrouting.UriActionCommand
interface. This interface is derived from java.lang.Runnable
and thus implements the Command Design Pattern. When a URI fragment is resolved to a URI action command factory, this factory will be used to create a new instance of an action command. This action command is subsequently executed as a result of the interpretation process.
Each action command needs some context before it can be executed. For example, an action command needs to know the URI parameter values which have been extracted from the URI fragment. This context data can be injected into the action command object by the mapper tree on demand. For this, you can add setter methods to your action command classes which are annotated with one of the following annotations: AllCapturedParameters
, CapturedParameter
, CurrentUriFragment
, and RoutingContext
.
We will learn about these annotations and how to implement URI action commands in section Writing URI Action Commands.
Action command factories are responsible for creating new instances of action commands. The interface UriActionCommandFactory
is a functional interface, so it can be defined using a lambda expression. The very simplest definition of a UriActionCommandFactory
is to define a constructor method reference for a UriActionCommand
class. For example, an action command factory which is responsible for creating command objects of type MyActionCommand
can be defined like this:
UriActionCommandFactory myActionCommandFactory = MyActionCommand::new;
When you want to add parameter values to your URI fragments, you need to define a parameter object for every parameter you want to use. URI parameters are represented by classes implementing the org.roklib.urifragmentrouting.parameter.UriParameter
interface. Parameter objects define the parameter’s data type (e. g. Integer, Date, or Double) and the parameter’s id. The id will be used to identify the parameter in the URI fragment. You will then only work with these type-safe parameter objects so that you don’t have to hassle with String values which need to be converted into the correct data type before they can be used. The data conversion between a parameter’s String representation and its typed value is taken care of by parameter value converters. Such a converter implements the interface org.roklib.urifragmentrouting.parameter.converter.ParameterValueConverter
. The framework provides parameter and converter implementations for the standard data types. Of course, you can define your own set of parameters and converters for other data types.
A URI parameter can be single-valued or multi-valued. Typical examples for single-valued parameters are entity ids, user names or boolean flags. A multi-valued parameter is represented by a single instance of a UriParameter
but consists of more than one parameter value. An example for such a type of parameters is a geographic coordinate which consists of a longitude and a latitude. With class org.roklib.urifragmentrouting.parameter.Point2DUriParameter
, the framework provides such a parameter out of the box.
When a parameterized URI fragment has been interpreted, all parameter values extracted from that URI fragment need to be transported to the UriActionCommand
which is executed as a result of the interpretation process. In addition to the typed parameter value, some more information needs to be transmitted with the parameter value. If a required parameter value could not successfully be extracted from the URI fragment, information about the concrete error needs to be preserved. If a URI parameter value is not present in the URI fragment but the parameter object defines a default value, this default value will be transmitted instead. This value then needs to be marked as such.
In order to be able to aggregate this information, a specific class org.roklib.urifragmentrouting.parameter.value.ParameterValue<V>
is used. This is a generic class whose generic type is set to the data type of the parameter. In addition to the converted parameter value extracted from the URI fragment, it also contains information about whether or not the parameter extraction was successful. This class also indicates with a boolean flag if the contained value is the parameter’s default value.
The framework supports three different types of parameter representations:
Directory mode with names
Directory mode without names
Query parameter mode
Using the enumeration org.roklib.urifragmentrouting.parameter.ParameterMode
you can specify in what mode the URI action mapper tree shall operate.
Let’s define these modes.
In this mode, parameter values are contained in a URI fragment in a directory-like format. Their parameter ids are also contained in the URI fragment. Example:
#!admin/users/id/4711/showHistory/startDate/2017-01-01/endDate/2017-01-31
This URI fragment contains three parameters: id
, startDate
and endDate
. As you can see, the parameters' ids are contained in the URI fragment together with their concrete values.
This mode operates similar to the previous one, with the difference that the parameters' ids are not contained in the URI fragment. In this mode, the example above looks like follows:
#!admin/users/4711/showHistory/2017-01-01/2017-01-31
When this mode is used, parameters must not be defined as optional (i. e. having a default value). Otherwise, a missing parameter value could not be distinguished from the consecutive URI fragment tokens not belonging to a parameter instance.
In this mode, all URI parameters are appended to the URI fragment in the same way as customary URI query parameters are appended to a URI (as described by RFC 3986). The above example will look like follows with this mode:
#!admin/users/showHistory?id=4711&startDate=2017-01-01&endDate=2017-01-31
When this mode is used, a parameter’s identifier must only be used once per action mapper tree. This is because a concrete parameter value could not be assigned to the correct action mapper otherwise.
Now that we have learned about the basic classes and concepts of this library, we’ll put our knowledge to use and start building URI action mapper trees. We will start small and construct the simplest possible mapper tree.
In this section, we will build a mapper tree which is able to handle the following URI fragment:
#!helloWorld
When the user visits this fragment, we want to print Hello World!
to the console. To do this, we need two things: we have to define an action class and the URI action mapper tree which can resolve this URI fragment to this action command.
Let’s first define the action class:
public static class HelloWorldActionCommand implements UriActionCommand {
@Override
public void run() {
System.out.println("Hello World!);
}
}
That was easy. Now we can build the URI action mapper tree.
UriActionMapperTree mapperTree =
UriActionMapperTree.create().buildMapperTree()
.map("helloWorld").onActionFactory(HelloWorldActionCommand::new)
.finishMapper().build();
To do so, we use the builder provided to us by UriActionMapperTree.create()
. This builder will guide us through the complete process of creating and configuring the full URI action mapper tree. We start the building process with buildMapperTree()
. A mapper tree is built in a depth-first manner. That is, we start with the first level of the directory-like URI fragment structure (#!firstLevel
) and continue building the sub-levels from there (#!firstLevel/secondLevel
). We will learn how to do that in the following sections.
In our simple example, we only want to map a single path segment on an action command (or more specifically, an action command factory). We do this with the map()
method. This method will create a SimpleUriPathSegmentActionMapper
for us. We set the action command factory for this mapper with the onActionFactory()
method. When we’re done configuring the current action mapper, we finalize it with finishMapper()
. After this method has been called for the current action mapper, we cannot add any further sub-mappers to it. However, this would not be possible in our example anyway, since we created a simple action mapper which does not support sub-mappers. Simple action mappers represent the leaves of the mapper tree.
When we finished composing the URI action mapper tree, we finalize the tree with the build()
method. This will return the fully configured UriActionMapperTree
ready for action.
How can we now interpret URI fragments visited by the user with this mapper tree?
This is done with the interpretFragment()
method. We can pass a String holding the current URI fragment to this method:
UriActionCommand command = mapperTree.interpretFragment("helloWorld");
This will trigger the interpretation process during which the URI fragment is disassembled and resolved to a URI action command factory. The action mapper tree will resolve this fragment to the command factory provided by us during the construction of the mapper tree: HelloWorldActionCommand::new
(this is a lambda expression consisting of a simple constructor method reference for class HelloWorldActionCommand
). It will then create an instance of this action command class using the factory, execute it and return the command object as a result.
If the given URI fragment could not be resolved (e. g., if we made a typo and passed heloWrold
to the interpretation method), null
is returned and no action command object is executed.
With this, we have successfully created a very simple but fully functional URI action mapper tree which is able to handle one particular URI fragment.
As we have seen in the previous section, if a URI fragment could not successfully be interpreted, null
is returned from the interpretation process as a result. We can prevent this by defining a default action command factory which will be used each time a URI fragment could not be successfully resolved. We can set this on the instance of the URI action mapper tree:
mapperTree.setDefaultActionCommandFactory(MyDefaultActionCommand::new);
or alternatively while building this tree with the builder objects:
mapperTree = UriActionMapperTree.create()
.useDefaultActionCommandFactory(MyDefaultActionCommand::new)
.buildMapperTree()
...
Now we have a good starting point from which we can go on. We will expand our mapper tree with more mappers in the next step. Let us define the following three top-level path segments which the mapper tree can handle (we’re dropping the mapper for the helloWorld
path segment and start over with a new mapper tree):
#!user #!admin #!settings
We can do this in the same way as we did above, except that we continue building the mapper tree after we have fully configured the first action mapper:
mapperTree = UriActionMapperTree.create().buildMapperTree()
.map("user").onActionFactory(GoToUserAreaActionCommand::new).finishMapper()
.map("admin").onActionFactory(GoToAdminAreaActionCommand::new).finishMapper()
.map("settings").onActionFactory(GoToSettingsActionCommand::new).finishMapper()
.build();
As you can see, after we called finishMapper()
, the builder is reset to the root of the mapper tree and we can go on adding the next sibling path segment to be handled by the mapper tree.
Next we want to add some hierarchy to the mapper tree. We would now like to be able to interpret the following URI fragments:
#!user #!user/profile #!admin #!admin/users #!admin/groups #!settings
We will do this in a depth-first manner:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("user").onActionFactory(GoToUserAreaActionCommand::new)
.onSubtree()
.map("profile").onActionFactory(GoToUserProfileActionCommand::new).finishMapper()
.finishMapper()
.mapSubtree("admin").onActionFactory(GoToAdminAreaActionCommand::new)
.onSubtree()
.map("users").onActionFactory(GoToUserAdministrationActionCommand::new).finishMapper()
.map("groups").onActionFactory(GoToGroupAdministrationActionCommand::new).finishMapper()
.finishMapper()
.map("settings").onActionFactory(GoToSettingsActionCommand::new).finishMapper()
.build();
The user
and admin
path segments now need to have their type changed from a simple action mapper into a dispatching action mapper which allows adding sub-mappers. This is reflected by the builder methods we have to use now: mapSubtree()
initiates the construction of a sub-mapper hierarchy. We can still assign an action command factory to this mapper. This factory will be used when the dispatching action mapper is directly accessed with a URI fragment, e. g. #!user
.
After the dispatching mapper has been fully configured, we can go to the next hierarchy level and configure the dispatching mapper’s sub-mappers. We initiate the construction of this sub-tree with method onSubtree()
. From this point on, we can continue with constructing the mapper tree on the next level as we did on the root level. As we can see, we are here dealing with a recursive structure. We can now use the same builders as we did on the root level. We can thus nest the action mappers as deeply as we like.
In our example, we only add simple action mappers on the second level of the action mapper tree. We could, however, choose to add a second level of dispatching action mappers and a third level of simple mappers and so on by repeatedly using mapSubtree()
.
It is important to note that method finishMapper()
will leave the current level of nesting and move the builder’s "cursor" up to the parent level. This is why we have to call finishMapper()
twice after we configured the action mapper for profile
. The first call to finishMapper()
moves the cursor up to the level of user
, while the second call moves it back to the root level.
Until now, we have defined action mappers for a unique set of path segment names. The following path segment names are currently in use by our action mapper tree: user
, profile
, admin
, users
, groups
, and settings
.
What happens when we reuse one of the path segment names? Let’s add a profile
sub-mapper for the admin
dispatching mapper:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("user").onActionFactory(GoToUserAreaActionCommand::new)
.onSubtree()
.map("profile").onActionFactory(GoToUserProfileActionCommand::new).finishMapper()
.finishMapper()
.mapSubtree("admin").onActionFactory(GoToAdminAreaActionCommand::new)
.onSubtree()
.map("profile").onActionFactory(GoToUserAdministrationActionCommand::new).finishMapper()
... // remainder omitted for brevity
This gives us the following URI fragment structure:
#!user #!user/profile #!admin #!admin/profile #!admin/users #!admin/groups #!settings
When we try to build this mapper tree, the following exception will be thrown:
java.lang.IllegalArgumentException: Mapper name 'profile' is already in use
What does that mean? While this URI fragment structure is perfectly valid, we are not allowed to construct it in the way shown. We must not reuse the mapper name which is defined with the methods map()
and mapSubtree()
. This mapper name serves as a unique identifier of a URI action mapper object. Therefore, a mapper name must only be used once per action mapper tree.
What we need to do in this case is to separately define the mapper name for the action mapper and the path segment for which it is responsible. The methods map()
and mapSubtree()
we used until now conveniently set these two values to the same String, which is the one we passed as a parameter to these methods. We now have to do without this convenient feature and define both the mapper name and the path segment name for which the mapper is responsible separately:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("user").onActionFactory(GoToUserAreaActionCommand::new)
.onSubtree()
.map("profile").onActionFactory(GoToUserProfileActionCommand::new).finishMapper()
.finishMapper()
.mapSubtree("admin").onActionFactory(GoToAdminAreaActionCommand::new)
.onSubtree()
.map("adminProfile").onPathSegment("profile")
.onActionFactory(GoToAdminProfileAreaActionCommand::new).finishMapper()
... // remainder omitted for brevity
Now we define the mapper name (the mapper’s id) with the map()
method and set the path segment name for which the mapper is responsible with onPathSegment()
in the next step. When we create a dispatching mapper with mapSubtree()
we can define the path segment name with the overloaded variant of mapSubtree()
:
mapSubtree("adminArea", "admin").onActionFactory(GoToAdminAreaActionCommand::new)
Here we define the mapper name adminArea
for the path segment name admin
.
We have now defined a hierarchy of URI action mappers which consists of sub-tree mappers and simple mappers for the hierarchy’s leaf nodes. Next, we want to add parameters to this hierarchy. We can add parameters to every level of the action mapper tree. The library provides a number of predefined URI parameter classes that can be used out of the box. If these classes don’t cover all of your use cases, you can easily write your own parameter classes. This is described in section Providing Custom Parameter Types.
As we have learned previously, URI fragment parameters can be single-valued or multi-valued. Regardless of the number of individual values a parameter is composed of, a parameter is always represented by a class which implements the org.roklib.urifragmentrouting.parameter.UriParameter
interface. In order to uniquely identify a parameter, a parameter object needs to be instantiated with a unique identifier. Besides this id, a URI fragment parameter has to be given one parameter name per individual parameter value.
Let’s look at a simple example. A single-valued parameter of type Integer is represented by class org.roklib.urifragmentrouting.parameter.SingleIntegerUriParameter
. Since single-valued parameters only have one parameter value, the value’s name and the parameter’s id are the same. We can create a SingleIntegerUriParameter
as follows:
SingleIntegerUriParameter parameter = new SingleIntegerUriParameter("userId");
Here, the parameter uses the String userId
both as parameter name (which will be visible in a URI fragment) and as its identifier.
This is different with multi-valued parameters. We create an instance of org.roklib.urifragmentrouting.parameter.Point2DUriParameter
as an example. This type of parameter can be used for setting a two-dimensional coordinate value in a URI fragment. This is typically used for geographic coordinates.
Point2DUriParameter locationParameter = new Point2DUriParameter("location", "lat", "lon");
The constructor of Point2DUriParameter
takes three Strings: the first String defines the parameter’s identifier, which is location
in the example. This value will never be visible in a URI fragment. The next two Strings define the parameter names of the two parameter values. These values will be visible in a URI fragment. This could look like the following example:
#!address/showOnMap/lat/49.563044/lon/8.708351
When this URI fragment is interpreted, a single instance of a Point2DUriParameter
will hold the two parameter values for the longitude and latitude.
URI parameters can be added to every URI action mapper regardless of their type. In this section we want to build an action mapper tree which can interpret the following URI fragment:
#!products/id/4711/details
In this example, we have a single-valued parameter of type Integer with the parameter name id
. We can build the action mapper tree as follows:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("products").withSingleValuedParameter("id").forType(Integer.class).noDefault()
.onSubtree()
.map("details").onActionFactory(ShowProductDetailsActionCommand::new).finishMapper()
.finishMapper()
.build();
For this action mapper tree, we define one dispatching action mapper products
and one simple action mapper details
. For the first action mapper, we define a single-valued, Integer-typed parameter with parameter name id
. Since Integer
is a standard data type, we can use a builder for constructing the URI parameter with a fluent API.
Note that we do not specify a URI action command factory for the products
-mapper. That’s because we do not support visiting the products
URI fragment on its own. A value for the id
parameter is required in the URI fragment followed by the details
path segment.
Single-valued URI parameters for standard data types are built with the builder returned by the withSingleValuedParameter()
method. We pass this method the parameter identifier we want to use for this parameter. Next we have to specify the parameter’s data type with forType()
. We pass this method the class object for the desired data type. Currently, the following data types are supported by this builder: String
, Integer
, Long
, Float
, Double
, Boolean
, java.util.Date
, and java.time.LocalDate
. If you pass a class object which is not supported, an IllegalArgumentException
is thrown.
After having specified the parameter’s data type, we need to define whether or not the URI parameter has a default value. In our example, we do not want to specify a default value and therefore call method noDefault()
to ascertain that. Refer to the next section to learn about how default values are defined and when they are used.
Next we want to provide our own parameter object. We will need to take this approach when we want to add parameter types which are not supported by the convenience withSingleValuedParameter()
builder method. In the next example, we want to build an action mapper tree which can interpret the following URI fragment:
#!shopLocation/lat/49.563044/lon/8.708351
We use the following builder configuration to do that:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.map("shopLocation")
.onActionFactory(MyActionCommand::new)
.withParameter(new Point2DUriParameter("coordinates", "lat", "lon"))
.finishMapper()
.build();
As you can see, we can specify our own URI parameter instance with method withParameter()
. We can simply pass a parameter object to this method which we have configured beforehand.
Note that you can also register a URI fragment parameter on the root mapper of the mapper tree. By that, all your URI fragments will be prefixed with the parameter values of the root mapper (depending on the ParameterMode
you use). This is explained in section Configuring the URI Action Mapper Tree.
When you specify a default value for a URI parameter, this value is assumed for the parameter if no concrete value could be found for it in a URI fragment. The parameter becomes effectively optional.
To set a default value for a parameter object directly, you can use method org.roklib.urifragmentrouting.parameter.UriParameter#setOptional()
. Using the builders, you can define a default value with the usingDefaultValue()
method. If we wanted to define the product id in our example above to be 0 by default, we can do this with the following code:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("products")
.withSingleValuedParameter("id").forType(Integer.class).usingDefaultValue(0)
.onSubtree()
.map("details").onActionFactory(ShowProductDetailsActionCommand::new).finishMapper()
.finishMapper()
.build();
It is important to note that you must not use optional URI parameters when you want your URI action mapper tree to operate in the DIRECTORY
parameter mode. In this mode, only the parameter values are contained in a URI fragment and not their parameter names. If there are optional URI parameters defined for such an action mapper tree, the mapper tree could not determine whether or not a value is missing for some optional parameter which would confuse the URI fragment interpretation process.
Now that we have defined the URI parameters available in our URI fragment structure, the question arises how we can access the concrete parameter values extracted from the currently interpreted URI fragment.
When a URI fragment is interpreted by the URI action mapper tree, all parameter values found in the URI fragment are automatically converted into their respective data type and collected in an object of class org.roklib.urifragmentrouting.parameter.value.CapturedParameterValues
. This class provides a storage for URI fragment parameter values which allows querying for particular parameter values using an action mapper name and a parameter id.
We can obtain the CapturedParameterValues
object for the currently interpreted URI fragment in our URI action commands so that we have full access to all parameter values in our action commands. It will be described in one of the next sections how we can obtain an object of this type in our action command classes.
In this section, we will first learn how we can work with class CapturedParameterValues
.
When we have access to a CapturedParameterValues
object containing the URI parameter values extracted from the current URI fragment, we can query this object with a number of methods:
isEmpty()
returns true
if the CapturedParameterValues
object does not contain any parameter values.
hasValueFor(String, String)
lets us query whether there is a parameter value available for a particular action mapper and parameter. The first argument of this method specifies the URI action mapper name for which the desired parameter is defined. The second argument specifies the identifier of the desired URI fragment parameter. We could, for example, query if a product id has been given for the example action mapper tree from the previous section using capturedParameterValues.hasValueFor("products", "id")
. Here, products
identifies the dispatching action mapper and id
identifies the single-valued, Integer-typed URI fragment parameter defined for it.
getValueFor(String, String)
returns the requested parameter value for a particular action mapper and parameter. The first two method arguments specify the same identifiers as method hasValueFor()
. If no parameter value is available for the given ids, null
is returned.
The library provides some parameter classes which may come in handy in specific situations.
This class represents a single-valued parameter of type Long
which ignores any text that is appended to a number. It handles parameter values consisting of two parts: a numerical value (interpreted as a number of type Long
) and a textual suffix. This is useful if you want to use an identifier value (e. g. the primary key of some item) including a human-readable textual description of the referred item as a URI fragment parameter. Consider, for example, a blogging software where individual blog posts are referred in the URI fragment by their database id. In order to give the users more context about the referred blog posts, a title can be added to the blog post’s id. Example:
#!posts/67234-my-first-blog-post
The data type of this URI fragment parameter is org.roklib.urifragmentrouting.parameter.SingleLongWithIgnoredTextUriParameter.IdWithText
.
This is a URI fragment parameter that takes a list of Strings as its value. The String list is converted by a org.roklib.urifragmentrouting.parameter.converter.StringListParameterValueConverter
. This converter converts a String into a String list by splitting a String around semicolons.
For example, the String
foo;bar%2Fbaz;foo%3Bbar
is converted into the list
["foo", "bar/baz", "foo;bar"]
As you can see, characters with a special meaning in a URI fragment (';' and '/') are transparently encoded and decoded.
The data type of this URI fragment parameter is List<String>
. Using this parameter, you can encode String lists as parameter values.
This URI parameter is used to put two-dimensional coordinates as a parameter pair into a URI fragment. A Point2DUriParameter
consists of two values which are of domain type Double
. These two values represent an x- and a y-coordinate, respectively. The data type of the URI fragment parameter itself is java.awt.geom.Point2D.Double
.
This parameter class is useful if you want to put geographical coordinates into a URI fragment, for example.
When you create a new Point2DUriParameter
object, you have to provide the parameter’s id and two parameter names: one for the x-coordinate and one for the y-coordinate:
Point2DUriParameter coordinates = new Point2DUriParameter("coords", "lat", "lon");
Besides the URI fragment parameter classes provided by the library, you can of course write your own custom parameter classes. All URI fragment parameter classes need to implement interface org.roklib.urifragmentrouting.parameter.UriParameter<V>
. It is, however, only very rarely necessary to implement this interface directly. In the most cases you can derive from class org.roklib.urifragmentrouting.parameter.AbstractUriParameter<V>
. This class implements the interface with a default implementation which is adequate for most standard parameter types.
When you derive your parameter class from AbstractUriParameter<V>
, you have to provide three things:
The data type to be used by the parameter class. This is the generic type parameter V
.
A custom implementation of interface org.roklib.urifragmentrouting.parameter.converter.ParameterValueConverter
which converts Strings into the parameter’s data type and vice versa.
An implementation for the abstract method AbstractUriParameter#consumeParametersImpl()
. This method extracts all data belonging to the URI fragment parameter from a map of values and returns an object of class org.roklib.urifragmentrouting.parameter.value.ParameterValue<V>
which contains the parameter value converted into the particular data type. Refer to the JavaDoc comment of this method for details on how to implement this method. Class ParameterValue
is described in more detail in section Working with Extracted Parameter Values.
The parameter value converter class is stateless. It only needs to implement two methods for converting a String into the parameter’s data type and vice versa: convertToString()
and convertToValue()
.
convertToValue()
takes a String as its argument and returns an object of the parameter’s data type as a result. It may happen that the given argument value cannot be converted into the target data type. In this case, the conversion function is required to throw an exception of type org.roklib.urifragmentrouting.exception.ParameterValueConversionException
.
Let’s look at the implementation of LongParameterValueConverter’s
implementation of convertToValue()
:
@Override
public Long convertToValue(final String valueAsString) throws ParameterValueConversionException {
try {
return Long.valueOf(valueAsString);
} catch (final NumberFormatException e) {
throw new ParameterValueConversionException(
valueAsString + " could not be converted into an object of type Long", e);
}
As you can see, if a NumberFormatException
is caught, it is rethrown wrapped in a ParameterValueConversionException
.
A pattern commonly used by the parameter value converters provided by the library is to only have one static instance available of each converter class. This is the recommended approach to take when you implement a custom URI fragment parameter.
For instance, the construtor of class SingleBooleanUriParameter
is implemented as follows:
public SingleBooleanUriParameter(final String parameterName) {
super(parameterName, BooleanParameterValueConverter.INSTANCE);
}
Here, for the second parameter value of the super class’s constructor the static instance variable of BooleanParameterValueConverter
is given.
If you want to implement a multi-valued parameter class, you can refer to the implementation of Point2DUriParameter
to get an idea of how this could be done.
Now that we have seen how to build a URI action mapper tree, we want to know how to implement the action commands which are executed at the end of the URI fragment interpretation process.
All URI action command classes have to implement interface org.roklib.urifragmentrouting.UriActionCommand
. This interface is derived from java.lang.Runnable
, and it does not define any methods of its own. So, the one method we have to implement for action commands is run()
.
As we have seen in the beginning, when we build a URI action mapper tree, we provide the action command factory objects for our URI action command classes to the particular action mappers. It is important to note that we cannot set concrete instances of our action command classes (which may have been instantiated with new
) here. We must use the factory approach instead. The reason for this is that a URI action mapper tree needs to remain thread-safe. If we were able to set concrete action command instances in this tree, these instances would be shared between different threads which would make the UriActionMapperTree
inherently unsafe for usage in a multi-threaded scenario. Therefore, each URI fragment interpretation process has to run on its own private set of data. When a URI fragment is interpreted, a new action command object will be created with a command factory at the end of this process by the action mapper tree if the URI fragment could be successfully resolved. This command object is then executed and afterwards passed back to the caller of the action mapper tree’s interpretFragment()
method. All this happens with data objects which are local to the current thread and which will not be shared with other threads.
When an action command object is executed, the command will need some sort of context in most of the cases. This might be access to some application or session data, such as service references or user data. Additionally, an action command object needs to know the URI parameter values extracted from the current URI fragment. This type of context can be injected into an action command object by the action mapper tree. The next sections describe how this can be achieved.
When an action command object needs access to an application-specific piece of data, this data can be provided to the command object through a routing context object. This is a custom object which may contain all types of data necessary for an action command object to execute. A concrete routing context class is defined by the application developer.
Let’s look at an example which uses a custom MyApplicationRoutingContext
class to wrap data which is relevant for the action commands. We now need the possibility to pass along an object of this class to our action command objects. To do this, we can use an overloaded variant of method interpretFragment()
:
public <C> UriActionCommand interpretFragment(final String uriFragment, final C context)
As you can see, this is a generic method with a type parameter C
which denotes the type of the routing context object. So we could call this method like so:
MyApplicationRoutingContext ctx = new MyApplicationRoutingContext(...)
interpretFragment("current/uri/fragment", ctx);
The action mapper tree will then relay this context object to the action command to be executed.
Now the question arises, how can this routing context object be injected into the action command object? This is done by providing a setter method in the action command class which is annotated with org.roklib.urifragmentrouting.annotation.RoutingContext
:
public static class MyActionCommand implements UriActionCommand {
protected MyApplicationRoutingContext context;
@Override
public void run() {
...
}
@RoutingContext
public void setContext(final MyApplicationRoutingContext context) {
this.context = context;
}
}
By providing a setter method for our routing context class annotated with @RoutingContext
, we define the injection point for this data. After the action mapper tree has instantiated this action command class with an action command factory, it will scan the class for such a method and invoke it using the context object provided to the interpretFragment()
method. The action command object can then use this object in its run()
method.
Note that the setter method for the routing context must be public and must have exactly one argument with the correct type of the routing context class passed to interpretFragment()
. Otherwise, an exception is thrown.
There is more data which can be injected into action command objects. You can have the currently interpreted URI fragment provided to your action commands. This is done by writing a public setter method with a single, String-typed parameter. This setter method needs to be annotated with org.roklib.urifragmentrouting.annotation.CurrentUriFragment
:
public static class MyActionCommand implements UriActionCommand {
protected MyApplicationRoutingContext context;
@Override
public void run() {
...
}
@CurrentUriFragment
public void setCurrentUriFragment(final String currentUriFragment) {
LOG.info("Interpreting fragment: '" + currentUriFragment + "'");
}
}
Arguably the most important task of URI action commands is to interpret and process all URI fragment parameter values which have been extracted from the currently interpreted URI fragment. As we have seen in section Processing Parameter Values in an Action Command, we can obtain the set of all parameter values extracted from the current URI fragment in an object of type CapturedParameterValues
. Now the question is, how do we obtain an instance of this class?
Again, this is done by providing an annotated setter method. This time, we have the option to specify two different setter methods, depending on whether we need all parameter values from the URI fragment or only one particular value.
If we need all extracted parameter values, we can provide a public setter method with exactly one parameter of type CapturedParameterValues
. This method is annotated with @AllCapturedParameters
.
public static class MyActionCommand implements UriActionCommand {
@Override
public void run() {
...
}
@AllCapturedParameters
public void setCapturedValues(final CapturedParameterValues values) {
// process parameters
}
}
The object injected into this method will contain all URI fragment parameters extracted from the currently interpreted URI fragment. We can then access the individual parameter values as described in section Processing Parameter Values in an Action Command.
If we want to provide a setter method for just one particular parameter value, we can use annotation @CapturedParameter
. This annotation expects two arguments:
mapperName
specifies the action mapper name for which the required parameter is registered.
parameterName
specifies the name of the required parameter.
These two values correspond to the two arguments of method CapturedParameterValues#getValueFor()
.
For example, if we want to process the geographic coordinate from the example in section Parameter Names and Parameter Identifiers, we can provide the following setter method:
public static class MyActionCommand implements UriActionCommand {
@Override
public void run() {
...
}
@CapturedParameter(mapperName = "showOnMap", parameterName = "location")
public void setLocation(final ParameterValue<Point2D.Double> locationCoordinate) {
// process parameter
}
}
These setter methods need to be public and must have exactly one argument of type ParameterValue
with its class type set to the type of the requested URI fragment parameter. In this example, the parameter’s generic type is Point2D.Double
.
Let us now take a closer look at the class which wraps a concrete URI fragment parameter value: org.roklib.urifragmentrouting.parameter.value.ParameterValue<V>
. You can obtain the parameter value itself from this class with method getValue()
. Before you do that, it is recommended to first check if a concrete value is available at all. You can do this with method hasValue()
. It may happen that the URI fragment interpretation process is not able to extract a valid parameter value from the currently interpreted URI fragment. This may be the case, for example, when a non-optional parameter value could not be found in the URI fragment or if a parameter value could not be successfully converted into the parameter’s data type (i. e., a ParameterValueConversionException
is thrown). If this is the case, the ParameterValue
object is not able to provide a valid parameter value. Instead, it contains an error indicator represented by the enum org.roklib.urifragmentrouting.parameter.UriParameterError
. This enum has three values:
NO_ERROR
: is assumed when the ParameterValue
object contains a valid parameter value.
PARAMETER_NOT_FOUND
: indicates that a required parameter could not be found in the currently interpreted URI fragment.
CONVERSION_ERROR
: indicates that the parameter value extracted from the URI fragment could not be successfully converted into the target data type, i. e., a ParameterValueConversionException
has been thrown from the corresponding converter.
We can use method ParameterValue#hasError()
to inquire whether the parameter value object is erroneous. If this method returns true
we can query whether the error is either PARAMETER_NOT_FOUND
or CONVERSION_ERROR
with ParameterValue#getError()
. A typical blueprint for a method processing a URI fragment parameter value in an action command looks like the following example:
@CapturedParameter(mapperName = "showOnMap", parameterName = "location")
public void setLocation(final ParameterValue<Point2D.Double> locationCoordinate) {
if (locationCoordinate.hasError()) {
UriParameterError error = locationCoordinate.getError();
// process error condition and act accordingly
} else {
Point2D.Double value = locationCoordinate.getValue();
// process parameter value
}
}
As described in section Default Parameter Values, we can make a URI fragment parameter optional by setting a default value. In this case, the corresponding ParameterValue
object will never assume the error code PARAMETER_NOT_FOUND
. If no concrete value is found for such a parameter in the currently interpreted URI fragment, the ParameterValue
object will return this default value. Using method ParameterValue#isDefaultValue()
you can check whether or not the value returned by the ParameterValue
object is the default value for this parameter.
When a URI fragment is processed by the URI action mapper tree, eventually there will be one URI action mapper in the tree which will be able to provide a UriActionCommandFactory
for this URI fragment (or if this is not the case, an optional default command factory is used). This specific action mapper object can be injected into a URI action command with a method annotated with @CurrentActionMapper
. Such a method needs to have exactly one parameter of type UriPathSegmentActionMapper
.
Receiving the current action mapper is useful if you need to assemble a new URI fragment for this mapper, or in other words if you need to change the current URI fragment in some way (e. g. change one of the URI fragment parameter values). For this, you need the corresponding UriPathSegmentActionMapper
object in order to invoke method UriActionMapperTree.assembleUriFragment()
with it.
The current action mapper object passed into methods annotated with @CurrentActionMapper
is immutable. That is, its configuration cannot be changed by client code. If any of the methods that configure an action mapper is called (e. g. setActionCommandFactory()
or registerURIParameter()
), an UnsupportedOperationException
will be thrown.
For example, receiving the current action mapper in an action command could be achieved like so:
public static class MyActionCommand implements UriActionCommand {
@Override
public void run() {
...
}
@CurrentActionMapper
public final void setCurrentActionMapper(UriPathSegmentActionMapper currentActionMapper) {
// do something with the current mapper
}
}
Note that such a method will never be called on default action command objects since for these no current action mapper object is available.
When a URI fragment is interpreted by the URI action mapper tree and could be successfully resolved to an action command, this action command will be executed right away by the mapper tree. This means, that you do not explicitly have to execute the action command’s run()
method (remember that the action command object created by the action mapper tree is returned from method #interpretFragment()
, so you do have access to this object).
If you do not want this to happen, you can instruct the action mapper tree to not automatically execute the action command object. To do this, there is another overloaded variant of interpretFragment()
available:
public <C> UriActionCommand interpretFragment(final String uriFragment,
final C context,
final boolean executeCommand)
When passing false
as the last argument executeCommand
, the URI action mapper tree will not execute the action command automatically. Instead, you are responsible for calling the action command’s run()
method on the command object returned by this method.
We have seen that we cannot set concrete instances of action command objects on the action mappers of a mapper tree. Instead, we have to define action command factories. These factories allow us to configure the action commands created by them in a very flexible way. Among other things, they make it possible to reuse the same action command class for more than one URI fragment. For example, you might want to write an action command class which can display a particular screen of an application. The concrete screen to be shown is defined via the action command’s constructor.
A simple implementation for this could look like the following example:
public class ShowScreenActionCommand implements UriActionCommand {
private String screen;
private ScreenManager screenManager;
public ShowScreenActionCommand(String screen, ScreenManager screenManager) {
this.screen = screen;
this.screenManager = screenManager;
}
@Override
public void run() {
screenManager.displayScreen(screen);
}
}
Using this configurable action command, we can rewrite the example from section Adding Some More Top-Level Mappers as follows:
mapperTree = UriActionMapperTree.create().buildMapperTree()
.map("user").onActionFactory(() -> new ShowScreenActionCommand("userScreen", screenManager)).finishMapper()
.map("admin").onActionFactory(() -> new ShowScreenActionCommand("adminScreen", screenManager)).finishMapper()
.map("settings").onActionFactory(() -> new ShowScreenActionCommand("settingsScreen", screenManager)).finishMapper()
.build();
This approach has the advantage that you can abstract common functionality away in only one class so that you don’t need to write an own class for each individual URI fragment.
We have now seen how this library helps us interpret complex and parameterized URI fragments. This is only half of the story we need to know. We did not yet answer the question from where the interpreted URI fragments originate. Before we can interpret a URI fragment, we need to create links containing these URI fragments in our application.
We could do this using the simple approach of manually assembling a link target like so:
Link productLink = new Link();
productLink.setTarget("#!products/details/id/" + selectedProduct.getId() + "/show");
This approach is very problematic and should be avoided at all costs, since it is very fragile and error-prone. This approach makes it very difficult to refactor the structure of an application’s URI fragments. Just imagine what happens when you want to rename the details
part with overview
or you need to change the data type of product identifiers from Long
into a data type which cannot easily be converted into a String with toString()
(e. g., a custom identifier type) so that it can be appended to a URI fragment like in the example above. You would have to search your whole application’s code to find all spots where a link is assembled manually like this. If you miss just one of these places, you would end up with a malfunctioning link in your application.
We want to do better. It is our goal to keep the definition of an application’s URI fragment structure in one single place so that we can change it there globally. This place is the definition of the application-scoped instance of UriActionMapperTree
. We want to be able to change an action mapper’s name or a URI fragment parameter’s name in the construction code of the action mapper tree. We want then to have all application links to be adapted accordingly.
The URI action mapper tree allows us to do so easily. We can entrust the task of assembling valid parameterized URI fragments to the action mapper tree. We need two things for this:
A reference to the URI action mapper for which we want to create a link.
The set of concrete parameter values to be encoded into the URI fragment. This can be omitted, of course, if we want to assemble a URI fragment which does not contain any parameter values.
We can obtain the action mapper references during the construction of the URI action mapper tree. This will be described shortly.
The easiest way to assemble a URI fragment is when you don’t need any parameter values to be included in the resulting URI fragment. You can then just pass the target action mapper reference to the single-argument method UriActionMapperTree#assembleUriFragment()
:
UriPathSegmentActionMapper targetMapper = ...
String uriFragment = uriActionMapperTree.assembleUriFragment(targetMapper);
Link aLink = new Link(uriFragment);
How can we obtain these action mapper references which we need for assembling a URI fragment? This is done by using the overloaded variants of method finishMapper()
and mapSubtree()
which we have seen in the first sections of this user manual. Both methods are overloaded with a variant which accepts a java.util.function.Consumer
as an additional argument. This consumer is invoked with the currently constructed (dispatching or simple) action mapper as consumer input. By that, you can store the action mapper instance, for instance, in a hash map for later reference.
The action mapper objects passed into the given consumers are immutable. That is, their configuration cannot be changed by client code. If any of the methods that configure an action mapper is called (e. g. setActionCommandFactory()
or registerURIParameter()
), an UnsupportedOperationException
will be thrown.
We enhance our examples from above with this option. First, we want to obtain a reference on the HelloWorld mapper from the first example:
HashMap<String, UriPathSegmentActionMapper> mappers = new HashMap<>();
UriActionMapperTree mapperTree =
UriActionMapperTree.create().buildMapperTree()
.map("helloWorld").onActionFactory(HelloWorldActionCommand::new)
.finishMapper(
simpleUriPathSegmentActionMapper ->
mappers.put(simpleUriPathSegmentActionMapper.getMapperName(), simpleUriPathSegmentActionMapper)
)
.build();
Here, we create a hash map with String keys and values of type UriPathSegmentActionMapper
. We can then add the simple action mapper instance to this map in a lambda expression provided as the Consumer<SimpleUriPathSegmentActionMapper>
argument of method finishMapper()
. The action mapper name helloWorld
is used as the key for this map entry. This can be done since action mapper names must be unique in a URI action mapper tree.
We can now assemble a URI fragment which resolves to the HelloWorld action mapper like so:
String uriFragment = mapperTree.assembleUriFragment(mappers.get("helloWorld");
Of course, when we pass a reference for an action mapper, which is contained in a deeper level of the URI action mapper tree, into assembleUriFragment()
, this method will return a URI fragment consisting of the full path of this action mapper beginning with the root of the action mapper tree.
For example, take the following mapper tree:
HashMap<String, UriPathSegmentActionMapper> mappers = new HashMap<>();
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("user").onActionFactory(GoToUserAreaActionCommand::new)
.onSubtree()
.map("profile").onActionFactory(GoToUserProfileActionCommand::new).finishMapper()
.finishMapper()
.mapSubtree("admin").onActionFactory(GoToAdminAreaActionCommand::new)
.onSubtree()
.map("adminProfile").onPathSegment("profile").onActionFactory(MyActionCommand::new).finishMapper(
adminProfileActionMapper ->
mappers.put(adminProfileActionMapper.getMapperName(), adminProfileActionMapper)
)
... // remainder omitted for brevity
We can create the URI fragment for the adminProfile
mapper as follows:
String uriFragment = mapperTree.assembleUriFragment(mappers.get("adminProfile");
This will yield the following URI fragment
admin/profile
(Note the important difference between an action mapper’s name (its identifier) and the path segment for which it is responsible. We need to reference an action mapper by its name.)
What we have seen until now is how to obtain a reference to a simple action mapper (a leaf node) constructed with the action mapper tree builder. How can we get a reference to a dispatching action mapper?
This can be done with the overloaded version of method mapSubtree()
which works similar to the variant we have seen just now.
HashMap<String, UriPathSegmentActionMapper> mappers = new HashMap<>();
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("user",
userActionMapper ->
mappers.put(userActionMapper.getMapperName(), userActionMapper)
).onActionFactory(GoToUserAreaActionCommand::new)
.onSubtree()
.map("profile").onActionFactory(GoToUserProfileActionCommand::new).finishMapper()
...
With this, we can assemble a URI fragment for the user
dispatching action mapper. This mapper will resolve to the factory which creates GoToUserAreaActionCommand
objects.
When we want to assemble a URI fragment which contains parameter values, we need to take an extra step. We have to gather all parameter values to be added to the URI fragment in an instance of class CapturedParameterValues
. We can then pass this instance as a first argument to the overloaded variant of method assembleUriFragment()
.
Let’s revise the first example from this manual which introduced a URI fragment parameter to an action mapper tree.
HashMap<String, UriPathSegmentActionMapper> mappers = new HashMap<>();
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree("products").withSingleValuedParameter("id").forType(Integer.class).noDefault()
.onSubtree()
.map("details").onActionFactory(ShowProductDetailsActionCommand::new).finishMapper(
detailsActionMapper ->
mappers.put(detailsActionMapper.getMapperName(), detailsActionMapper)
)
.finishMapper()
.build();
Here, we have a simple action mapper for path segment details
for which we would like to generate a parameterized URI fragment. This fragment needs to contain a valid value for a product id and is supposed to resolve to the factory which creates ShowProductDetailsActionCommand
objects.
To do this, we have to provide a value for the id parameter. Let’s say, we want to create the following URI fragment:
products/id/4711/details
For this, we have to create an object of type CapturedParameterValues
and add value 4711
for parameter id
to it.
CapturedParameterValues values = new CapturedParameterValues();
values.setValueFor("products", "id", ParameterValue.forValue(4711));
String uriFragment = mapperTree.assembleUriFragment(values, mappers.get("details"));
// yields products/id/4711/details
Adding a value to a CapturedParameterValues
object is done with method setValueFor()
. We have to pass this method the name of the action mapper, for which the targeted URI fragment parameter is registered, as the first argument. The second argument is the identifier of the target parameter. Lastly, we pass the desired parameter value as an object of class ParameterValue
.
ParameterValue
does not have any publicly visible constructors. It provides a number of factory methods which create correctly configured ParameterValue
objects. Using forValue()
, you can create parameter values which contain a valid domain object and do not indicate a UriParameterError
.
In the example, the parameter with identifier id
is added to the action mapper with name products
. Therefore, we have to specify these two identifiers to correctly define for which URI fragment parameter the given parameter value is determined.
With this information, the URI action mapper tree is able to assemble a fully parameterized URI fragment.
You may wonder what will happen with special characters contained in parameter values added to a URI fragment. Characters with a special meaning, such as /
or %
, will be transparently URL encoded and decoded by the library. By that, it is totally safe to have a String-typed URI fragment parameter value such as e. g. some/value
or 100%
. You don’t have to take care of correctly encoding/decoding such values.
HashMap<String, UriPathSegmentActionMapper> mappers = new HashMap<>();
mapperTree = UriActionMapperTree.create().buildMapperTree()
.map("helloWorld").onActionFactory(MyActionCommand::new).withSingleValuedParameter("msg").forType(String.class).noDefault()
.finishMapper(actionMapper -> mappers.put(actionMapper.getMapperName(), actionMapper))
.build();
CapturedParameterValues values = new CapturedParameterValues();
values.setValueFor("helloWorld", "msg", ParameterValue.forValue("some/value/100%"));
System.out.println(mapperTree.assembleUriFragment(values, mappers.get("helloWorld")));
This example will print the following URI fragment to the console:
helloWorld/msg/some%252Fvalue%252F100%2525
When this fragment is interpreted by the mapper tree, the original parameter value some/value/100%
will be added to the CapturedParameterValues
object.
In this section, we will learn how a URI action mapper tree can be further configured. As we have seen in the beginning of this document, a URI action mapper tree can be constructed with a set of builders. Constructing a new action mapper tree is initiated with the static function create()
:
UriActionMapperTree mapperTree = UriActionMapperTree.create(). ...
Before we start configuring all action mappers for this tree with buildMapperTree()
, we have the option to further configure the mapper tree itself. We can
set a default action command factory to be used by the tree,
set the parameter mode to be used,
set a custom URI token extraction strategy, and
set a custom query parameter extraction strategy,
set an action command factory for the root action mapper,
register a URI fragment parameter on the root action mapper.
We have already learned how a default action command is used and which parameter modes are available. The latter two options, URI token extraction strategy and query parameter extraction strategy, are new to us. We will learn about them in section Writing a Custom Token Extraction Strategy and Query Parameter Extraction Strategy.
With setRootActionCommandFactory()
we can define an action command factory for the root action mapper. The root action mapper is active by default and is responsible for handling the empty URI fragment or the fragment /
which is equivalent to the empty fragment.
With registerRootActionMapperParameter()
we can register a URI fragment parameter directly on the root action mapper. If you, for instance, register a String-typed parameter with name lang
on the root mapper then this parameter will be available for every URI fragment of your application as in the following examples: /lang/de/home
, /lang/en/profile/user/john.doe
, or /lang/sv/admin
.
The root action mapper’s name is defined by the constant UriActionMapperTree.ROOT_MAPPER
. You will need this constant when you refer to this mapper for example when working with a CapturedParameterValues
object or when defining methods annotated with @CapturedParameter
.
If we want to set all these values, we can do this using the builder’s fluent API:
SingleStringUriParameter languageParameter = new SingleStringUriParameter("lang");
UriActionMapperTree mapperTree = UriActionMapperTree.create()
.setRootActionCommandFactory(GoToHomePageActionCommand::new)
.registerRootActionMapperParameter(languageParameter)
.useDefaultActionCommandFactory(Show404FileNotFoundPageActionCommand::new)
.useParameterMode(ParameterMode.QUERY)
.useUriTokenExtractionStrategy(new CustomUriTokenExtractionStrategy())
.useQueryParameterExtractionStrategy(new CustomQueryParameterExtractionStrategy())
.buildMapperTree()
...
Of course, these values can also be set with corresponding setter methods directly on the action mapper tree object.
The library provides three special purpose action mappers which we have not seen yet. These will be described in this section. The following advanced action mapper types are available in addition to the standard mappers:
RegexUriPathSegmentActionMapper
StartsWithUriPathSegmentActionMapper
CatchAllUriPathSegmentActionMapper
The RegexUriPathSegmentActionMapper
is derived from DispatchingUriPathSegmentActionMapper
and thus acts as a dispatching action mapper. This mapper’s distinctive feature is that it is not responsible for one fixed path segment but for a whole set of path segments. The set of path segments handled by this mapper is defined by a regular expression.
The StartsWithUriPathSegmentActionMapper
is derived from RegexUriPathSegmentActionMapper
and is preconfigured with a regular expression which matches all path segments that start with a given prefix.
The CatchAllUriPathSegmentActionMapper
is also derived from RegexUriPathSegmentActionMapper
and is preconfigured with a regular expression which matches all path segments.
There are no builders available which could be used to construct these special action mappers. You will have to create them using their respective constructors. The action mapper tree builders provide, however, methods to add instances of these special purpose mappers to the action mapper tree. You can add a StartsWithUriPathSegmentActionMapper
to an action mapper like in the following example:
StartsWithUriPathSegmentActionMapper mapper = new StartsWithUriPathSegmentActionMapper("blogPostId", "id_", "blogId");
mapperTree = UriActionMapperTree.create().buildMapperTree()
.mapSubtree(startsWithMapper)
.onActionFactory(MyActionCommand::new)
.onSubtree()
.finishMapper()
.build();
As you can see, there is an overloaded version of mapSubtree()
available which allows you to directly add a pre-built dispatching action mapper to the mapper tree.
You can alternatively use method addMapper()
which allows you to add any pre-built UriPathSegmentActionMapper
class to the mapper tree.
These special purpose action mapper types are described in detail in the following sections.
To create a new instance of a RegexUriPathSegmentActionMapper
you have to provide three arguments: the mapper name for the regex action mapper, a parameter identifier to be used by the mapper, and a custom implementation of class org.roklib.urifragmentrouting.parameter.converter.AbstractRegexToStringListParameterValueConverter
.
The regular expression to be used by the regex mapper is provided as a constructor argument of class AbstractRegexToStringListParameterValueConverter
. We will see shortly what the task of this converter is.
We have to define a URI fragment parameter identifier as the second argument of RegexUriPathSegmentActionMapper’s
constructor. This identifier is used internally by the regex mapper for a URI fragment parameter of type StringListUriParameter
. This parameter will hold the extracted values for all regex capturing groups defined in the regular expression.
For example, if you use the following regular expression
(\d+)_(\w+)
and interpret the following path segment with it
4711_myFirstBlogPost
then the string list URI fragment parameter will contain the following list of Strings:
["4711", "myFirstBlogPost"]
This parameter is managed internally by the regex action mapper but can be used by the developer in the exact same way as described in the previous sections. In fact, if you want to assemble a URI fragment for an action mapper with a regex action mapper as one of its parent mappers, you have to provide a parameter value for this internal URI fragment parameter in a CapturedParameterValues
object.
In order to be able to convert a URI path segment into a list of Strings using a regular expression, we have to provide a class which derives from AbstractRegexToStringListParameterValueConverter
. This class expects the regular expression to be used as its only constructor argument. In our sub-class we have to implement method convertToString(List<String> value)
inherited from ParameterValueConverter
. In this method, we need to convert the given list of Strings in such a way that the resulting String can be converted into the exact same String list by using the given regular expression.
Let’s look at a simple implementation for this converter class which will use the regular expression from the example above.
AbstractRegexToStringListParameterValueConverter regexConverter =
new AbstractRegexToStringListParameterValueConverter("(\\d+)_\\w+") {
@Override
public String convertToString(final List<String> value) {
return value.get(0) + "_" + value.get(1);
}
};
We implement the converter as an anonymous class, pass the desired regular expression to the super constructor and implement method convertToString()
. An instance of this converter class can now be passed as the last constructor argument of RegexUriPathSegmentActionMapper
. You can refer to the library’s unit test suite for an example of how to use the regex action mapper.
Class StartsWithUriPathSegmentActionMapper
is derived from RegexUriPathSegmentActionMapper
. An object of this mapper is also constructed with three constructor arguments. In this case, however, you don’t need to supply a custom converter class. Instead, you define a prefix as the second argument which a path segment needs to have in order for it to be handled by this action mapper. The third constructor argument is again the parameter identifier for the internal string list parameter.
The regular expression used by this specific regex action mapper is predefined with
<prefix>(.*)
That is, the prefix you define as the second argument is matched as is, while the remainder of the matched String will be added to the String list URI parameter value. Note that if the prefix contains any characters which have a special meaning in a regular expression, they will be escaped so that the underlying regex does not get mangled by these characters.
This dispatching action mapper is also derived from RegexUriPathSegmentActionMapper
. Here, the regular expression used by this mapper is predefined with (.*)
. That is, this mapper matches the complete path segment and returns this as a whole as the first list entry of its internal parameter value.
To construct a new CatchAllUriPathSegmentActionMapper
, you only have to provide the mapper name and a parameter identifier for the internal String list URI parameter.
When this action mapper is used in the action mapper tree along with one or more sibling action mappers, this catch-all action mapper is always invoked last to check if it is responsible for handling the current path segment. Otherwise, this action mapper would override all other sibling action mappers.
As we have learned until now, we can fully construct a URI action mapper tree using the fluent API provided by the builder classes of class UriActionMapperTree
. But we are not required to do so. Another option is to assemble an action mapper tree by hand. This section explains how this can be done.
When you want to construct an action mapper tree by hand, all you have to do is create instances of dispatching action mappers (for the inner sections of the tree) and simple action mappers (for the leaf nodes) and stick them together.
Adding a sub-mapper to a dispatching action mapper is done with method DispatchingUriPathSegmentActionMapper#addSubMapper()
. The following example will add two simple action mappers as the sub-mappers of a dispatching action mapper.
SimpleUriPathSegmentActionMapper users = new SimpleUriPathSegmentActionMapper("users", "users", GoToUserAdministrationActionCommand::new);
SimpleUriPathSegmentActionMapper groups = new SimpleUriPathSegmentActionMapper("groups", "groups", GoToGroupAdministrationActionCommand::new);
DispatchingUriPathSegmentActionMapper admin = new DispatchingUriPathSegmentActionMapper("admin");
admin.addSubMapper(users);
admin.addSubMapper(groups);
With that we build the action mappers which are able to handle the following URI fragments:
#!admin/users #!admin/groups
Before we can actually interpret these fragments, we first need to add the dispatching mapper admin
to an action mapper tree. We can do this by obtaining a reference to the root action mapper of the mapper tree and then add the dispatching action mapper to it:
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree().build();
mapperTree.getRootActionMapper().addSubMapper(admin);
Since UriActionMapperTree’s
constructor is private, we have to create a new instance of it using the builder class.
URI fragment parameters can be added to an action mapper with method UriPathSegmentActionMapper#registerURIParameter()
. For example, if we want to build a mapper tree which can handle the URI fragment from section Adding a Parameter to a URI Action Mapper
#!products/id/4711/details
we can do this with the following code:
SimpleUriPathSegmentActionMapper details = new SimpleUriPathSegmentActionMapper("details", "details", ShowProductDetailsActionCommand::new);
DispatchingUriPathSegmentActionMapper products = new DispatchingUriPathSegmentActionMapper("products");
products.addSubMapper(details);
SingleIntegerUriParameter idParameter = new SingleIntegerUriParameter("id");
products.registerURIParameter(idParameter);
UriActionMapperTree mapperTree = UriActionMapperTree.create().buildMapperTree().build();
mapperTree.getRootActionMapper().addSubMapper(products);
As described in section Configuring the URI Action Mapper Tree, the root action mapper is always available and is responsible for handling the empty URI fragment or the fragment /
. You can register a URI fragment parameter on this specific action mapper as in the following code:
UriActionMapperTree mapperTree = ...
mapperTree.getRootActionMapper().registerURIParameter(parameter);
The root action mapper’s name is defined by the constant UriActionMapperTree.ROOT_MAPPER
. You will need this constant when you refer to this mapper for example when working with a CapturedParameterValues
object or when defining methods annotated with @CapturedParameter
.
When you have finished building the URI action mapper tree of your application, it would be nice if you could send an overview of the mapper tree to your log. This can be achieved using method UriActionMapperTree#getMapperOverview()
. This method returns a lexicographically sorted list of Strings each of which represents an action mapper of the tree. An action mapper’s path is contained in this list if it is either a leaf node (a simple action mapper) or if it is a dispatching mapper with an action command factory assigned. These Strings contain a summary of all action mappers contained on the path from the tree’s root to the target node, including all registered URI fragment parameters for these action mappers. If an action command factory is assigned to an action mapper, the name of the action class this factory creates is added to the Strings.
Let’s take a look at a few examples.
First, we will log the action mapper tree from the example in section Adding Hierarchy:
/admin -> com.example.GoToAdminAreaActionCommand /admin/groups -> com.example.GoToGroupAdministrationActionCommand /admin/users -> com.example.GoToUserAdministrationActionCommand /settings -> com.example.GoToSettingsActionCommand /user -> com.example.GoToUserAreaActionCommand /user/profile -> com.example.GoToUserProfileActionCommand
As you can see, for each URI fragment, the action class which is executed when this fragment is interpreted will be added to the output.
If URI parameters are defined for an action mapper tree, the overview list looks as follows:
/shopLocation[{Point2DUriParameter: id='coordinates', xParam='lat', yParam='lon'}] -> com.example.MyActionCommand
If a default value is defined for a URI fragment parameter:
/products[{SingleIntegerUriParameter: id='id' default='0'}]/details -> com.example.ShowProductDetailsActionCommand
If a path segment name is assigned twice, the action mapper’s name is printed in brackets:
/admin -> com.example.GoToAdminAreaActionCommand /admin/profile[adminProfile] -> com.example.GoToAdminProfileAreaActionCommand /user -> com.example.GoToUserAreaActionCommand /user/profile -> com.example.GoToUserProfileActionCommand
This feature comes in handy when you want to visually check if the action mapper tree you are constructing meets the requirements of your application.
This library uses the Simple Logging Facade for Java (SLF4J) as its logging framework. By that, the log statements emitted by the classes of this library can be routed through any logging framework used by an application which makes use of this library as long as there is a SLF4J binding JAR available for the logging framework used. Please refer to the documentation of SLF4J for details about how to properly configure a binding for SLF4J.
The URI action routing library basically uses two log levels for logging its data: INFO
and DEBUG
.
The DEBUG
log level is used for logging information about the internal workings of this library. Enabling this mode in your logging configuration is useful when you want to obtain a deeper understanding of what exactly the classes do. As this level indicates, this is useful for debugging purposes.
The INFO
log level is used for logging runtime information which might be useful for evaluation by external processes, such as log analyzers. In particular, each URI fragment interpretation process will be logged with this level. By that, you get the equivalent of a customary access log. The following log extract is an example for this:
INFO - [adeab535-dbb2-4910-a26f-202891146bb4] interpretFragment() - INTERPRET - [ show ] - CONTEXT={RoutingContext object} INFO - [adeab535-dbb2-4910-a26f-202891146bb4] getActionForUriFragment() - NOT_FOUND - No registered URI action mapper found for fragment: show INFO - [adeab535-dbb2-4910-a26f-202891146bb4] getActionForUriFragment() - NOT_FOUND - Executing default action command class: class com.example.DefaultActionCommand INFO - [e26a611f-c72a-47ad-b818-8a2a4477ccd5] interpretFragment() - INTERPRET - [ productLocation/lon/17.0/lat/42.0/details/mode/summary ] - CONTEXT={RoutingContext object}
As you can see, it may happen that more than one logging statement is written to the log for a single URI fragment interpretation process. In order to be able to correlate these related log statements, each URI fragment interpretation process is assigned a UUID which is prepended to each logging statement emitted from this process. This facilitates the analysis of these logging outputs.
In addition to the URI fragment, which is currently being interpreted, the routing context object is written to the log, too. By that, you have the option to include additional information to the log by implementing the routing context class’s toString()
method accordingly. You could, for example, log the current session ID or information about the remote host.
If you additionally want to see in the log which concrete action command objects are executed, you have to enable the DEBUG
log level.
When the URI action mapper tree interprets a URI fragment, any given fragment is first broken down into a list of tokens. For example, the URI fragment
#!products/id/4711/details
will be transformed into the list
["products", "id", "4711", "details"]
This list is then interpreted sequentially from start to end. As you can see, the /
character is used as the separator of the tokens. The URI fragment is split along this character.
This behavior can be changed. The concrete algorithm that breaks a URI fragment into a token list is implemented in a strategy class. You can provide your own implementation of this strategy by implementing interface org.roklib.urifragmentrouting.strategy.UriTokenExtractionStrategy
. To do so, you need to implement two methods.
List<String> extractUriTokens(String uriFragment)
receives a URI fragment and transforms it into a list of tokens.
String assembleUriFragmentFromTokens(List<String> tokens)
does the opposite and assembles a String from a list of tokens which can be transformed back into the initial list with the first method.
The default implementation of this interface is org.roklib.urifragmentrouting.strategy.DirectoryStyleUriTokenExtractionStrategyImpl
.
There is another strategy interface you can implement. When you use parameter mode ParameterMode#QUERY
for your action mapper tree, the URI fragment parameters are appended to the URI fragment like URI query parameters:
#!admin/users/showHistory?id=4711&startDate=2017-01-01&endDate=2017-01-31
You can implement interface org.roklib.urifragmentrouting.strategy.QueryParameterExtractionStrategy
to define your own set of rules which determine how this type of parameters should be extracted from the URI fragment and transformed into a Map<String, String>
. To do so, you need to implement three methods.
Map<String, String> extractQueryParameters(String uriFragment)
extracts all URI parameters contained in the given URI fragment in query mode and passes them back as a parameter map.
String stripQueryParametersFromUriFragment(String uriFragment)
removes the section from the given URI fragment which contains the query parameters.
String assembleQueryParameterSectionForUriFragment(Map<String, String> forParameters)
receives a map of URI parameter values and returns the query String for these parameters to be appended to the URI fragment.
The default implementation of this interface is org.roklib.urifragmentrouting.strategy.StandardQueryNotationQueryParameterExtractionStrategyImpl
.
Refer to section Configuring the URI Action Mapper Tree to see how you can set your custom strategy implementations on an action mapper tree.