Part 3.1: Implementing plugin functionality

This guide covers the basic steps in adding code to implement functionality for a plugin.

Prerequisites

Before using this guide ensure that you have created a new plugin using a Maven archetype and imported it into IntelliJ.

Write Java code to implement functionality

If you are familiar with writing Groovy code see: Groovy/Java equivalent code examples for a series of examples to help you get started when writing a plugin in Java.

The Maven archetype creates a blank plugin with the bare bones required to get started with writing a new plugin.

  1. Open the TitlePrefixSearchLifeCyclePlugin file inside IntelliJ. Observe that the file is basically empty - the Maven archetype has added some basic methods that you need to implement but these are all empty.

  2. Hold Ctrl and click on SearchLifeCyclePlugin, it will open the underlying file with the documentation we added earlier.

    If you get a decompiled class file when you click this, it means that the sources haven’t been downloaded (and you won’t see the documentation, just the interface and method names. IntelliJ should provide an option you can click on to download the source files.

    The documentation provides some basic information on the different methods you can implement.

    documented_search_life_cycle

    If you are familiar with the old Groovy hook scripts, you will recognize that there is a method for each of the different hook scripts you previously wrote. The plugin encapsulates all the code that you would previously spread across the different hook scripts and puts the code relevant for the plugin into each corresponding method.

    In this tutorial, we will create a plugin which affects the titles of the search results. Looking at the methods above, postProcess is the logical place to implement the plugin functionality.

Define plugin configuration options

The PluginUtils.java file is where you configure the basic details about the plugin, and also configuration keys that can be used to configure how the plugin operates.

Open the PluginUtils.java file in IntelliJ.

Observe that the Archetype has already configured a number of settings within the PluginUtils class. It has defined the package name and also a number of get functions (towards the end of the file) that return information about the plugin (such as the plugin name and description).

The get methods in this file are used to return information that is used with the administration dashboard and also to populate sections of the generated documentation. If you need to tweak the values you entered when creating the plugin you can update the corresponding code within this file.

The generated PluginUtils,java file also includes a couple of example methods for setting a configuration key or configuration file. These should be removed/updated as part of editing your plugin.

The plugin we are writing for the tutorial needs to be configured with two parameters:

  • A field that defines a pattern used to match what is being replaced.

  • A field that defines the replacement text.

To do this we add the following to the PluginUtils:

    public final PluginConfigKey<String> PATTERN = PluginConfigKey.<String>builder()
            .pluginId(getPluginId())
            .id("pattern")
            .type(PluginConfigKeyType.builder().type(PluginConfigKeyType.Format.STRING).build())
            .defaultValue(".")
            .label("prefix pattern")
            .description("Pattern of characters to match prefix against")
            .longDescription("The pattern specified here is a _Java regular expression_ that is matched against the title.\n" +
                             "\n"+
                             "See also: link:https://www.regexplanet.com/advanced/java/index.html[Java regex tester]")
            .build();

    public final PluginConfigKey<String> REPLACEMENT = PluginConfigKey.<String>builder()
            .pluginId(getPluginId())
            .id("replaceWith")
            .type(PluginConfigKeyType.builder().type(PluginConfigKeyType.Format.STRING).build())
            .defaultValue("1")
            .label("prefix pattern")
            .description("Pattern of characters to match prefix against")
            .build();
The configuration key builder requires you to set a number of properties for each key. In the above example we are setting things like the key type, display info like labels and descriptions and validation information. See: Defining plugin configuration keys for detailed information on the various options that are available.

You also need to update the getConfigKeys() and getConfigFiles() every time you make changes to the plugin configuration keys and fiiles.

Update the two methods as follows:

    /**
     *  Returns list of Configuration keys defined for plugin
     *   All configuration keys defined above need to be included in the list to ensure that they can be configured via UI
     */
    @Override public List <PluginConfigKeyDetails> getConfigKeys() {
        return List.of(PATTERN, REPLACEMENT); (1)
    }

    /**
     *  Returns list of Configuration files defined for plugin
     *  All configuration files defined above need to be included in the list to ensure that they can be uploaded via UI
     */
    @Override public List <PluginConfigFile> getConfigFiles() {
        return List.of(); (2)
    }
1 This needs to be set to a list of configuration keys used by the plugin. Here we are updating it to return the two keys that you have just defined.
2 This needs to be set to the list of configuration files used by the plugin. This plugin doesn’t use any configuration files se this is updated to return an empty list.

Finally, don’t forget to remove the two example methods for setting configuration - remove:

   /**
     *   Example key of type String:
     *   PluginConfigKey builder accepts all values needed to create a key such as:
     *   PluginId: Id for current plugin
     *   id: Key Id
     *   type: Type of key, options for this are enlisted here {@link PluginConfigKeyType}
     *   default value: defines default value for key
     *   allowed value: defines range of allowed values
     *   label: key label
     *   description: key description
     *   required: accepts true/false
     *   showIfKeyHasValue: this parameter defines conditional usage of plugin configuration key based on other key's value,
     *                      for details see {@link PluginConfigKeyConditional}
     *
     *   With above configuration, the created key name will be: plugin.__fixed_package__.config.list. This name can be used in configUI to set values.
     *
     *   For details see {@link PluginConfigKey}
     */
    public final PluginConfigKey<List<String>> LIST_KEY = PluginConfigKey.<List<String>>builder()
            .pluginId(getPluginId())
            .id("list")
            .type(PluginConfigKeyType.builder().type(PluginConfigKeyType.Format.ARRAY).subtype(PluginConfigKeyType.Format.STRING).build())
            .defaultValue(List.of())
            .label("List key")
            .description("Define a list of strings")
            .build();

    /**
     *   Example of plugin configuration file.
     *
     *   If a file is needed to define config rules it can be defined using PluginConfigFile properties and applied while implementing the plugin logic.
     *   Please note that validation check for file format needs to be handled by plugin developer.
     *   Also there is no option of checking file format while uploading it. Validations can be done only while reading the file.
     *
     *   For details see {@link PluginConfigFile}
     **/
    public final PluginConfigFile RULES_FILE = PluginConfigFile.builder()
            .name("config-rules.cfg")
            .format("json")
            .label("Config file")
            .description("List of rules to gather data")
            .build();

Implement the plugin logic

The actual logic of what the plugin does is implemented in the other Java files. The files you will need to edit, and the methods you need to write depend on what interfaces your plugin is implementing.

For our simple example we are only implementing the SearchLifeCycle interface, which means you only need to implement your plugin logic in a single java file.

  1. Swap back to the TitlePrefixSearchLifeCyclePlugin.java file and add the following code:

    @Override
    public void postProcess(SearchLifeCycleContext searchLifeCycleContext, SearchTransaction transaction) {
      // This is where your code will go.
    }

    The code within this method implements the different steps that you wish to occur (for this plugin) during the post process phase of the search lifecycle.

    If you are converting a plugin that previously used a Groovy hook script, cut and paste the corresponding Groovy code into the method and then convert any Groovy native code into the equivalent Java. IntelliJ will help you identify any errors in your code.

For this tutorial, the code will implement the following:

  • Read a pattern that will be matched against each title from the results page configuration.

  • Examine each of the search results and replace the title if there is a match to the pattern.

Getting input from the data source or results pages configuration

This uses the getCurrentProfileConfig() method to read results page configuration.

Update the postProcess method:

package com.example.plugin.titleprefixplugin;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.funnelback.plugin.SearchLifeCyclePlugin;
import com.funnelback.publicui.search.model.transaction.SearchTransaction;

public class TitlePrefixPluginSearchLifeCyclePlugin implements SearchLifeCyclePlugin {

    private static final Logger log = LogManager.getLogger(TitlePrefixPluginSearchLifeCyclePlugin.class); (1)

    @Override
    public void postProcess(SearchLifeCycleContext searchLifeCycleContext, SearchTransaction transaction) {
        String prefixToMatch = transaction.getQuestion().getCurrentProfileConfig().get(pluginUtils.PATTERN.getKey()); (2)
        String givenReplaceWith = transaction.getQuestion().getCurrentProfileConfig().get(pluginUtils.REPLACEMENT.getKey()); (3)

        if (prefixToMatch == null || prefixToMatch.isBlank()) { (4)
            // There is no prefix configured, we can't do anything. Let's log a warning and abort.
            log.warn("Could not find a pattern specified at " + pluginUtils.PATTERN.getKey() +  ". The TitlePrefixPlugin will do nothing."); (5)
            return;
        }
        // We'll mark replaceWith as an optional key, if someone doesn't supply it, then we'll just remove the pattern configured.
        String replaceWithToUse = givenReplaceWith != null ? givenReplaceWith: ""; (6)

    }
}
1 A logging mechanism has already been set up for us. This will come in handy when we learn about debugging and logging later.
2 Read the pattern used for title comparisons from the results page configuration and assign it to prefixToMatch. The getCurrentProfileConfig helper method reads results page configuration. This call reads the value of the plugin.title-prefix.config.pattern key.
3 Read the replacement value from plugin.title-prefix.config.replaceWith in the results page configuration.
4 Check for the existence of the pattern configuration value because that is required for this plugin to run and it may have not been set in configuration. If the values are not set then the plugin can’t run so it should exit graciously.
5 Write a log message that indicates why this plugin is not running. This will help someone who has misconfigured the plugin to understand why it is not working.
6 Check that a replacement pattern (defined by the replaceWith option) is set. This option has been written to be optional - if it is not set in configuration then a default value is applied instead.

Change the titles which match the pattern

Update the postProcess method to modify the response that our search results have. Add the following lines to the end of the method:

transaction.getResponse().getResultPacket().getResults().stream().filter(result -> { { (1)
    return result.getTitle().startsWith(prefixToMatch); (2)
}).forEach(matchingResult -> { (3)
    String modifiedTitle = matchingResult.getTitle().replace(prefixToMatch, replaceWithToUse); (4)
    matchingResult.setTitle(modifiedTitle); (5)
});
1 Iterate over each search result and apply a filter.
2 Filters the list by returning only the results whose titles start with the prefix that was set in configuration.
3 Go through the results which passed our filter.
4 Calculate the new title to use.
5 Modify the title to be the new title.

The functionality of the plugin has now been implemented in the source code.

The completed PluginUtils.java and TitlePrefixSearchLifeCyclePlugin.java are shown below:

PluginUtils.java
package com.example.plugin.titleprefix;

import com.funnelback.plugin.details.model.*;
import com.funnelback.plugin.docs.model.*;
import com.funnelback.plugin.PluginUtilsBase;

import java.util.List;

public class PluginUtils implements PluginUtilsBase {

    public final PluginConfigKey<String> PATTERN = PluginConfigKey.<String>builder()
            .pluginId(getPluginId())
            .id("pattern")
            .type(PluginConfigKeyType.builder().type(PluginConfigKeyType.Format.STRING).build())
            .defaultValue(".")
            .label("Prefix pattern")
            .description("Pattern of characters to match prefix against")
            .longDescription("The pattern specified here is a _Java regular expression_ that is matched against the title.\n"+
                            "\n"+
                    "See also: link:https://www.regexplanet.com/advanced/java/index.html[Java regex tester]")
            .build();

    public final PluginConfigKey<String> REPLACEMENT = PluginConfigKey.<String>builder()
            .pluginId(getPluginId())
            .id("replaceWith")
            .type(PluginConfigKeyType.builder().type(PluginConfigKeyType.Format.STRING).build())
            .defaultValue("1")
            .label("Replace with")
            .description("Pattern of characters to match prefix against")
            .build();

    /**
     *  Returns list of Configuration keys defined for plugin
     *   All configuration keys defined above need to be included in the list to ensure that they can be configured via UI
     */
    @Override public List <PluginConfigKeyDetails> getConfigKeys() {
        return List.of(PATTERN, REPLACEMENT);
    }

    /**
     *  Returns list of Configuration files defined for plugin
     *  All configuration files defined above need to be included in the list to ensure that they can be uploaded via UI
     */
    @Override public List <PluginConfigFile> getConfigFiles() {
        return List.of();
    }
    /**
      *  Audience field is used to flag the content of the documentation page as being suitable for one or more of these DXP audiences.
      *  There are different types of Audience defined here {@link Audience}
      *  Please choose appropriate option from the available list.
      *  Most widely used option is site builder, others can be set while developing a plugin on need basis.
      *
      *  This method returns list of Audience selected for the plugin.
     */
    @Override public List <Audience> getAudience() {
        return List.of(Audience.SITE_BUILDER);
    }

    /**
     *  Marketplace subtype is mostly defined by the interfaces that are implemented for the plugin.
     *  Complete list if marketplace subtypes is defined here {@link MarketplaceSubtype}
     */
    @Override public List <MarketplaceSubtype> getMarketplaceSubtype() {
        return List.of(MarketplaceSubtype.GATHERER);
    }

    /**
     *   Product topic is about describing the topics that the plugin you’re building relates to.
     *   So when you select one of those topics in the facets for example the plugin will come up in the possible results.
     *   Available options can be found here {@link ProductTopic}
     */
    @Override public List <ProductTopic> getProductTopic() {
        return List.of(ProductTopic.DATA_SOURCES, ProductTopic.INTEGRATION_DEVELOPMENT);
    }

    /**
     *   Product subtopics are defined for each of the product topic.
     *   Available options can be found here {@link ProductSubtopicCategory}
     */
    @Override public List <ProductSubtopicCategory> getProductSubtopic() {
        return List.of(ProductSubtopic.DataSources.CUSTOM, ProductSubtopic.IntegrationDevelopment.PERFORMANCE);
    }

    /**
     *    Returns plugin ID which should match artifactId from pom.xml
     */
    @Override public String getPluginId() {
        return "title-prefix";
    }

    /**
     *    Returns plugin name which should match plugin-name from pom.xml
     */
    @Override public String getPluginName() {
        return "Modify title prefix";
    }

    /**
     *    Returns plugin description which should match plugin-description from pom.xml
     */
    @Override public String getPluginDescription() {
        return "Use this plugin to modify the prefix of search result titles";
    }

    /**
     *   Plugin target can be set here - it indicates if the plugin runs on a data source or a results page (or both)
     *   Available options can be found here {@link PluginTarget}
     **/
    @Override public List <PluginTarget> getPluginTarget() {
        return List.of(PluginTarget.RESULTS_PAGE);
    }

    @Override public String getFilterClass() {
        return null;
    }

    @Override public String getJsoupFilterClass() {
        return null;
    }

}
TitlePrefixSearchLifeCyclePlugin.java
package com.example.plugin.titleprefix;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.funnelback.plugin.SearchLifeCyclePlugin;
import com.funnelback.publicui.search.model.transaction.SearchTransaction;
import com.funnelback.plugin.search.SearchLifeCycleContext;

public class TitleprefixSearchLifeCyclePlugin implements SearchLifeCyclePlugin {

    final PluginUtils pluginUtils = new PluginUtils();
    private static final Logger log = LogManager.getLogger(TitleprefixSearchLifeCyclePlugin.class);

    @Override
    public void postProcess(SearchLifeCycleContext searchLifeCycleContext, SearchTransaction transaction) {

        String prefixToMatch = transaction.getQuestion().getCurrentProfileConfig().get(pluginUtils.PATTERN.getKey());
        String givenReplaceWith = transaction.getQuestion().getCurrentProfileConfig().get(pluginUtils.REPLACEMENT.getKey());

        if (prefixToMatch == null || prefixToMatch.isBlank()) {
            // There is no prefix configured, we can't do anything. Let's log a warning and abort.
            log.warn("Could not find a pattern specified at " + pluginUtils.PATTERN.getKey() +  ". The TitlePrefixPlugin will do nothing.");
            return;
        }

        // We'll mark replaceWith as an optional key, if someone doesn't supply it, then we'll just remove the pattern configured.
        String replaceWithToUse = givenReplaceWith != null ? givenReplaceWith: "";

        transaction.getResponse().getResultPacket().getResults().stream().filter(result -> {
            return result.getTitle().startsWith(prefixToMatch);
        }).forEach(matchingResult -> {
            String modifiedTitle = matchingResult.getTitle().replace(prefixToMatch, replaceWithToUse);
            matchingResult.setTitle(modifiedTitle);
        });
    }
}

Before we can build the plugin we need to create some documentation for the plugin.

Next steps

The next tutorial covers the creation of documentation for the plugin from a template.