com.thomasjensen.checkstyle.addons.checks.misc.ModuleDirectoryLayoutCheck.java Source code

Java tutorial

Introduction

Here is the source code for com.thomasjensen.checkstyle.addons.checks.misc.ModuleDirectoryLayoutCheck.java

Source

package com.thomasjensen.checkstyle.addons.checks.misc;
/*
 * Checkstyle-Addons - Additional Checkstyle checks
 * Copyright (C) 2015 Thomas Jensen
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License, version 3, as published by the Free
 * Software Foundation.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this
 * program.  If not, see <http://www.gnu.org/licenses/>.
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.thomasjensen.checkstyle.addons.util.Util;

/**
 * This check helps ensure that the source folder structure in a module follows a configurable convention. <p><a
 * href="http://checkstyle-addons.thomasjensen.com/latest/checks/misc.html#ModuleDirectoryLayout"
 * target="_blank">Documentation</a></p>
 *
 * @author Thomas Jensen
 */
public class ModuleDirectoryLayoutCheck extends AbstractFileSetCheck {
    private static final String DEFAULT_CONFIG_FILENAME = "ModuleDirectoryLayout-default.json";

    /** the base directory to be assumed for this check, usually the project's root directory */
    private File baseDir = Util.canonize(new File("."));

    /** the parsed contents of the UTF-8 encoded configuration file in JSON */
    private MdlJsonConfig mdlConfig;

    /** matches the file extension in a pathname in its capturing group */
    private static final Pattern FILE_EXTENSION = Pattern.compile("\\.([^\\\\/]+)$");

    private static final Pattern SINGLE_MODULE_PROJECT = Pattern.compile("");

    private Pattern moduleRegexp = SINGLE_MODULE_PROJECT;

    /** SpecLists can be allow or deny lists. */
    private enum SpecListType {
        Allow(true),

        Deny(false);

        //

        private final boolean caseSensitive;

        private SpecListType(final boolean pCaseSensitive) {
            caseSensitive = pCaseSensitive;
        }

        public boolean isCaseSensitive() {
            return caseSensitive;
        }
    }

    /**
     * Constructor. Activates the default config file, which may be overridden by the <code>configFile</code> check
     * property.
     */
    public ModuleDirectoryLayoutCheck() {
        super();
        InputStream is = null;
        try {
            is = ModuleDirectoryLayoutCheck.class.getResourceAsStream(DEFAULT_CONFIG_FILENAME);
            activateConfigFile(is, DEFAULT_CONFIG_FILENAME);
        } finally {
            Util.closeQuietly(is);
        }
    }

    @Override
    protected void processFiltered(final File pFile, final List<String> pLines) {
        if (mdlConfig != null) {
            final String filePath = Util.canonize(pFile).getPath();
            final DecomposedPath decomposedPath = decomposePath(filePath);
            if (decomposedPath != null) {
                MdlJsonConfig.MdlSpec mdlSpec = mdlConfig.getStructure().get(decomposedPath.getMdlPath());

                if (!mdlConfig.getSettings().isAllowNestedSrcFolder()
                        && decomposedPath.getSpecificFolders().contains("src")) {
                    log(0, "moduledirectorylayout.nestedsrcfolder", decomposedPath.getSpecificPath());
                }

                else if (!isMdlAllowedForModule(mdlSpec, decomposedPath)) {
                    log(0, "moduledirectorylayout.notinthismodule", decomposedPath.getMdlPath(),
                            decomposedPath.getModulePath());
                }

                else if (!isSpecificPathAllowedInMdl(mdlSpec, decomposedPath)
                        || !isAllowListPostProcessingOk(mdlSpec.getAllow(), decomposedPath)) {
                    log(0, "moduledirectorylayout.illegalcontent", decomposedPath.getMdlPath(),
                            decomposedPath.getSpecificPath());
                }
            }
        }
    }

    private boolean isAllowListPostProcessingOk(@Nullable final List<MdlJsonConfig.SpecElement> pAllowList,
            @Nonnull final DecomposedPath pDecomposedPath) {
        boolean ok = true;
        if (pAllowList != null && pAllowList.size() > 0) {
            for (final MdlJsonConfig.SpecElement se : pAllowList) {
                if (se.getType() == MdlContentSpecType.TopLevelFolder) {
                    ok = isTopLevelFolderNestingOk(se.getSpec(), pDecomposedPath.getSpecificFolders());
                    if (!ok) {
                        break;
                    }
                }
            }
        }
        return ok;
    }

    private boolean checkTopLevelFolder(@Nonnull final String pTopLevelFolder,
            @Nonnull final List<String> pSpecificFolders, final boolean pCaseSensitive) {
        boolean ok = true;
        if (pSpecificFolders.size() > 0) {
            ok = Util.stringEquals(pSpecificFolders.get(0), pTopLevelFolder, pCaseSensitive)
                    && isTopLevelFolderNestingOk(pTopLevelFolder, pSpecificFolders);
        }

        return ok;
    }

    private boolean isTopLevelFolderNestingOk(@Nonnull final String pTopLevelFolder,
            @Nonnull final List<String> pSpecificFolders) {
        boolean ok = true;
        if (pSpecificFolders.size() > 1) {
            ok = !Util.containsString(pSpecificFolders.subList(1, pSpecificFolders.size()), pTopLevelFolder, false); // never case sensitive
        }
        return ok;
    }

    private boolean isSpecificPathAllowedInMdl(@Nonnull final MdlJsonConfig.MdlSpec pMdlSpec,
            @Nonnull final DecomposedPath pDecomposedPath) {
        boolean allowed = true;
        boolean denied = false;
        if (pMdlSpec.isWhitelist() && pMdlSpec.getAllow() != null && !pMdlSpec.getAllow().isEmpty()) {
            allowed = processSpecList(pMdlSpec.getAllow(), pDecomposedPath, SpecListType.Allow);
        }
        if (pMdlSpec.getDeny() != null && !pMdlSpec.getDeny().isEmpty()) {
            denied = processSpecList(pMdlSpec.getDeny(), pDecomposedPath, SpecListType.Deny);
        }
        return allowed && !denied;
    }

    private boolean isMdlAllowedForModule(@Nonnull final MdlJsonConfig.MdlSpec pMdlSpec,
            @Nonnull final DecomposedPath pDecomposedPath) {
        return pDecomposedPath.getModulePath().isEmpty() || pMdlSpec.getModules() == null
                || pMdlSpec.getModules().matcher(pDecomposedPath.getModulePath()).find();
    }

    private boolean processSpecList(@Nullable final List<MdlJsonConfig.SpecElement> pSpecList,
            @Nonnull final DecomposedPath pDecomposedPath, @Nonnull final SpecListType pListType) {
        boolean match = false;
        if (pSpecList != null) {
            for (final MdlJsonConfig.SpecElement se : pSpecList) {
                switch (se.getType()) {

                case FileExtensions:
                    for (final String allowedExtension : se.getSpec().split("\\s*,\\s*")) {
                        if (Util.containsString(pDecomposedPath.getFileExtensions(), allowedExtension,
                                pListType.isCaseSensitive())) {
                            match = true;
                            break;
                        }
                    }
                    break;

                case TopLevelFolder:
                    if (pListType == SpecListType.Allow) {
                        match = checkTopLevelFolder(se.getSpec(), pDecomposedPath.getSpecificFolders(),
                                pListType.isCaseSensitive());
                        break;
                    }
                    // fall through

                case SimpleFolder:
                    match = Util.containsString(pDecomposedPath.getSpecificFolders(), se.getSpec(),
                            pListType.isCaseSensitive());
                    break;

                case SimpleName:
                    match = Util.stringEquals(pDecomposedPath.getSimpleFilename(), se.getSpec(),
                            pListType.isCaseSensitive());
                    break;

                case SpecificPathRegex:
                    match = Pattern.compile(se.getSpec()).matcher(pDecomposedPath.getSpecificPath()).find();
                    break;

                case FromPath:
                    // only possible in deny lists
                    match = processSpecList(mdlConfig.getStructure().get(se.getSpec()).getAllow(), pDecomposedPath,
                            pListType);
                    break;

                default:
                    throw new IllegalStateException("Unexpected enum constant: " + se.getType());
                }
                if (match) {
                    break;
                }
            }
        }
        return match;
    }

    @CheckForNull
    DecomposedPath decomposePath(@Nonnull final String pFilePath) {
        String modulePath = "";
        String mdlPath = null;
        String specificPath = null;
        String simpleFilename = null;
        Set<String> fileExtensions = new HashSet<String>();
        List<String> specificFolders = new ArrayList<String>();

        if (!pFilePath.startsWith(baseDir.getPath())) {
            return null;
        }
        String filePath = cutSlashes(pFilePath.substring(baseDir.getPath().length()));

        if (moduleRegexp.pattern().length() > 0) {
            final Matcher matcher = moduleRegexp.matcher(filePath);
            if (matcher.find() && matcher.start() == 0) {
                modulePath = cutSlashes(matcher.group(0));
            } else {
                log(0, "moduledirectorylayout.invalid.module", filePath, moduleRegexp.pattern());
                return null;
            }
        }
        if (modulePath.length() > 0) {
            filePath = cutSlashes(filePath.substring(modulePath.length()));
        }

        for (final String mdlPathCandidate : mdlConfig.getStructure().keySet()) {
            if (filePath.startsWith(Util.standardizeSlashes(mdlPathCandidate))) {
                mdlPath = mdlPathCandidate;
                filePath = cutSlashes(filePath.substring(mdlPathCandidate.length()));
            }
        }
        if (mdlPath == null) {
            if (filePath.indexOf(File.separatorChar) > 0) { // no error if file is in module root
                log(0, "moduledirectorylayout.invalid.mdlpath", filePath);
            }
            return null;
        }

        specificPath = filePath;

        Matcher matcher = FILE_EXTENSION.matcher(filePath);
        if (matcher.find()) {
            String ext = matcher.group(1);
            for (int d = ext.lastIndexOf('.'); d > 0; d = ext.lastIndexOf('.', d - 1)) {
                fileExtensions.add(ext.substring(d + 1));
            }
            fileExtensions.add(ext);
        }

        int lastSlash = filePath.lastIndexOf(File.separatorChar);
        simpleFilename = filePath.substring(lastSlash + 1);

        String[] fragments = filePath.split("[\\\\/]");
        specificFolders.addAll(Arrays.asList(fragments).subList(0, fragments.length - 1));

        DecomposedPath result = new DecomposedPath(modulePath, mdlPath, specificPath, simpleFilename,
                fileExtensions, specificFolders);
        return result;
    }

    @Nonnull
    String cutSlashes(@Nonnull final String pPath) {
        String result = pPath;
        if (pPath.length() > 0) {
            boolean leadingSlash = false;
            if (pPath.charAt(0) == '\\' || pPath.charAt(0) == '/') {
                leadingSlash = true;
            }
            boolean trailingSlash = false;
            if (pPath.length() > 1
                    && (pPath.charAt(pPath.length() - 1) == '\\' || pPath.charAt(pPath.length() - 1) == '/')) {
                trailingSlash = true;
            }
            if (leadingSlash || trailingSlash) {
                result = pPath.substring(leadingSlash ? 1 : 0, pPath.length() - (trailingSlash ? 1 : 0));
            }
        }
        return result;
    }

    public void setBaseDir(final String pBaseDir) {
        baseDir = Util.canonize(new File(pBaseDir));
    }

    /**
     * Setter.
     *
     * @param pConfigFile the location of the JSON configuration file
     */
    public final void setConfigFile(@Nonnull final String pConfigFile) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(Util.canonize(new File(pConfigFile)));
            activateConfigFile(fis, pConfigFile);
        } catch (IllegalArgumentException e) {
            activateConfigFile(null, null);
            throw e;
        } catch (FileNotFoundException e) {
            activateConfigFile(null, null);
            throw new IllegalArgumentException(
                    "Config file not found for " + getClass().getSimpleName() + ": " + pConfigFile, e);
        } finally {
            Util.closeQuietly(fis);
        }
    }

    private void activateConfigFile(@Nullable final InputStream pInputStream, @Nullable final String pFilename) {
        if (pInputStream != null) {
            try {
                mdlConfig = readConfigFile(pInputStream);
            } catch (IOException e) {
                throw new IllegalArgumentException(
                        "Could not read or parse the module directory layout configFile: " + pFilename, e);
            }

            try {
                mdlConfig.validate();
                moduleRegexp = Pattern.compile(mdlConfig.getSettings().getModuleRegex());
            } catch (ConfigValidationException e) {
                mdlConfig = null;
                moduleRegexp = SINGLE_MODULE_PROJECT;
                throw new IllegalArgumentException(
                        "Module directory layout configFile contains invalid configuration: " + pFilename, e);
            }
        } else {
            mdlConfig = null;
            moduleRegexp = SINGLE_MODULE_PROJECT;
        }
    }

    static MdlJsonConfig readConfigFile(@Nonnull final InputStream pInputStream) throws IOException {
        final byte[] fileContents = Util.readBytes(pInputStream);
        final String json = new String(fileContents, Util.UTF8);
        final MdlJsonConfig result = new ObjectMapper().readValue(json, MdlJsonConfig.class);
        return result;
    }
}