net.sf.eclipsecs.core.config.meta.MetadataFactory.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.eclipsecs.core.config.meta.MetadataFactory.java

Source

//============================================================================
//
// Copyright (C) 2002-2014  David Schneider, Lars Kdderitzsch
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//============================================================================

package net.sf.eclipsecs.core.config.meta;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.ResourceBundle.Control;

import net.sf.eclipsecs.core.CheckstylePlugin;
import net.sf.eclipsecs.core.config.ConfigProperty;
import net.sf.eclipsecs.core.config.Module;
import net.sf.eclipsecs.core.config.Severity;
import net.sf.eclipsecs.core.config.XMLTags;
import net.sf.eclipsecs.core.util.CheckstyleLog;
import net.sf.eclipsecs.core.util.CheckstylePluginException;
import net.sf.eclipsecs.core.util.XMLUtil;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;

/**
 * This class is the factory for all Checkstyle rule metadata.
 */
public final class MetadataFactory {

    /** Map containing the public - internal DTD mapping. */
    private static final Map<String, String> PUBLIC2INTERNAL_DTD_MAP = new HashMap<String, String>();

    /** Metadata for the rule groups. */
    private static Map<String, RuleGroupMetadata> sRuleGroupMetadata;

    /** Metadata for all rules, keyed by internal rule name. */
    private static Map<String, RuleMetadata> sRuleMetadata;

    /**
     * Mapping for all rules, keyed by alternative rule names (full qualified, old full qualified).
     */
    private static Map<String, RuleMetadata> sAlternativeNamesMap;

    /** Name of the rules metadata XML file. */
    private static final String METADATA_FILENAME = "checkstyle-metadata.xml"; //$NON-NLS-1$

    /**
     * Private constructor to prevent instantiation.
     */
    private MetadataFactory() {
    }

    /**
     * Static initializer.
     */
    static {
        PUBLIC2INTERNAL_DTD_MAP.put("-//eclipse-cs//DTD Check Metadata 1.0//EN", //$NON-NLS-1$
                "/com/puppycrawl/tools/checkstyle/checkstyle-metadata_1_0.dtd"); //$NON-NLS-1$
        PUBLIC2INTERNAL_DTD_MAP.put("-//eclipse-cs//DTD Check Metadata 1.1//EN", //$NON-NLS-1$
                "/com/puppycrawl/tools/checkstyle/checkstyle-metadata_1_1.dtd"); //$NON-NLS-1$

        refresh();
    }

    /**
     * Get a list of metadata objects for all rule groups.
     *
     * @return List of <code>RuleGroupMetadata</code> objects.
     */
    public static List<RuleGroupMetadata> getRuleGroupMetadata() {

        List<RuleGroupMetadata> groups = new ArrayList<RuleGroupMetadata>(sRuleGroupMetadata.values());
        Collections.sort(groups, new Comparator<RuleGroupMetadata>() {

            public int compare(RuleGroupMetadata arg0, RuleGroupMetadata arg1) {
                int prio1 = arg0.getPriority();
                int prio2 = arg1.getPriority();

                return (prio1 < prio2 ? -1 : (prio1 == prio2 ? 0 : 1));
            }
        });

        return groups;
    }

    /**
     * Get metadata for a check rule.
     *
     * @param name
     *            The rule's name within the checkstyle configuration file.
     * @return The metadata.
     */
    public static RuleMetadata getRuleMetadata(String name) {

        RuleMetadata metadata = null;

        // first try the internal name mapping
        metadata = sRuleMetadata.get(name);

        // try the alternative names
        if (metadata == null) {
            metadata = sAlternativeNamesMap.get(name);
        }

        return metadata;
    }

    /**
     * Returns the metadata for a rule group.
     *
     * @param name
     *            the group name
     * @return the RuleGroupMetadata object or <code>null</code>
     */
    public static RuleGroupMetadata getRuleGroupMetadata(String name) {
        return sRuleGroupMetadata.get(name);
    }

    /**
     * Creates a set of generic metadata for a module that has no metadata delivered with the plugin.
     *
     * @param module
     *            the module
     * @return the generic metadata built
     */
    public static RuleMetadata createGenericMetadata(Module module) {

        String parent = null;
        try {

            Class<?> checkClass = CheckstylePlugin.getDefault().getAddonExtensionClassLoader()
                    .loadClass(module.getName());

            Object moduleInstance = checkClass.newInstance();

            if (moduleInstance instanceof AbstractFileSetCheck) {
                parent = XMLTags.CHECKER_MODULE;
            } else {
                parent = XMLTags.TREEWALKER_MODULE;
            }
        } catch (Exception e) {
            // Ok we tried... default to TreeWalker
            parent = XMLTags.TREEWALKER_MODULE;
        }

        RuleGroupMetadata otherGroup = getRuleGroupMetadata(XMLTags.OTHER_GROUP);
        RuleMetadata ruleMeta = new RuleMetadata(module.getName(), module.getName(), parent,
                MetadataFactory.getDefaultSeverity(), false, true, true, false, otherGroup);
        module.setMetaData(ruleMeta);
        sRuleMetadata.put(ruleMeta.getInternalName(), ruleMeta);

        List<ConfigProperty> properties = module.getProperties();
        int size = properties != null ? properties.size() : 0;
        for (int i = 0; i < size; i++) {

            ConfigProperty property = properties.get(i);
            ConfigPropertyMetadata meta = new ConfigPropertyMetadata(ConfigPropertyType.String, property.getName(),
                    null, null);
            property.setMetaData(meta);
        }
        return ruleMeta;
    }

    /**
     * Returns the default severity level.
     *
     * @return the default severity.
     */
    public static Severity getDefaultSeverity() {
        return Severity.inherit;
    }

    /**
     * Returns Checkstyles standard message for a given module name and message key.
     *
     * @param messageKey
     *            the message key
     * @param moduleInternalName
     *            the module name
     * @return Checkstyles standard message for this module and key
     */
    public static String getStandardMessage(String messageKey, String moduleInternalName) {

        RuleMetadata rule = getRuleMetadata(moduleInternalName);
        return getStandardMessage(messageKey, rule);
    }

    /**
     * Returns Checkstyles standard message for a given module and message key.
     *
     * @param messageKey
     *            the message key
     * @param rule
     *            the module metadata
     * @return Checkstyles standard message for this module and key
     */
    public static String getStandardMessage(String messageKey, RuleMetadata rule) {

        // for some unknown reason there is no metadata or key
        if (messageKey == null || rule == null) {
            return null;
        }

        List<String> namesToCheck = new ArrayList<String>();
        namesToCheck.add(rule.getInternalName());
        namesToCheck.addAll(rule.getAlternativeNames());

        for (String moduleClass : namesToCheck) {
            try {

                int endIndex = moduleClass.lastIndexOf('.');
                String messages = "messages"; //$NON-NLS-1$
                if (endIndex >= 0) {
                    String packageName = moduleClass.substring(0, endIndex);
                    messages = packageName + "." + messages; //$NON-NLS-1$
                }
                ResourceBundle resourceBundle = ResourceBundle.getBundle(messages,
                        CheckstylePlugin.getPlatformLocale(), CheckstylePlugin.class.getClassLoader(),
                        new UTF8Control());

                String message = resourceBundle.getString(messageKey);
                return message;
            } catch (MissingResourceException e) {
                // let's continue to check the other alternative names
            }
        }
        return null;
    }

    /**
     * Custom ResourceBundle.Control implementation which allows explicitly read the properties files as UTF-8.
     *
     * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
     */
    private static class UTF8Control extends Control {
        @Override
        public ResourceBundle newBundle(String aBaseName, Locale aLocale, String aFormat, ClassLoader aLoader,
                boolean aReload) throws IOException {
            // The below is a copy of the default implementation.
            final String bundleName = toBundleName(aBaseName, aLocale);
            final String resourceName = toResourceName(bundleName, "properties");
            ResourceBundle bundle = null;
            InputStream stream = null;
            if (aReload) {
                final URL url = aLoader.getResource(resourceName);
                if (url != null) {
                    final URLConnection connection = url.openConnection();
                    if (connection != null) {
                        connection.setUseCaches(false);
                        stream = connection.getInputStream();
                    }
                }
            } else {
                stream = aLoader.getResourceAsStream(resourceName);
            }
            if (stream != null) {
                Reader streamReader = null;
                try {
                    streamReader = new InputStreamReader(stream, "UTF-8");
                    // Only this line is changed to make it to read properties files as UTF-8.
                    bundle = new PropertyResourceBundle(streamReader);
                } finally {
                    streamReader.close();
                    stream.close();
                }
            }
            return bundle;
        }
    }

    /**
     * Refreshes the metadata.
     */
    public static synchronized void refresh() {
        sRuleGroupMetadata = new TreeMap<String, RuleGroupMetadata>();
        sRuleMetadata = new HashMap<String, RuleMetadata>();
        sAlternativeNamesMap = new HashMap<String, RuleMetadata>();
        try {
            doInitialization();
        } catch (CheckstylePluginException e) {
            CheckstyleLog.log(e);
        }
    }

    /**
     * Initializes the meta data from the xml file.
     *
     * @throws CheckstylePluginException
     *             error loading the meta data file
     */
    private static void doInitialization() throws CheckstylePluginException {

        ClassLoader classLoader = CheckstylePlugin.getDefault().getAddonExtensionClassLoader();
        Collection<String> potentialMetadataFiles = getAllPotentialMetadataFiles(classLoader);
        for (String metadataFile : potentialMetadataFiles) {

            InputStream metadataStream = null;

            try {
                metadataStream = classLoader.getResourceAsStream(metadataFile);
                if (metadataStream != null) {

                    ResourceBundle metadataBundle = getMetadataI18NBundle(metadataFile, classLoader);
                    parseMetadata(metadataStream, metadataBundle);
                }
            } catch (DocumentException e) {
                CheckstyleLog.log(e, "Could not read metadata " + metadataFile); //$NON-NLS-1$
            } finally {
                IOUtils.closeQuietly(metadataStream);
            }
        }
    }

    /**
     * Helper method to get all potential metadata files using the checkstyle_packages.xml as base where to look. It is
     * not guaranteed that the files returned acutally exist.
     *
     * @return the collection of potential metadata files.
     * @throws CheckstylePluginException
     *             an unexpected exception ocurred
     */
    private static Collection<String> getAllPotentialMetadataFiles(ClassLoader classLoader)
            throws CheckstylePluginException {

        Collection<String> potentialMetadataFiles = new ArrayList<String>();

        Set<String> packages = null;
        try {
            packages = PackageNamesLoader.getPackageNames(classLoader);
        } catch (CheckstyleException e) {
            CheckstylePluginException.rethrow(e);
        }

        for (String packageName : packages) {
            String metaFileLocation = packageName.replace('.', '/') + METADATA_FILENAME;
            potentialMetadataFiles.add(metaFileLocation);
        }

        return potentialMetadataFiles;
    }

    /**
     * Returns the ResourceBundle for the given meta data file contained i18n'ed names and descriptions.
     *
     * @param metadataFile
     * @return the corresponding ResourceBundle for the metadata file or <code>null</code> if none exists
     */
    private static ResourceBundle getMetadataI18NBundle(String metadataFile, ClassLoader classLoader) {
        String bundle = metadataFile.substring(0, metadataFile.length() - 4).replace('/', '.');
        try {
            return PropertyResourceBundle.getBundle(bundle, CheckstylePlugin.getPlatformLocale(), classLoader);
        } catch (MissingResourceException e) {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private static void parseMetadata(InputStream metadataStream, ResourceBundle metadataBundle)
            throws DocumentException, CheckstylePluginException {

        SAXReader reader = new SAXReader();
        reader.setEntityResolver(new XMLUtil.InternalDtdEntityResolver(PUBLIC2INTERNAL_DTD_MAP));
        Document document = reader.read(metadataStream);

        List<Element> groupElements = document.getRootElement().elements(XMLTags.RULE_GROUP_METADATA_TAG);

        for (Element groupEl : groupElements) {

            String groupName = groupEl.attributeValue(XMLTags.NAME_TAG).trim();
            groupName = localize(groupName, metadataBundle);

            RuleGroupMetadata group = getRuleGroupMetadata(groupName);

            if (group == null) {

                boolean hidden = Boolean.valueOf(groupEl.attributeValue(XMLTags.HIDDEN_TAG)).booleanValue();
                int priority = 0;
                try {
                    priority = Integer.parseInt(groupEl.attributeValue(XMLTags.PRIORITY_TAG));
                } catch (Exception e) {
                    CheckstyleLog.log(e);
                    priority = Integer.MAX_VALUE;
                }

                group = new RuleGroupMetadata(groupName, hidden, priority);
                sRuleGroupMetadata.put(groupName, group);
            }

            // process the modules
            processModules(groupEl, group, metadataBundle);
        }
    }

    @SuppressWarnings("unchecked")
    private static void processModules(Element groupElement, RuleGroupMetadata groupMetadata,
            ResourceBundle metadataBundle) throws CheckstylePluginException {

        List<Element> moduleElements = groupElement.elements(XMLTags.RULE_METADATA_TAG);
        for (Element moduleEl : moduleElements) {

            // default severity
            String defaultSeverity = moduleEl.attributeValue(XMLTags.DEFAULT_SEVERITY_TAG);
            Severity severity = defaultSeverity == null || defaultSeverity.trim().length() == 0
                    ? getDefaultSeverity()
                    : Severity.valueOf(defaultSeverity);

            String name = moduleEl.attributeValue(XMLTags.NAME_TAG).trim();
            name = localize(name, metadataBundle);
            String internalName = moduleEl.attributeValue(XMLTags.INTERNAL_NAME_TAG).trim();

            String parentName = moduleEl.attributeValue(XMLTags.PARENT_TAG) != null
                    ? moduleEl.attributeValue(XMLTags.PARENT_TAG).trim()
                    : null;
            boolean hidden = Boolean.valueOf(moduleEl.attributeValue(XMLTags.HIDDEN_TAG)).booleanValue();
            boolean hasSeverity = !"false".equals(moduleEl.attributeValue(XMLTags.HAS_SEVERITY_TAG));
            boolean deletable = !"false".equals(moduleEl.attributeValue(XMLTags.DELETABLE_TAG)); //$NON-NLS-1$
            boolean isSingleton = Boolean.valueOf(moduleEl.attributeValue(XMLTags.IS_SINGLETON_TAG)).booleanValue();

            // create rule metadata
            RuleMetadata module = new RuleMetadata(name, internalName, parentName, severity, hidden, hasSeverity,
                    deletable, isSingleton, groupMetadata);
            groupMetadata.getRuleMetadata().add(module);

            // register internal name
            sRuleMetadata.put(internalName, module);

            // process description
            String description = moduleEl.elementTextTrim(XMLTags.DESCRIPTION_TAG);
            description = localize(description, metadataBundle);
            module.setDescription(description);

            // process properties
            processProperties(moduleEl, module, metadataBundle);

            // process alternative names
            for (Element altNameEl : (List<Element>) moduleEl.elements(XMLTags.ALTERNATIVE_NAME_TAG)) {

                String alternativeName = altNameEl.attributeValue(XMLTags.INTERNAL_NAME_TAG);

                // register alternative name
                sAlternativeNamesMap.put(alternativeName, module);
                module.addAlternativeName(alternativeName);
            }

            // process quickfixes
            for (Element quickfixEl : (List<Element>) moduleEl.elements(XMLTags.QUCKFIX_TAG)) {

                String quickfixClassName = quickfixEl.attributeValue(XMLTags.CLASSNAME_TAG);
                module.addQuickfix(quickfixClassName);
            }

            // process message keys
            for (Element quickfixEl : (List<Element>) moduleEl.elements(XMLTags.MESSAGEKEY_TAG)) {

                String messageKey = quickfixEl.attributeValue(XMLTags.KEY_TAG);
                module.addMessageKey(messageKey);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private static void processProperties(Element moduleElement, RuleMetadata moduleMetadata,
            ResourceBundle metadataBundle) throws CheckstylePluginException {

        List<Element> propertyElements = moduleElement.elements(XMLTags.PROPERTY_METADATA_TAG);
        for (Element propertyEl : propertyElements) {

            ConfigPropertyType type = ConfigPropertyType.valueOf(propertyEl.attributeValue(XMLTags.DATATYPE_TAG));

            String name = propertyEl.attributeValue(XMLTags.NAME_TAG).trim();
            String defaultValue = StringUtils.trim(propertyEl.attributeValue(XMLTags.DEFAULT_VALUE_TAG));
            String overrideDefaultValue = StringUtils
                    .trim(propertyEl.attributeValue(XMLTags.DEFAULT_VALUE_OVERRIDE_TAG));

            ConfigPropertyMetadata property = new ConfigPropertyMetadata(type, name, defaultValue,
                    overrideDefaultValue);

            moduleMetadata.getPropertyMetadata().add(property);

            // get description
            String description = propertyEl.elementTextTrim(XMLTags.DESCRIPTION_TAG);
            description = localize(description, metadataBundle);
            property.setDescription(description);

            // get property enumeration values
            Element enumEl = propertyEl.element(XMLTags.ENUMERATION_TAG);
            if (enumEl != null) {
                String optionProvider = enumEl.attributeValue(XMLTags.OPTION_PROVIDER);
                if (optionProvider != null) {

                    try {
                        Class<?> providerClass = CheckstylePlugin.getDefault().getAddonExtensionClassLoader()
                                .loadClass(optionProvider);

                        if (IOptionProvider.class.isAssignableFrom(providerClass)) {

                            IOptionProvider provider = (IOptionProvider) providerClass.newInstance();
                            property.getPropertyEnumeration().addAll(provider.getOptions());
                        } else if (Enum.class.isAssignableFrom(providerClass)) {

                            EnumSet<?> values = EnumSet.allOf((Class<Enum>) providerClass);
                            for (Enum e : values) {
                                property.getPropertyEnumeration().add(e.name().toLowerCase());
                            }
                        }
                    } catch (ClassNotFoundException e) {
                        CheckstylePluginException.rethrow(e);
                    } catch (InstantiationException e) {
                        CheckstylePluginException.rethrow(e);
                    } catch (IllegalAccessException e) {
                        CheckstylePluginException.rethrow(e);
                    }
                }

                // get explicit enumeration option values
                for (Element optionEl : (List<Element>) enumEl.elements(XMLTags.PROPERTY_VALUE_OPTIONS_TAG)) {
                    property.getPropertyEnumeration().add(optionEl.attributeValue(XMLTags.VALUE_TAG));
                }
            }
        }
    }

    private static String localize(String localizationCandidate, ResourceBundle metadataBundle) {

        if (metadataBundle != null && localizationCandidate != null && localizationCandidate.startsWith("%")) {
            try {
                return metadataBundle.getString(localizationCandidate.substring(1));
            } catch (MissingResourceException e) {
                return localizationCandidate;
            }
        }
        return localizationCandidate;
    }
}