Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

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
languagejson
{
    "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() and getAvailableValues()).

  • 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) or setPlugin(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
languagegroovy
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
languagejson
{
    "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
languagejson
[
	"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
languagejson
[
	{
		"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
nameWXM_CONFIG_RESOLVER_GPP.zip

Le fichier The config-resolver-mappings.json définit des file defines additional mappings supplémentaires :

Code Block
languagejson
{
        "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
languagejson
{{
	"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 mappings mapping_GPP_2, mapping_GPP_3 and mapping_GPP_4.

  • gppProcessorWithConfig source script is located in folder /res/gpp. It is used by mapping mapping_GPP_3.

  • gppProcessorPlugin source script is located in folder /res/gpp. It is used by mapping mapping_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
languagejson
[
	"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
languagejson
[
	{
		"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"
		]
	}
]

...

  1. The getAvailableValues() method must return all possible values returned by the other method.

  2. 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 the ProcessorContext class that allows access to the invocation context:

    1. ProcessorContext.getSurfer(): returns the surfer for which we want to execute the postprocessor

    2. ProcessorContext.getSurferProperty(String name): returns the value of a property of the surfer (or null if it does not exist).

    3. ProcessorContext.getHeaders(String name): returns the values of the specified header

    4. ProcessorContext.getParameters(): returns a wrapper to retrieve query parameters

    5. ProcessorContext.getMappingId(): returns the ID of the mapping we are trying to solve

    6. ProcessorContext.getPathInfo(): allows to have a contextual description of invocation to log in particular

    7. ProcessorContext.getProcessorName(): get the name of the post processor

    8. As 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).

...