Mapper introduction
Mappers allow you to "map" incoming objects to output objects. For a general overview of what mappers are and how they work see Mappers.
Creating a custom mapper
Creating a custom mapper requires two things: an SBB part with the actual
code, and an entry in the SentinelMapperSetEntryTable
. The table already
contains entries for all the standard mappers, so if you want to replace an
existing mapper then the table should not need to be updated.
The mapper SBB part
Mappers typically live in their own modules, so the first step of creating a
mapper is to create a new module. The easiest way to accomplish this is to use
the create-module
functionality of the sdkadm tool. See
Creating a new module for more
information about this. Some modules already contain a mapper as a nested
module, making the initial creation of the mapper module very easy.
The sentinel-sip-example
module is an example of such a case. The example
mapper that it contains will be used to illustrate the explanations in this
document.
The mapper class
The implementing class of a mapper has two requirements:
-
It must contain a
SentinelMapper
annotation that defines the name, the input class and the output class of the mapper. -
It must implement the
Mapper
interface, which consists of the single methodmap()
.
To illustrate this here is a simple example of a mapper class, taken from the
aforementioned sentinel-sip-example
module:
package com.opencloud.sentinel.example.feature;
@SentinelMapper(
mappings = {
@Mapping(name = "StringToString", fromClass = String.class, toClass = String.class)
}
)
public class StringToStringMapper implements Mapper<NullSentinelSessionState> {
@Override
public Object map(final Object arg, final NullSentinelSessionState sessionState, final MapperFacilities facilities) throws MapperException {
return arg;
}
}
This mapper takes a String
as an input and outputs a String
as well. Note
that you can have multiple @Mapping
annotations to have a mapper handle more
than one combination of classes.
The map()
implementation here simply returns the input string unmodified. It
also only uses the NullSentinelSessionState
class instead of a concrete one
that would allow it to read and manipulate the available session state fields
available through the concrete implementation. This is something that a real
mapper implementation is likely to want to do.
Finally, the MapperFacilities
object gives the mapper access to the log()
method so it can log diagnostic messages.
Note that instead of directly implementing the Mapper
interface a mapper
could also subclass an existing mapper.
The package info class
In addition to the actual mapper implementation a mapper module also needs a
package-info.java
file in the same package as the mapper class. This class
contains the @SBBPart
annotation necessary to create a valid SBB part out of
the mapper. This class is subsequently very simple:
@SBBPart(
id = @ComponentId(name = "@component.name@", vendor = "@component.vendor@", version = "@component.version@")
)
package com.opencloud.sentinel.example.feature;
The component variables are substituted with the correct variables at
compilation time by the build system. The definitions of them should reside in
the module.properties
file in the module root directory and look like this:
component.name=${ivy.module} component.vendor=${sdk.component.vendor} component.version=${sdk.component.version}
If the module was created using sdkadm then this file should already contain the correct properties.
The execution point enum
Mappers support different mapper execution points. These allow specialising mappers based on a condition. An example of an execution point could be a specific state of an ongoing call, but there are no restrictions on what they have to represent. When looking up a mapper the mapper registry will first perform a lookup that respects the execution point, and if that fails it will perform a more general lookup that ignores the execution point (for more information about mapper lookup refer to Mappers).
The execution points have to be specified as a Java enum
in the mapper
module.
Publishing the SBB part artifact
In order for Ivy to be able to publish the SBB part correctly it needs the
correct publication configuration in the module’s ivy.xml
file. This
consists of an <artifact>
line in the file’s <publications>
section and
typically would look like this:
<artifact name="${ivy.module}" type="sbbpart" ext="jar" conf="slee-component,api"/>
Making the mapper known to Sentinel
In order to be able to use a mapper it has to be added to the
SentinelMapperSetEntryTable
profile table, which contains the information
that Sentinel needs to be able to look up mappers at runtime. For configuring
the table in a running Rhino see
Mappers.
If the new mapper should be deployed as part of a full Sentinel installation
it can also be manipulated directly by editing the
config/<full-deploy-module>/profiles/SentinelMapperSetEntryTable.yaml
file
in your SDK’s deployment module.
Each profile in this table specifies the configuration for one mapper. The below example illustrates the structure of such a profile:
${platform.operator.name}:::::StringToString: MapperExecutionPoint: StringExecutionPoint MapperSetName: '${platform.operator.name}:' MappingName: StringToString PlanId: '' SessionType: ''
The name of such a profile consists of a Sentinel selection key and the appended mapper name.
The MapperExecutionPoint
, PlanId
, and SessionType
attributes are
optional. The MapperExecutionPoint
attribute specifies the execution point
explained above and must be one of the values of the mapper’s enum
.
Using a mapper in a feature
Making use of a mapper in a feature consists of two parts: giving the feature access to the mapper registry through the mapper library, and actually calling the mapper from the feature. The feature also needs a reference to the mapper component.
Injecting the mapper library
The mapper library is injected into a feature just like a resource adaptor is. Subsequently it requires two changes in a feature:
-
Changing the
@SentinelFeature
annotation to include theuseMapperLibrary
andmappingExecutionPointEnum
fields, and -
Implementing the
InjectMapperLibrary
interface’sinjectMapperLibrary()
method.
The following shows a minimal example of this, with all the non-relevant code elided:
package com.opencloud.sentinel.example.feature;
// import ...
@SentinelFeature(
// ...
useMapperLibrary = true,
mappingExecutionPointEnum = MyExecutionPointEnum.class,
// ...
)
@SBBPartReferences(
sbbPartRefs = {
// ...
// The appropriate variables here should be available in a feature's
// "target/generated/module.properties" file after building if the
// feature has the correct dependency on the mapper module.
@SBBPartReference(id = @ComponentId(name = "@sentinel-sip-example-mapper.name@",
vendor = "@sentinel-sip-example-mapper.vendor@",
version = "@sentinel-sip-example-mapper.version@"))
}
)
// ...
public class ExampleFeature extends BaseFeature<SessionStateType, FeatureEndpoint>
implements ..., InjectMapperLibrary<MyExecutionPointEnum> {
// ...
@Override
public void injectMapperLibrary(MapperLibrary<MyExecutionPointEnum> mapperLibrary,
MapperFacilities mapperFacilities) {
this.mapperLibrary = mapperLibrary;
this.mapperFacilities = mapperFacilities;
}
// ...
private MapperFacilities mapperFacilities;
private MapperLibrary<MyExecutionPointEnum> mapperLibrary;
}
Calling the mapper from the feature
This is where it all comes together. Mapper usage in a feature again comes down to two steps:
-
Looking up the appropriate mapper in the mapper registry, and
-
Calling the mapper.
Looking up a mapper
Looking up a mapper is done with the mapper library that got injected into the feature earlier. This is where the execution point comes into play, as the lookup can optionally include an execution point.
Mapper<SessionStateType> mapper = mapperLibrary.findMapper(getSessionState().getSentinelSelectionKey(),
String.class,
String.class,
MyExecutionPointEnum.FooExecutionPoint);
Calling the mapper
Calling a mapper simply involves calling the map()
function with the input
object we want to map and checking that the result is actually as expected.
Object mappingResult = mapper.map(inputObject, getSessionState(), mapperFacilities);
if (mappingResult == null)
throw new MapperException("Mapper " + mapper + " returned null");
if (!(mappingResult instanceof String))
throw new MapperException("Mapper " + mapper + " returned incorrect class: " + mappingResult.getClass());
String resultString = (String) mappingResult;