org.dita.dost.platform.Integrator.java Source code

Java tutorial

Introduction

Here is the source code for org.dita.dost.platform.Integrator.java

Source

/*
 * This file is part of the DITA Open Toolkit project.
 * See the accompanying license.txt file for applicable licenses.
 */

/*
 * (c) Copyright IBM Corp. 2005, 2006 All Rights Reserved.
 */
package org.dita.dost.platform;

import org.dita.dost.log.DITAOTJavaLogger;
import org.dita.dost.log.DITAOTLogger;
import org.dita.dost.log.MessageUtils;
import org.dita.dost.util.Configuration;
import org.dita.dost.util.FileUtils;
import org.dita.dost.util.StringUtils;
import org.dita.dost.util.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.net.URI;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import static java.util.Arrays.asList;
import static javax.xml.XMLConstants.XML_NS_PREFIX;
import static javax.xml.XMLConstants.XML_NS_URI;
import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.dita.dost.platform.PluginParser.FEATURE_ELEM;
import static org.dita.dost.platform.PluginParser.FEATURE_ID_ATTR;
import static org.dita.dost.util.Configuration.configuration;
import static org.dita.dost.util.Constants.*;
import static org.dita.dost.util.URLUtils.getRelativePath;
import static org.dita.dost.util.URLUtils.toFile;

/**
 * Integrator is the main class to control and excute the integration of the
 * toolkit and different plug-ins.
 * 
 * @author Zhang, Yuan Peng
 */
public final class Integrator {

    private static final String CONF_PLUGIN_ORDER = "plugin.order";
    private static final String CONF_PLUGIN_IGNORES = "plugin.ignores";
    private static final String CONF_PLUGIN_DIRS = "plugindirs";
    /** Feature name for supported image extensions. */
    private static final String FEAT_IMAGE_EXTENSIONS = "dita.image.extensions";
    /** Feature name for supported image extensions. */
    private static final String FEAT_HTML_EXTENSIONS = "dita.html.extensions";
    /** Feature name for supported resource file extensions. */
    private static final String FEAT_RESOURCE_EXTENSIONS = "dita.resource.extensions";
    /** Feature name for print transformation types. */
    private static final String FEAT_PRINT_TRANSTYPES = "dita.transtype.print";
    private static final String FEAT_LIB_EXTENSIONS = "dita.conductor.lib.import";
    private static final String ELEM_PLUGINS = "plugins";

    public static final String FEAT_VALUE_SEPARATOR = ",";
    private static final String PARAM_VALUE_SEPARATOR = ";";

    public static final Pattern ID_PATTERN = Pattern.compile("[0-9a-zA-Z_\\-]+(?:\\.[0-9a-zA-Z_\\-]+)*");
    public static final Pattern VERSION_PATTERN = Pattern
            .compile("\\d+(?:\\.\\d+(?:\\.\\d+(?:\\.[0-9a-zA-Z_\\-]+)?)?)?");

    /** Plugin table which contains detected plugins. */
    private final Map<String, Features> pluginTable;
    private final Set<String> templateSet = new HashSet<>(16);
    private final File ditaDir;
    /** Plugin configuration file. */
    private final Set<File> descSet;
    private final XMLReader reader;
    private final Document pluginsDoc;
    private final PluginParser parser;
    private DITAOTLogger logger;
    private final Set<String> loadedPlugin;
    private final Hashtable<String, List<String>> featureTable;
    @Deprecated
    private File propertiesFile;
    private final Set<String> extensionPoints;
    private boolean strict = false;
    private final Map<String, Integer> pluginOrder = new HashMap<>();
    private Properties properties;

    /**
     * Default Constructor.
     */
    public Integrator(final File ditaDir) {
        this.ditaDir = ditaDir;
        pluginTable = new HashMap<>(16);
        descSet = new HashSet<>(16);
        loadedPlugin = new HashSet<>(16);
        featureTable = new Hashtable<>(16);
        extensionPoints = new HashSet<>();
        try {
            reader = XMLUtils.getXMLReader();
        } catch (final Exception e) {
            throw new RuntimeException("Failed to initialize XML parser: " + e.getMessage(), e);
        }
        reader.setErrorHandler(new ErrorHandler() {
            @Override
            public void error(final SAXParseException e) throws SAXException {
                throw e;
            }

            @Override
            public void fatalError(final SAXParseException e) throws SAXException {
                throw e;
            }

            @Override
            public void warning(final SAXParseException e) throws SAXException {
                throw e;
            }
        });
        parser = new PluginParser(ditaDir);
        try {
            pluginsDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        } catch (final ParserConfigurationException e) {
            throw new RuntimeException("Failed to create compound document: " + e.getMessage(), e);
        }
        //        pluginsDoc.setResult(new StreamResult(new File(ditaDir, RESOURCES_DIR + File.separator + "plugins.xml")));
    }

    /**
     * Execute point of Integrator.
     */
    public void execute() throws Exception {
        if (logger == null) {
            logger = new DITAOTJavaLogger();
        }

        // Read the properties file, if it exists.
        properties = new Properties();
        if (propertiesFile != null) {
            FileInputStream propertiesStream = null;
            try {
                propertiesStream = new FileInputStream(propertiesFile);
                properties.load(propertiesStream);
            } catch (final Exception e) {
                if (strict) {
                    throw new RuntimeException(e);
                } else {
                    logger.error(e.getMessage(), e);
                }
            } finally {
                if (propertiesStream != null) {
                    try {
                        propertiesStream.close();
                    } catch (final IOException e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        } else {
            properties.putAll(Configuration.configuration);
        }
        if (!properties.containsKey(CONF_PLUGIN_DIRS)) {
            properties.setProperty(CONF_PLUGIN_DIRS,
                    configuration.containsKey(CONF_PLUGIN_DIRS) ? configuration.get(CONF_PLUGIN_DIRS)
                            : "plugins;demo");
        }
        if (!properties.containsKey(CONF_PLUGIN_IGNORES)) {
            properties.setProperty(CONF_PLUGIN_IGNORES,
                    configuration.containsKey(CONF_PLUGIN_IGNORES) ? configuration.get(CONF_PLUGIN_IGNORES) : "");
        }

        // Get the list of plugin directories from the properties.
        final String[] pluginDirs = properties.getProperty(CONF_PLUGIN_DIRS).split(PARAM_VALUE_SEPARATOR);

        final Set<String> pluginIgnores = new HashSet<>();
        if (properties.getProperty(CONF_PLUGIN_IGNORES) != null) {
            pluginIgnores.addAll(
                    Arrays.asList(properties.getProperty(CONF_PLUGIN_IGNORES).split(PARAM_VALUE_SEPARATOR)));
        }

        final String pluginOrderProperty = properties.getProperty(CONF_PLUGIN_ORDER);
        if (pluginOrderProperty != null) {
            final List<String> plugins = asList(pluginOrderProperty.trim().split("\\s+"));
            Collections.reverse(plugins);
            int priority = 1;
            for (final String plugin : plugins) {
                pluginOrder.put(plugin, priority++);
            }
        }

        for (final String tmpl : properties.getProperty(CONF_TEMPLATES, "").split(PARAM_VALUE_SEPARATOR)) {
            final String t = tmpl.trim();
            if (t.length() != 0) {
                templateSet.add(t);
            }
        }

        for (final String pluginDir2 : pluginDirs) {
            File pluginDir = new File(pluginDir2);
            if (!pluginDir.isAbsolute()) {
                pluginDir = new File(ditaDir, pluginDir.getPath());
            }
            final File[] pluginFiles = pluginDir.listFiles();

            for (int i = 0; (pluginFiles != null) && (i < pluginFiles.length); i++) {
                final File f = pluginFiles[i];
                final File descFile = new File(pluginFiles[i], "plugin.xml");
                if (pluginFiles[i].isDirectory() && !pluginIgnores.contains(f.getName()) && descFile.exists()) {
                    descSet.add(descFile);
                }
            }
        }

        parsePlugin();
        integrate();
    }

    /**
     * Generate and process plugin files.
     */
    private void integrate() throws Exception {
        writePlugins();

        // Collect information for each feature id and generate a feature table.
        final FileGenerator fileGen = new FileGenerator(featureTable, pluginTable);
        fileGen.setLogger(logger);
        for (final String currentPlugin : orderPlugins(pluginTable.keySet())) {
            loadPlugin(currentPlugin);
        }

        // generate the files from template
        for (final String template : templateSet) {
            final File templateFile = new File(ditaDir, template);
            logger.debug("Process template " + templateFile.getPath());
            fileGen.generate(templateFile);
        }

        // generate configuration properties
        final Properties configuration = new Properties();
        // image extensions, support legacy property file extension
        final Set<String> imgExts = new HashSet<>();
        for (final String ext : properties.getProperty(CONF_SUPPORTED_IMAGE_EXTENSIONS, "")
                .split(CONF_LIST_SEPARATOR)) {
            final String e = ext.trim();
            if (e.length() != 0) {
                imgExts.add(e);
            }
        }
        if (featureTable.containsKey(FEAT_IMAGE_EXTENSIONS)) {
            for (final String ext : featureTable.get(FEAT_IMAGE_EXTENSIONS)) {
                final String e = ext.trim();
                if (e.length() != 0) {
                    imgExts.add(e);
                }
            }
        }
        configuration.put(CONF_SUPPORTED_IMAGE_EXTENSIONS, StringUtils.join(imgExts, CONF_LIST_SEPARATOR));
        // extensions
        configuration.put(CONF_SUPPORTED_HTML_EXTENSIONS, readExtensions(FEAT_HTML_EXTENSIONS));
        configuration.put(CONF_SUPPORTED_RESOURCE_EXTENSIONS, readExtensions(FEAT_RESOURCE_EXTENSIONS));

        // print transtypes
        final Set<String> printTranstypes = new HashSet<>();
        if (featureTable.containsKey(FEAT_PRINT_TRANSTYPES)) {
            for (final String ext : featureTable.get(FEAT_PRINT_TRANSTYPES)) {
                final String e = ext.trim();
                if (e.length() != 0) {
                    printTranstypes.add(e);
                }
            }
        }
        // support legacy property
        final String printTranstypeValue = properties.getProperty(CONF_PRINT_TRANSTYPES);
        if (printTranstypeValue != null) {
            printTranstypes.addAll(Arrays.asList(printTranstypeValue.split(PARAM_VALUE_SEPARATOR)));
        }
        configuration.put(CONF_PRINT_TRANSTYPES, StringUtils.join(printTranstypes, CONF_LIST_SEPARATOR));

        for (final Entry<String, Features> e : pluginTable.entrySet()) {
            final Features f = e.getValue();
            final String name = "plugin." + e.getKey() + ".dir";
            final List<String> baseDirValues = f.getFeature("dita.basedir-resource-directory");
            if (Boolean
                    .parseBoolean(baseDirValues == null || baseDirValues.isEmpty() ? null : baseDirValues.get(0))) {
                //configuration.put(name, ditaDir.getAbsolutePath());
                configuration.put(name, ".");
            } else {
                configuration.put(name,
                        FileUtils.getRelativePath(new File(ditaDir, "dummy"), f.getPluginDir()).getPath());
            }
        }
        configuration.putAll(getParserConfiguration());

        OutputStream out = null;
        try {
            final File outFile = new File(ditaDir, "lib" + File.separator + getClass().getPackage().getName()
                    + File.separator + GEN_CONF_PROPERTIES);
            if (!(outFile.getParentFile().exists()) && !outFile.getParentFile().mkdirs()) {
                throw new RuntimeException("Failed to make directory " + outFile.getParentFile().getAbsolutePath());
            }
            logger.debug("Generate configuration properties " + outFile.getPath());
            out = new BufferedOutputStream(new FileOutputStream(outFile));
            configuration.store(out, "DITA-OT runtime configuration, do not edit manually");
        } catch (final Exception e) {
            if (strict) {
                throw new RuntimeException("Failed to write configuration properties: " + e.getMessage(), e);
            } else {
                logger.error(e.getMessage(), e);
            }
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (final IOException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }

        final Collection<File> jars = featureTable.containsKey(FEAT_LIB_EXTENSIONS)
                ? relativize(new LinkedHashSet<>(featureTable.get(FEAT_LIB_EXTENSIONS)))
                : Collections.EMPTY_SET;
        writeEnvShell(jars);
        writeEnvBatch(jars);
    }

    private Iterable<String> orderPlugins(final Set<String> ids) {
        final List<String> res = new ArrayList<>(ids);
        Collections.sort(res, new Comparator<String>() {
            @Override
            public int compare(final String s1, final String s2) {
                final int score1 = pluginOrder.containsKey(s1) ? pluginOrder.get(s1) : 0;
                final int score2 = pluginOrder.containsKey(s2) ? pluginOrder.get(s2) : 0;
                if (score1 < score2) {
                    return 1;
                } else if (score1 > score2) {
                    return -1;
                } else {
                    return s1.compareTo(s2);
                }
            }
        });
        return res;
    }

    private Map<String, String> getParserConfiguration() {
        final Map<String, String> res = new HashMap<>();
        final NodeList features = pluginsDoc.getElementsByTagName(FEATURE_ELEM);
        for (int i = 0; i < features.getLength(); i++) {
            final Element feature = (Element) features.item(i);
            if (feature.getAttribute(FEATURE_ID_ATTR).equals("dita.parser")) {
                final NodeList parsers = feature.getElementsByTagName("parser");
                for (int j = 0; j < parsers.getLength(); j++) {
                    final Element parser = (Element) parsers.item(j);
                    res.put("parser." + parser.getAttribute("format"), parser.getAttribute("class"));
                }
            }
        }
        return res;
    }

    private Collection<File> relativize(final Collection<String> src) {
        final Collection<File> res = new ArrayList<>(src.size());
        final File base = new File(ditaDir, "dummy");
        for (final String lib : src) {
            res.add(FileUtils.getRelativePath(base, toFile(lib)));
        }
        return res;
    }

    private void writeEnvShell(final Collection<File> jars) {
        Writer out = null;
        try {
            final File outFile = new File(ditaDir, "resources" + File.separator + "env.sh");
            if (!(outFile.getParentFile().exists()) && !outFile.getParentFile().mkdirs()) {
                throw new RuntimeException("Failed to make directory " + outFile.getParentFile().getAbsolutePath());
            }
            logger.debug("Generate environment shell " + outFile.getPath());
            out = new BufferedWriter(new FileWriter(outFile));

            out.write("#!/bin/sh\n");
            for (final File relativeLib : jars) {
                out.write("CLASSPATH=\"$CLASSPATH:");
                if (!relativeLib.isAbsolute()) {
                    out.write("$DITA_HOME" + UNIX_SEPARATOR);
                }
                out.write(relativeLib.toString().replace(File.separator, UNIX_SEPARATOR));
                out.write("\"\n");
            }
        } catch (final Exception e) {
            if (strict) {
                throw new RuntimeException("Failed to write environment shell: " + e.getMessage(), e);
            } else {
                logger.error(e.getMessage(), e);
            }
        } finally {
            closeQuietly(out);
        }
    }

    private void writeEnvBatch(final Collection<File> jars) {
        Writer out = null;
        try {
            final File outFile = new File(ditaDir, "resources" + File.separator + "env.bat");
            if (!(outFile.getParentFile().exists()) && !outFile.getParentFile().mkdirs()) {
                throw new RuntimeException("Failed to make directory " + outFile.getParentFile().getAbsolutePath());
            }
            logger.debug("Generate environment batch " + outFile.getPath());
            out = new BufferedWriter(new FileWriter(outFile));

            for (final File relativeLib : jars) {
                out.write("set \"CLASSPATH=%CLASSPATH%;");
                if (!relativeLib.isAbsolute()) {
                    out.write("%DITA_HOME%" + WINDOWS_SEPARATOR);
                }
                out.write(relativeLib.toString().replace(File.separator, WINDOWS_SEPARATOR));
                out.write("\"\r\n");
            }
        } catch (final Exception e) {
            if (strict) {
                throw new RuntimeException("Failed to write environment batch: " + e.getMessage(), e);
            } else {
                logger.error(e.getMessage(), e);
            }
        } finally {
            closeQuietly(out);
        }
    }

    /**
     * Read plug-in feature.
     * 
     * @param featureName plug-in feature name
     * @return combined list of values
     */
    private String readExtensions(final String featureName) {
        final Set<String> exts = new HashSet<>();
        if (featureTable.containsKey(featureName)) {
            for (final String ext : featureTable.get(featureName)) {
                final String e = ext.trim();
                if (e.length() != 0) {
                    exts.add(e);
                }
            }
        }
        return StringUtils.join(exts, CONF_LIST_SEPARATOR);
    }

    /**
     * Load the plug-ins and aggregate them by feature and fill into feature
     * table.
     * 
     * @param plugin plugin ID
     * @return {@code true}> if plugin was loaded, otherwise {@code false}
     */
    private boolean loadPlugin(final String plugin) {
        if (checkPlugin(plugin)) {
            final Features pluginFeatures = pluginTable.get(plugin);
            final Map<String, List<String>> featureSet = pluginFeatures.getAllFeatures();
            for (final Map.Entry<String, List<String>> currentFeature : featureSet.entrySet()) {
                if (!extensionPoints.contains(currentFeature.getKey())) {
                    final String msg = "Plug-in " + plugin + " uses an undefined extension point "
                            + currentFeature.getKey();
                    if (strict) {
                        throw new RuntimeException(msg);
                    } else {
                        logger.debug(msg);
                    }
                }
                if (featureTable.containsKey(currentFeature.getKey())) {
                    final List<String> value = featureTable.get(currentFeature.getKey());
                    value.addAll(currentFeature.getValue());
                    featureTable.put(currentFeature.getKey(), value);
                } else {
                    featureTable.put(currentFeature.getKey(), currentFeature.getValue());
                }
            }

            for (final String templateName : pluginFeatures.getAllTemplates()) {
                templateSet.add(FileUtils.getRelativeUnixPath(ditaDir + File.separator + "dummy",
                        pluginFeatures.getPluginDir() + File.separator + templateName));
            }
            loadedPlugin.add(plugin);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Check whether the plugin can be loaded.
     * 
     * @param currentPlugin plugin ID
     * @return {@code true} if plugin can be loaded, otherwise {@code false}
     */
    private boolean checkPlugin(final String currentPlugin) {
        final Features pluginFeatures = pluginTable.get(currentPlugin);
        final Iterator<PluginRequirement> iter = pluginFeatures.getRequireListIter();
        // check whether dependcy is satisfied
        while (iter.hasNext()) {
            boolean anyPluginFound = false;
            final PluginRequirement requirement = iter.next();
            final Iterator<String> requiredPluginIter = requirement.getPlugins();
            while (requiredPluginIter.hasNext()) {
                // Iterate over all alternatives in plugin requirement.
                final String requiredPlugin = requiredPluginIter.next();
                if (pluginTable.containsKey(requiredPlugin)) {
                    if (!loadedPlugin.contains(requiredPlugin)) {
                        // required plug-in is not loaded
                        loadPlugin(requiredPlugin);
                    }
                    // As soon as any plugin is found, it's OK.
                    anyPluginFound = true;
                }
            }
            if (!anyPluginFound && requirement.getRequired()) {
                // not contain any plugin required by current plugin
                final String msg = MessageUtils.getInstance()
                        .getMessage("DOTJ020W", requirement.toString(), currentPlugin).toString();
                if (strict) {
                    throw new RuntimeException(msg);
                } else {
                    logger.warn(msg);
                }
                return false;
            }
        }
        return true;
    }

    /**
     * Parse plugin configuration files.
     */
    private void parsePlugin() {
        final Element root = pluginsDoc.createElement(ELEM_PLUGINS);
        pluginsDoc.appendChild(root);
        if (!descSet.isEmpty()) {
            final URI b = new File(ditaDir, RESOURCES_DIR + File.separator + "plugins.xml").toURI();
            for (final File descFile : descSet) {
                logger.debug("Read plug-in configuration " + descFile.getPath());
                final Element plugin = parseDesc(descFile);
                if (plugin != null) {
                    final URI base = getRelativePath(b, descFile.toURI());
                    plugin.setAttributeNS(XML_NS_URI, XML_NS_PREFIX + ":base", base.toString());
                    root.appendChild(pluginsDoc.importNode(plugin, true));
                }
            }
        }
    }

    private void writePlugins() throws TransformerException {
        final File plugins = new File(ditaDir, RESOURCES_DIR + File.separator + "plugins.xml");
        logger.debug("Writing " + plugins);
        try {
            final Transformer serializer = TransformerFactory.newInstance().newTransformer();
            serializer.transform(new DOMSource(pluginsDoc), new StreamResult(plugins));
        } catch (final TransformerConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Parse plugin configuration file
     * 
     * @param descFile plugin configuration
     */
    private Element parseDesc(final File descFile) {
        try {
            parser.setPluginDir(descFile.getParentFile());
            final Element root = parser.parse(descFile.getAbsoluteFile());
            final Features f = parser.getFeatures();
            final String id = f.getPluginId();
            validatePlugin(f);
            extensionPoints.addAll(f.getExtensionPoints().keySet());
            pluginTable.put(id, f);
            return root;
        } catch (final RuntimeException e) {
            throw e;
        } catch (final SAXParseException e) {
            final RuntimeException ex = new RuntimeException(
                    "Failed to parse " + descFile.getAbsolutePath() + ": " + e.getMessage(), e);
            if (strict) {
                throw ex;
            } else {
                logger.error(ex.getMessage(), ex);
            }
        } catch (final Exception e) {
            if (strict) {
                throw new RuntimeException(e);
            } else {
                logger.error(e.getMessage(), e);
            }
        }
        return null;
    }

    /**
     * Validate plug-in configuration.
     * 
     * Follow OSGi symbolic name syntax rules:
     * 
     * <pre>
     * digit         ::= [0..9]
     * alpha         ::= [a..zA..Z]
     * alphanum      ::= alpha | digit
     * token         ::= ( alphanum | '_' | '-' )+
     * symbolic-name ::= token('.'token)*
     * </pre>
     * 
     * Follow OSGi bundle version syntax rules:
     * 
     * <pre>
     * version   ::= major( '.' minor ( '.' micro ( '.' qualifier )? )? )?
     * major     ::= number
     * minor     ::=number
     * micro     ::=number
     * qualifier ::= ( alphanum | '_' | '-' )+
     * </pre>
     * 
     * @param f Features to validate
     */
    private void validatePlugin(final Features f) {
        final String id = f.getPluginId();
        if (!ID_PATTERN.matcher(id).matches()) {
            final String msg = "Plug-in ID '" + id
                    + "' doesn't follow recommended syntax rules, support for nonconforming IDs may be removed in future releases.";
            if (strict) {
                throw new IllegalArgumentException(msg);
            } else {
                logger.warn(msg);
            }
        }
        final List<String> version = f.getFeature("package.version");
        if (version != null && !version.isEmpty() && !VERSION_PATTERN.matcher(version.get(0)).matches()) {
            final String msg = "Plug-in version '" + version
                    + "' doesn't follow recommended syntax rules, support for nonconforming version may be removed in future releases.";
            if (strict) {
                throw new IllegalArgumentException(msg);
            } else {
                logger.warn(msg);
            }
        }
    }

    /**
     * Set the properties file.
     * 
     * @param propertiesfile properties file
     */
    public void setProperties(final File propertiesfile) {
        propertiesFile = propertiesfile;
    }

    /**
     * Setter for strict/lax mode.
     * 
     * @param strict {@code true} for strict mode, {@code false} for lax mode
     */
    public void setStrict(final boolean strict) {
        this.strict = strict;
    }

    /**
     * Set logger.
     * 
     * @param logger logger instance
     */
    public void setLogger(final DITAOTLogger logger) {
        this.logger = logger;
    }

    /**
     * Get all and combine extension values
     * 
     * @param featureTable plugin features
     * @param extension extension ID
     * @return combined extension value, {@code null} if no value available
     */
    static String getValue(final Map<String, Features> featureTable, final String extension) {
        final List<String> buf = new ArrayList<>();
        for (final Features f : featureTable.values()) {
            final List<String> v = f.getFeature(extension);
            if (v != null) {
                buf.addAll(v);
            }
        }
        if (buf.isEmpty()) {
            return null;
        } else {
            return StringUtils.join(buf, ",");
        }
    }

}