
com.thomasjensen.checkstyle.addons.checks.misc.ModuleDirectoryLayoutCheck Maven / Gradle / Ivy
The newest version!
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 .
*/
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.CallableNoEx;
import com.thomasjensen.checkstyle.addons.util.Util;
/**
* This check helps ensure that the source folder structure in a module follows a configurable convention.
*/
public class ModuleDirectoryLayoutCheck
extends AbstractFileSetCheck
{
private static final String DEFAULT_CONFIG_FILENAME = "ModuleDirectoryLayout-default.json";
/** 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("");
/** the base directory to be assumed for this check, usually the project's root directory */
private File baseDir = Util.canonize(new File("."));
private boolean failQuietly = false;
private CallableNoEx mdlConfigCallable;
private MdlConfig mdlConfigCache = null;
/** 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 configFile
check
* property.
*/
public ModuleDirectoryLayoutCheck()
{
super();
mdlConfigCallable = new CallableNoEx()
{
@Override
@Nonnull
public MdlConfig call()
{
MdlConfig result = null;
InputStream is = null;
try {
is = ModuleDirectoryLayoutCheck.class.getResourceAsStream(DEFAULT_CONFIG_FILENAME);
result = activateConfigFile(is, DEFAULT_CONFIG_FILENAME);
}
finally {
Util.closeQuietly(is);
}
return result;
}
};
}
@Override
public void beginProcessing(final String pCharset)
{
super.beginProcessing(pCharset);
mdlConfigCache = null;
}
@Override
protected void processFiltered(final File pFile, final List pLines)
{
final MdlConfig wrapper = getMdlConfig();
final MdlJsonConfig mdlConfig = wrapper.getJson();
if (mdlConfig != null) {
final String filePath = Util.canonize(pFile).getPath();
final DecomposedPath decomposedPath = decomposePath(wrapper, 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(mdlConfig, mdlSpec, decomposedPath)
|| !isAllowListPostProcessingOk(mdlSpec.getAllow(), decomposedPath)) {
log(0, "moduledirectorylayout.illegalcontent", decomposedPath.getMdlPath(),
decomposedPath.getSpecificPath());
}
}
}
}
private boolean isAllowListPostProcessingOk(@Nullable final List 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 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 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 pJsonConfig,
@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(pJsonConfig, pMdlSpec.getAllow(), pDecomposedPath, SpecListType.Allow);
}
if (pMdlSpec.getDeny() != null && !pMdlSpec.getDeny().isEmpty()) {
denied = processSpecList(pJsonConfig, 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(@Nonnull final MdlJsonConfig pJsonConfig,
@Nullable final List 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(pJsonConfig, pJsonConfig.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 MdlConfig pMdlConfig, @Nonnull final String pFilePath)
{
final MdlJsonConfig mdlConfig = pMdlConfig.getJson();
final Pattern moduleRegexp = pMdlConfig.getModuleRegex();
String modulePath = "";
String mdlPath = null;
String specificPath = null;
String simpleFilename = null;
Set fileExtensions = new HashSet();
List specificFolders = new ArrayList();
if (!pFilePath.startsWith(baseDir.getPath())) {
return null;
}
String filePath = cutSlashes(pFilePath.substring(baseDir.getPath().length()));
if (pMdlConfig.getExcludeRegex().matcher(filePath).find()) {
return null; // the file path is excluded from checking
}
if (moduleRegexp.pattern().length() > 0) {
final Matcher matcher = moduleRegexp.matcher(filePath);
if (matcher.find() && matcher.start() == 0) {
modulePath = cutSlashes(matcher.group(0));
}
else if (filePath.contains("\\") || filePath.contains("/")) { // no error if file is in baseDir
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 (mdlPathCandidate.length() < filePath.length() //
&& filePath.startsWith(Util.standardizeSlashes(mdlPathCandidate)) //
&& "/\\".indexOf(filePath.charAt(mdlPathCandidate.length())) >= 0) //
{
mdlPath = mdlPathCandidate;
filePath = cutSlashes(filePath.substring(mdlPathCandidate.length()));
break;
}
}
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)
{
mdlConfigCallable = new CallableNoEx()
{
@Override
@Nonnull
public MdlConfig call()
{
MdlConfig result = null;
FileInputStream fis = null;
try {
fis = new FileInputStream(Util.canonize(new File(pConfigFile)));
result = activateConfigFile(fis, pConfigFile);
}
catch (IllegalArgumentException e) {
result = activateConfigFile(null, null);
throw e;
}
catch (FileNotFoundException e) {
result = activateConfigFile(null, null);
if (!failQuietly) {
throw new IllegalArgumentException(
"Config file not found for " + ModuleDirectoryLayoutCheck.class.getSimpleName() + ": "
+ pConfigFile, e);
}
}
finally {
Util.closeQuietly(fis);
}
return result;
}
};
}
public void setFailQuietly(final boolean pFailQuietly)
{
failQuietly = pFailQuietly;
}
@Nonnull
private MdlConfig activateConfigFile(@Nullable final InputStream pInputStream, @Nullable final String pFilename)
{
MdlConfig result = new MdlConfig(null, SINGLE_MODULE_PROJECT, Util.NEVER_MATCH);
if (pInputStream != null) {
MdlJsonConfig json = null;
Pattern moduleRegexp = null;
Pattern excludeRegexp = null;
try {
json = readConfigFile(pInputStream);
}
catch (IOException e) {
throw new IllegalArgumentException(
"Could not read or parse the module directory layout configFile: " + pFilename, e);
}
try {
json.validate();
moduleRegexp = Pattern.compile(json.getSettings().getModuleRegex());
excludeRegexp = Pattern.compile(json.getSettings().getExcludeRegex());
}
catch (ConfigValidationException e) {
json = null;
moduleRegexp = SINGLE_MODULE_PROJECT;
excludeRegexp = Util.NEVER_MATCH;
throw new IllegalArgumentException(
"Module directory layout configFile contains invalid configuration: " + pFilename, e);
}
result = new MdlConfig(json, moduleRegexp, excludeRegexp);
}
return result;
}
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;
}
@Nonnull
MdlConfig getMdlConfig()
{
if (mdlConfigCache == null) {
mdlConfigCache = mdlConfigCallable.call();
}
return mdlConfigCache;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy