com.github.mikkoi.maven.plugins.enforcer.rule.charsetencoding.CharacterSetEncodingRule.java Source code

Java tutorial

Introduction

Here is the source code for com.github.mikkoi.maven.plugins.enforcer.rule.charsetencoding.CharacterSetEncodingRule.java

Source

package com.github.mikkoi.maven.plugins.enforcer.rule.charsetencoding;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import com.google.common.io.ByteStreams;
import org.apache.maven.enforcer.rule.api.EnforcerRule;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Checks file encodings to see if they match the parameter
 * or Maven property project.build.sourceEncoding.
 * If file requireEncoding can not be determined it is skipped.
 */
@SuppressWarnings("WeakerAccess")
public final class CharacterSetEncodingRule implements EnforcerRule {

    @Nonnull
    private static final String INCLUDE_REGEX_DEFAULT = ".*";
    @Nonnull
    private static final String EXCLUDE_REGEX_DEFAULT = "";
    @Nonnull
    private static final String DIRECTORY_DEFAULT = "src";

    /**
     * Faulty files list. Can be accessed after processing execute().
     */
    @Nonnull
    private final Collection<FileResult> faultyFiles = new ArrayList<>();
    /**
     * Validate files must match this requireEncoding.
     * Default: ${project.builder.sourceEncoding}.
     */
    @Nullable
    private String requireEncoding = null;
    /**
     * Directory to search for files.
     */
    @Nullable
    private String directory = null;
    /**
     * Regular Expression to match file names against for filtering in.
     */
    @Nullable
    private String includeRegex = null;
    /**
     * Regular Expression to match file names against for filtering out
     * Can be used together with includeRegex.
     * includeRegex will first pick files in,
     * then excludeRegex will filter out files from the selected ones.
     */
    @Nullable
    private String excludeRegex = null;

    /**
     * Get the faulty files list.
     */
    @Nonnull
    public Collection<FileResult> getFaultyFiles() {
        return faultyFiles;
    }

    /**
     * @param helper EnforcerRuleHelper
     * @throws EnforcerRuleException Throws when error
     */
    public void execute(@Nonnull final EnforcerRuleHelper helper) throws EnforcerRuleException {
        Log log = helper.getLog();

        try {
            // get the various expressions out of the helper.
            String basedir = helper.evaluate("${project.basedir}").toString();

            log.debug("Retrieved Basedir: " + basedir);
            log.debug("requireEncoding: " + (requireEncoding == null ? "null" : requireEncoding));
            log.debug("directory: " + (directory == null ? "null" : directory));
            log.debug("includeRegex: " + (includeRegex == null ? "null" : includeRegex));
            log.debug("excludeRegex: " + (excludeRegex == null ? "null" : excludeRegex));

            if (this.getRequireEncoding() == null || this.getRequireEncoding().trim().length() == 0) {
                final String sourceEncoding = (String) helper.evaluate("${project.build.sourceEncoding}");
                log.info(
                        "No parameter 'requiredEncoding' set. Defaults to property 'project.build.sourceEncoding'.");
                if (sourceEncoding != null && sourceEncoding.trim().length() > 0) {
                    this.setRequireEncoding(sourceEncoding);
                } else {
                    throw new EnforcerRuleException(
                            "Missing parameter 'requireEncoding' and property 'project.build.sourceEncoding'.");
                }
            }
            try {
                Charset.forName(this.getRequireEncoding()); //  Charset names are not case-sensitive
            } catch (IllegalCharsetNameException e) {
                throw new EnforcerRuleException("Illegal value (illegal character set name) '" + requireEncoding
                        + "' for parameter 'requireEncoding'.");
            } catch (UnsupportedCharsetException e) {
                throw new EnforcerRuleException("Illegal value (not supported character set) '" + requireEncoding
                        + "' for parameter 'requireEncoding'.");
            } catch (IllegalArgumentException e) {
                throw new EnforcerRuleException(
                        "Illegal value (empty) '" + requireEncoding + "' for parameter 'requireEncoding'.");
            }
            if (this.getDirectory() == null || this.getDirectory().trim().length() == 0) {
                log.info("No parameter 'directory' set. Defaults to '" + DIRECTORY_DEFAULT + "'.");
                this.setDirectory(DIRECTORY_DEFAULT);
            }
            if (this.getIncludeRegex() == null || this.getIncludeRegex().trim().length() == 0) {
                log.info("No parameter 'includeRegex' set. Defaults to '" + INCLUDE_REGEX_DEFAULT + "'.");
                this.setIncludeRegex(INCLUDE_REGEX_DEFAULT);
            }
            if (this.getExcludeRegex() == null || this.getExcludeRegex().trim().length() == 0) {
                log.info("No parameter 'excludeRegex' set. Defaults to '" + EXCLUDE_REGEX_DEFAULT + "'.");
                this.setExcludeRegex(EXCLUDE_REGEX_DEFAULT);
            }
            log.debug("requireEncoding: " + this.getRequireEncoding());
            log.debug("directory: " + this.getDirectory());
            log.debug("includeRegex: " + this.getIncludeRegex());
            log.debug("excludeRegex: " + this.getExcludeRegex());

            // Check the existence of the wanted directory:
            final Path dir = Paths.get(basedir, getDirectory());
            log.debug("Get files in dir '" + dir.toString() + "'.");
            if (!dir.toFile().exists()) {
                throw new EnforcerRuleException("Directory '" + dir.toString() + "' not found."
                        + " Specified by parameter 'directory' (value: '" + this.getDirectory() + "')!");
            }

            // Put all files into this collection:
            Collection<FileResult> allFiles = getFileResults(log, dir);

            // Copy faulty files to another list.
            log.debug("Moving possible faulty files (faulty encoding) to another list.");
            for (FileResult res : allFiles) {
                log.debug("Checking if file '" + res.getPath().toString() + "' has encoding '" + requireEncoding
                        + "'.");
                boolean hasCorrectEncoding = true;
                try (FileInputStream fileInputStream = new FileInputStream(res.getPath().toFile())) {
                    byte[] bytes = ByteStreams.toByteArray(fileInputStream);
                    Charset charset = Charset.forName(this.getRequireEncoding());
                    CharsetDecoder decoder = charset.newDecoder();
                    ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
                    decoder.decode(byteBuffer);
                } catch (CharacterCodingException e) {
                    hasCorrectEncoding = false;
                } catch (IOException e) {
                    e.printStackTrace();
                    log.error(e.getMessage());
                    hasCorrectEncoding = false;
                }
                if (!hasCorrectEncoding) {
                    log.debug("Moving faulty file: " + res.getPath());
                    FileResult faultyFile = new FileResult.Builder(res.getPath())
                            .lastModified(res.getLastModified()).build();
                    faultyFiles.add(faultyFile);
                } else {
                    log.debug("Has correct encoding. Not moving to faulty files list.");
                }
            }
            log.debug("All faulty files moved.");

            // Report
            if (!faultyFiles.isEmpty()) {
                final StringBuilder builder = new StringBuilder();
                builder.append("Wrong encoding in following files:");
                builder.append(System.getProperty("line.separator"));
                for (FileResult res : faultyFiles) {
                    builder.append(res.getPath());
                    builder.append(System.getProperty("line.separator"));
                }
                throw new EnforcerRuleException(builder.toString());
            }
        } catch (ExpressionEvaluationException e) {
            throw new EnforcerRuleException("Unable to lookup an expression " + e.getLocalizedMessage(), e);
        }
    }

    @Nonnull
    private Collection<FileResult> getFileResults(final Log log, final Path dir) {
        Collection<FileResult> allFiles = new ArrayList<>();
        FileVisitor<Path> fileVisitor = new GetEncodingsFileVisitor(log,
                this.getIncludeRegex() != null ? this.getIncludeRegex() : INCLUDE_REGEX_DEFAULT,
                this.getExcludeRegex() != null ? this.getExcludeRegex() : EXCLUDE_REGEX_DEFAULT, allFiles);
        try {
            Set<FileVisitOption> visitOptions = new LinkedHashSet<>();
            visitOptions.add(FileVisitOption.FOLLOW_LINKS);
            Files.walkFileTree(dir, visitOptions, Integer.MAX_VALUE, fileVisitor);
        } catch (Exception e) {
            log.error(e.getCause() + e.getMessage());
        }
        return allFiles;
    }

    /**
     * If your rule is cacheable, you must return a unique id when parameters or conditions
     * change that would cause the result to be different. Multiple cached results are stored
     * based on their id.
     * <p>
     * The easiest way to do this is to return a hash computed from the values of your parameters.
     * <p>
     * If your rule is not cacheable, then the result here is not important, you may return anything.
     *
     * @return Always false here.
     */
    @Nullable
    public String getCacheId() {
        return String.valueOf(false);
    }

    /**
     * This tells the system if the results are cacheable at all. Keep in mind that during
     * forked builds and other things, a given rule may be executed more than once for the same
     * project. This means that even things that change from project to project may still
     * be cacheable in certain instances.
     *
     * @return Always false here.
     */
    public boolean isCacheable() {
        return false;
    }

    /**
     * If the rule is cacheable and the same id is found in the cache, the stored results
     * are passed to this method to allow double checking of the results. Most of the time
     * this can be done by generating unique ids, but sometimes the results of objects returned
     * by the helper need to be queried. You may for example, store certain objects in your rule
     * and then query them later.
     *
     * @param arg0 EnforcerRule
     * @return Always false here.
     */
    public boolean isResultValid(@Nullable final EnforcerRule arg0) {
        return false;
    }

    /**
     * Getters and setters for the parameters (these are filled by Maven).
     */

    @SuppressWarnings("WeakerAccess")
    @Nullable
    public String getDirectory() {
        return directory;
    }

    @SuppressWarnings("WeakerAccess")
    public void setDirectory(@Nullable final String directory) {
        this.directory = directory;
    }

    @SuppressWarnings("WeakerAccess")
    @Nullable
    public String getIncludeRegex() {
        return includeRegex;
    }

    @SuppressWarnings("WeakerAccess")
    public void setIncludeRegex(@Nullable final String includeRegex) {
        this.includeRegex = includeRegex;
    }

    @SuppressWarnings("WeakerAccess")
    @Nullable
    public String getExcludeRegex() {
        return excludeRegex;
    }

    @SuppressWarnings("WeakerAccess")
    public void setExcludeRegex(@Nullable final String excludeRegex) {
        this.excludeRegex = excludeRegex;
    }

    @SuppressWarnings("WeakerAccess")
    @Nullable
    public String getRequireEncoding() {
        return requireEncoding;
    }

    @SuppressWarnings("WeakerAccess")
    public void setRequireEncoding(@Nullable final String requireEncoding) {
        this.requireEncoding = requireEncoding;
    }

    /**
     * Extended SimpleFileVisitor for walking through the files.
     */
    private static class GetEncodingsFileVisitor extends SimpleFileVisitor<Path> {
        @Nonnull
        private final Log log;
        private final boolean includeRegexUsed;
        @Nonnull
        private final Pattern includeRegexPattern;
        private final boolean excludeRegexUsed;
        @Nonnull
        private final Pattern excludeRegexPattern;
        @Nonnull
        private final Collection<FileResult> results;

        /**
         * Constructor.
         *
         * @param pluginLog    Maven Plugin logging channel.
         * @param includeRegex Include regex pattern.
         * @param excludeRegex Exclude regex pattern.
         * @param fileResults  Initialized collection to be filled.
         */
        GetEncodingsFileVisitor(@Nonnull final Log pluginLog, @Nonnull final String includeRegex,
                @Nonnull final String excludeRegex, @Nonnull final Collection<FileResult> fileResults) {
            this.log = pluginLog;
            // Attn. Because we have includeRegex default (.*) which replaces
            // an empty includeRegex, includeRegex can never have length 0 chars!
            // But excludeRegex can have length 0 chars!
            includeRegexUsed = true;
            includeRegexPattern = Pattern.compile(includeRegex);
            if (excludeRegex.length() > 0) {
                excludeRegexUsed = true;
                excludeRegexPattern = Pattern.compile(excludeRegex);
            } else {
                excludeRegexUsed = false;
                excludeRegexPattern = Pattern.compile("");
            }
            this.results = fileResults;
        }

        @Override
        public FileVisitResult visitFile(final Path aFile, final BasicFileAttributes aAttrs) throws IOException {
            log.debug("Visiting file '" + aFile.toString() + "'.");
            if (includeRegexUsed && !includeRegexPattern.matcher(aFile.toString()).find()) {
                log.debug("File not matches includeRegex in-filter. Exclude file from list!");
                return FileVisitResult.CONTINUE;
            }
            if (excludeRegexUsed && excludeRegexPattern.matcher(aFile.toString()).find()) {
                log.debug("File matches excludeRegex out-filter. Exclude file from list!");
                return FileVisitResult.CONTINUE;
            }
            log.debug(
                    "File matches includeRegex in-filter and not matches excludeRegex out-filter. Include file to list!");
            File file = aFile.toFile();
            FileResult res = new FileResult.Builder(aFile.toAbsolutePath()).lastModified(file.lastModified())
                    .build();
            results.add(res);
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult preVisitDirectory(final Path aDir, final BasicFileAttributes aAttrs)
                throws IOException {
            log.debug("Visiting directory '" + aDir.toString() + "'.");
            return FileVisitResult.CONTINUE;
        }

    }

}