Overview
The purpose of the plugin is to allow to set up a specific configuration for a Wedia portal application, by overriding a basic configuration provided by the product.
...
_portal
_portal@config
_portal@config@picker
The path _portal@$config@picker designates a configuration defined in 3 folders whose relative paths are successively :
...
If a post processor already exists, it is ignored (note that you cannot control the order of the plugins).
Un exemple complet de contribution de mappings et postprocessors
La configuration du plugin WXM_CONFIG_RESOLVER est
Where to put the groovy script
In config-resolver-postprocessors.json
, when the definition is a groovy file path, it is a relative path to the root of the plugin.
By example, if your script file myprocessor.groovy is in the folder res/postprocessors, the json will be like this:
Code Block | ||
---|---|---|
| ||
{ "mappingsmyprocessor": { "portalprocessor": "_portal@club-wed@:surferRole@:userAgentres/postprocessors/myprocessor.groovy", "picker": "_portal@club-wed@:picker" }, "postProcessors} } |
You can also put a slash at beginning like this:
Code Block |
---|
{ "myprocessor": { "surferRoleprocessor": "fr.wedia.confres.core.model.processor.SurferRolePostProcessor/res/postprocessors/myprocessor.groovy", "userAgent} } |
It’s also possible to put the script in a plugin jar, by using a JAR URI :
Code Block |
---|
{ "myprocessor": { "processor": "fr.wedia.confres.core.model.processor.UserAgentPostProcessorjar:file:/thejarfile.jar!/fr/wedia/mypackage/postprocessors/myprocessorinjar.groovy", } } |
The file thejarfile.jar must be in plugin lib directory. If you want to put this jar in another folder, prefix you jar path by the relative (to the plugin root) path:
Code Block |
---|
{ "configmyprocessor": [ { "pattern"processor": ".*Mobi.*jar:file:/other/path/thejarfile.jar!/fr/wedia/mypackage/postprocessors/myprocessorinjar.groovy", } } |
Deal with the host plugin
It is possible to exchange with the contribution plugin. However, it is not certain that it is started when the postprocessor is running, whatever the method, and even less so when the postprocessor is loaded, initialized and started. The plugin can even be stopped and restarted at any time.
This means:
always make sure the plugin is started when calling one of its classes, and provide a fallback (for
getValue()
andgetAvailableValues()
).to retrieve parameters, you have to proceed in lazy, try to systematically read the parameters as long as it failed, then store them (cache) to use them.
before invoking a method of a plugin class, check the state of the plugin
to have the plugin instance available, implement the injection method (setPlugin(IPlugin)
orsetPlugin(String)
)any modification of the plugin requires a restart of WXM_CONFIG_RESOLVER after.
Here is an example of possible interactions with plugin (get parameters and invoke method):
Code Block | ||
---|---|---|
| ||
package fr.wedia.confref.postprocessor.examples;
import java.util.stream.*;
import java.util.Collection;
import com.google.gson.*;
import com.noheto.plugins.IPlugin;
import fr.wedia.confres.core.model.processor.*;
import fr.wedia.confres.utils.Utils;
import fr.wedia.confres.utils.annot.PostProcessorConfigExclude;
import fr.wedia.confres.core.model.processor.EngineToolkit.ParameterType; // need to import explicitly the enum (in groovy)
/**
* A basic example of postprocessor implementation in groovy.
*
* <p>This is an example of a postprocessor with interaction with the plugin that declares the postprocessor.
* The value is a configurable fixed value.</p>
* <p>There are two types of interactions </p>
* <ul>
* <li>We read some configuration properties of the plugin (they are not used, it is just for the example)</li>
* <li>We invoke a method of the plugin at each call (it doesn't matter what it does, it's just for the example)</li>
* </ul>
*
* <p></p>
*
* @author joel.drigo@wedia-group.com
*
*/
class CustomProcessorExampleOfInteractionWithThePlugin extends AbstractPostProcessor {
/**
* This value is always returned. It is configurable because there isn't any {@link @PostProcessorConfig} annotation.
*/
private String value = "fixed";
/**
* This is the list of possible returned values. It is initialized at startup, since the returned value is configurable (variable {@code value})
* <p>This variable is excluded explicitly from configuration, because there isn't any {@link @PostProcessorConfig} annotation</p>
*/
@PostProcessorConfigExclude
private Set<String> values;
/**
* Stores the plugin that declares the postprocessor (initialized at loading)
*/
@PostProcessorConfigExclude
private IPlugin plugin;
/**
* True when the plugin parameters has been effectly loaded
*/
@PostProcessorConfigExclude
private boolean configured;
/**
* This is a string parameter from plugin parameters
*/
@PostProcessorConfigExclude
private String paramExample1;
/**
* This is a JsonObject parameter from plugin parameters
*/
@PostProcessorConfigExclude
private JsonObject paramExample2;
/**
* This method is used when loading the postprocessor to inject the plugin that declares this postprocessor.
*
* @param plugin the name of the plugin that declares this postprocessor.
*/
public void setPlugin(IPlugin plugin) {
this.plugin = plugin;
loadConfig(); // lazy loading of plugin parameters
}
/**
* Start the processor.
*/
public void start() {
loadConfig(); // lazy loading of plugin parameters
value = Utils.withDefault(value, ""); // if value is null or blank, replace with empty string
values = Utils.unmodifiableSet(value);
}
/**
* This method reads the configuration from plugin (whose declare this postprocessor).
*/
private void loadConfig() {
if ( !configured ) { // if configuration has not been yet loaded
if ( plugin==null ) {
PostProcessorLogger.warn("Plugin has not be set correctly");
configured = true; // we force this state to avoid trying to load again the configuration
}
else if ( plugin.isStarted() ) {
if ( PostProcessorLogger.debug() ) PostProcessorLogger.debug(CustomProcessorExampleOfInteractionWithThePlugin.class,"Load parameters from plugin " + plugin.getName() );
try {
// we read the desired plugin parameters
Map<String,Object> parameters = new EngineToolkit.PluginParameters()
.registerParameter("param_example_1", ParameterType.STRING, "default") // reads parameter_example_1 of type string
.registerParameter("param_example_2", ParameterType.JSON, ()-> new JsonObject()) // reads parameter_example_2 of type json
.collect(plugin);
configured = true;
paramExample1 = (String)parameters.get("param_example_1");
JsonElement json = (JsonElement)parameters.get("param_example_2");
if ( json instanceof JsonObject ) {
paramExample2 = (JsonObject)json;
}
else {
paramExample2 = new JsonObject();
}
if ( PostProcessorLogger.debug() ) {
PostProcessorLogger.debug(CustomProcessorExampleOfInteractionWithThePlugin.class,"Parameter param_example_1 = " + paramExample1 );
PostProcessorLogger.debug(CustomProcessorExampleOfInteractionWithThePlugin.class,"Parameter param_example_2 = " + paramExample2 );
}
}
catch(Throwable t) {
// show stack trace only in debug level
if ( PostProcessorLogger.debug() ) PostProcessorLogger.debug(CustomProcessorExampleOfInteractionWithThePlugin.class,"Error while loading parameters from plugin " + plugin.getName() +".", t );
else if ( PostProcessorLogger.warn() ) PostProcessorLogger.warn(CustomProcessorExampleOfInteractionWithThePlugin.class,"Error while loading parameters from plugin " + plugin.getName() +". Set log to debug to see stacktrace." );
}
}
}
}
/**
* This method invoke a method of a class of the plugin (whose declare this postprocessor).
*/
private void invokePlugin(ProcessorContext context) {
if ( PostProcessorLogger.trace() ) {
PostProcessorLogger.trace("Invoking " + plugin.getName() + " / fr.wedia.confref.postprocessor.examples.Example.helloPlugin()");
}
String pluginMessage = EngineToolkit.invokePlugin(plugin, t->{
if ( t!=null ) { // if t is null plugin is not started
// show stack trace only in debug level
if ( PostProcessorLogger.debug() ) PostProcessorLogger.debug(CustomProcessorExampleOfInteractionWithThePlugin.class,"Error while invoking plugin " + plugin.getName() +".", t );
else if ( PostProcessorLogger.warn() ) PostProcessorLogger.warn(CustomProcessorExampleOfInteractionWithThePlugin.class,"Error while invoking plugin " + plugin.getName() +". Set log to debug to see stacktrace." );
}
}, "fr.wedia.confref.postprocessor.examples.Example","helloPlugin", context.getProcessorName() );
if ( pluginMessage!=null ) {
if ( PostProcessorLogger.info() ) {
PostProcessorLogger.info("The plugin " + plugin.getName() + " says: \"" + pluginMessage + "\"");
}
}
else {
if ( PostProcessorLogger.debug() ) {
PostProcessorLogger.debug("Invoking " + plugin.getName() + "... seems to be not started.");
}
}
}
/**
* {@inheritDoc}
* Returns the postprocessor value corresponding (always the value {@code value}.
*/
@Override
String getValue(ProcessorContext context) {
loadConfig(); // lazy loading of plugin parameters
if ( PostProcessorLogger.trace() ) {
PostProcessorLogger.trace("Invoking " + plugin.getName() + "...");
}
invokePlugin(context); // invoke plugin
return value;
}
/**
* Returns all possible values.
*
* @return all possible values.
*/
@Override
public Collection<String> getAvailableValues() {
loadConfig(); // lazy loading of plugin parameters
return values;
}
/**
* Returns a hash code value for this postprocessor.
*/
public int hashCode() {
return Objects.hash(value);
}
/**
* Returns true if the specified object is equals to this processor, i.e. the classes of both are equals and indexes are equals.
*
* return true if the specified object is equalis to this processor.
*/
@Override
public boolean equals(Object obj) {
return fr.wedia.confres.utils.Utils.equals(CustomProcessorExampleOfInteractionWithThePlugin.class, this, obj, (o1,o2)-> Objects.equals(o1.value,o2.value));
}
} |
A complete example of the contribution of mappings and postprocessors
The configuration of the WXM_CONFIG_RESOLVER plugin is
Code Block | ||
---|---|---|
| ||
{ "mappings": { "portal": "_portal@club-wed@:surferRole@:userAgent", "picker": "_portal@club-wed@:picker" }, "postProcessors": { "surferRole": "fr.wedia.confres.core.model.processor.SurferRolePostProcessor", "userAgent": { "processor": "fr.wedia.confres.core.model.processor.UserAgentPostProcessor", "config": [ { "pattern": ".*Mobi.*", "name": "mobile" } ] } } } |
Le The service GET /api/portalconfig/mappings
(liste des list of mappings) retourne returns the following two mappings:
Code Block | ||
---|---|---|
| ||
[ "portal", "picker" ] |
Le The service GET api/portalconfig/postprocessors/list?options=%7B%0A%09withMappings%3A%20true,%0A%09withValues%3A%20true%0A%7D
(liste des list of postprocessors, avec leurs mappings associés et valeurs) retourne with their associated mappings and values) returns the following two postprocessors :
Code Block | ||
---|---|---|
| ||
[ { "name": "surferrole", "mappings": [ "portal" ], "values": [ "", "role-1", "role-4", "role-27", "role-31", "role-32", "role-33" ] }, { "name": "useragent", "mappings": [ "portal" ], "values": [ "", "mobile" ] } ] |
Je créé un plugin de contribution I create a contribution plugin WXM_CONFIG_RESOLVER_GPP tel que (ici j’ai mis les sources des as (here I put two of the sources of the postprocessors (groovy) dans un dossier in a res , mais ils peuvent être mis dans un autre dossier).folder, but they can be put in another folder). The third processor is in a JAR.
📁 WXM_CONFIG_RESOLVER_GPP
...
📄 config-resolver-postprocessors.json
📁 lig
📄 gpp.jar
📁 res
📁 gpp
📄 CustomProcessor.groovy
📄 CustomProcessorExample.groovy
📄 CustomProcessorExampleOfInteractionWithThePlugin.groovy
📄 CustomProcessorWithConfig.groovy
(le plugin d’exemple contient d’autres processors groovy non utilisés dans cet exemple)the plugin may contain other groovy processors not used in this example).
Here is the zip of the plugin (including source of class fr.wedia.confref.postprocessor.examples.Example
.
View file | ||
---|---|---|
|
Le fichier The config-resolver-mappings.json définit des file defines additional mappings supplémentaires :
Code Block | ||
---|---|---|
| ||
{ "mapping_GPP_1": "_empty", "mapping_GPP_2": "_empty@:gppProcessor", "mapping_GPP_3": "_empty@:gppProcessor@:gppProcessorWithConfig", "mapping_GPP_4": "_empty@:gppProcessor@:gppProcessorPlugin" } |
Le fichier The config-resolver-postprocessors.json définit des file declares additional postprocessors supplémentaires :
Code Block | ||
---|---|---|
| ||
{{ "gppProcessor": { "processor": "jar:file:/gpp.jar!/fr/wedia/confref/postprocessor/examples/CustomProcessor.groovy" }, "gppProcessorgppProcessorWithConfig": { "processor": "/res/gpp/CustomProcessorCustomProcessorWithConfig.groovy", "config": { "values": ["x","y" ] } }, "gppProcessorWithConfiggppProcessorPlugin": { "processor": "/res/gpp/CustomProcessorWithConfigCustomProcessorExampleOfInteractionWithThePlugin.groovy", "config": { "values": ["x","y" ] } } } |
...
}
} |
This defines the following postprocessors:
gppProcessor
source script is located in jar gpp.jar (located in the directory /lib of the plugin). The source is also in folder /res/gpp if you want to look at the source. It is used by mappingsmapping_GPP_2
,mapping_GPP_3
andmapping_GPP_4
.gppProcessorWithConfig
source script is located in folder /res/gpp. It is used by mappingmapping_GPP_3
.gppProcessorPlugin
source script is located in folder /res/gpp. It is used by mappingmapping_GPP_4
.
We use here the "object" syntax without "plugin" property, which determines that the groovy file will be searched in this plugin (WXM_CONFIG_RESOLVER_GPP), avec un path relatif à sa racine.On installe et active ce plugin et on redémarre with a relative path to its root.
We install and activate this plugin and restart WXM_CONFIG_RESOLVER.
Le service GET /api/portalconfig/mappings
(liste des mappings) retourne :
Code Block | ||
---|---|---|
| ||
[ "portal", "picker", "mapping_GPP_1", "mapping_GPP_2", "mapping_GPP_3", "mapping_GPP_4" ] |
Le service GET api/portalconfig/postprocessors/list?options=%7B%0A%09withMappings%3A%20true,%0A%09withValues%3A%20true%0A%7D
(liste des postprocessors, avec leurs mappings associés et valeurs) retourne :
Code Block | ||
---|---|---|
| ||
[ { "name": "surferrole", "mappings": [ "portal" ], "values": [ "", "role-1", "role-4", "role-27", "role-31", "role-32", "role-33" ] }, { "name": "useragent", "mappings": [ "portal" ], "values": [ "", "role-33mobile" ] }, { "name": "useragentgppprocessorplugin", "mappings": [ "portalmapping_GPP_4" ], "values": [ "fixed", "mobile" ] }, { "name": "gppprocessor", "mappings": [ "mapping_GPP_2", "mapping_GPP_3", "mapping_GPP_4" ], "values": [ "V1", "V2", "V3", "V4", "V5" ] }, { "name": "gppprocessorwithconfig", "mappings": [ "mapping_GPP_3" ], "values": [ "x", "y" ] } ] |
...
The
getAvailableValues()
method must return all possible values returned by the other method.The method
getValue(ProcessorContext)
will return the desired value in the invocation context. It is important to consider the context rather than the query, because in some cases, for the purposes of the configuration editing tool, and for testing purposes, the context information may override query values. It is theProcessorContext
class that allows access to the invocation context:ProcessorContext.getSurfer()
: returns the surfer for which we want to execute the postprocessorProcessorContext.getSurferProperty(String name)
: returns the value of a property of the surfer (or null if it does not exist).ProcessorContext.getHeaders(String name)
: returns the values of the specified headerProcessorContext.getParameters()
: returns a wrapper to retrieve query parametersProcessorContext.getMappingId()
: returns the ID of the mapping we are trying to solveProcessorContext.getPathInfo()
: allows to have a contextual description of invocation to log in particularProcessorContext.getProcessorName()
: get the name of the post processorAs a last resort, you can retrieve the request, with the method
ProcessorContext.getRequest().
Note |
---|
Avoid using the query to retrieve context information (like the surfer for example). Always prefer to use the context when possible. Simulation queries can pass context information (like the surfer, or a header) as parameters, and this information is put in ProcessorContext, the query cannot be modified. Moreover, during unit tests your postprocessor could be called without request with a mocked server. In this case, the request instance will be null. If the context does not give you access to the information you want, you can use the request, but in this case your postprocessor cannot be used in simulation for this information nor in testing. |
Info |
---|
There is no guarantee that any of the returned values are not null: if any of the information provided by the ProcessorContext is used, all cases must be tested (including, for example, testing whether getRequest() is null, as the postprocessor could be invoked as part of a simulation outside of HTTP calls). |
...