org.mapstruct.ap.testutil.runner.CompilingStatement.java Source code

Java tutorial

Introduction

Here is the source code for org.mapstruct.ap.testutil.runner.CompilingStatement.java

Source

/**
 *  Copyright 2012-2016 Gunnar Morling (http://www.gunnarmorling.de/)
 *  and/or other contributors as indicated by the @authors tag. See the
 *  copyright.txt file in the distribution for a full listing of all
 *  contributors.
 *
 *  Licensed 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.
 */
package org.mapstruct.ap.testutil.runner;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.WithServiceImplementation;
import org.mapstruct.ap.testutil.WithServiceImplementations;
import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption;
import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOptions;
import org.mapstruct.ap.testutil.compilation.model.CompilationOutcomeDescriptor;
import org.mapstruct.ap.testutil.compilation.model.DiagnosticDescriptor;
import org.xml.sax.InputSource;

import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
import com.puppycrawl.tools.checkstyle.DefaultLogger;
import com.puppycrawl.tools.checkstyle.PropertiesExpander;

/**
 * A JUnit4 statement that performs source generation using the annotation processor and compiles those sources.
 *
 * @author Andreas Gudian
 */
abstract class CompilingStatement extends Statement {

    private static final String TARGET_COMPILATION_TESTS = "/target/compilation-tests/";

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    private static final DiagnosticDescriptorComparator COMPARATOR = new DiagnosticDescriptorComparator();

    protected static final String SOURCE_DIR = getBasePath() + "/src/test/java";

    protected static final List<String> TEST_COMPILATION_CLASSPATH = buildTestCompilationClasspath();

    protected static final List<String> PROCESSOR_CLASSPATH = buildProcessorClasspath();

    private final FrameworkMethod method;
    private final CompilationCache compilationCache;
    private Statement next;

    private String classOutputDir;
    private String sourceOutputDir;
    private String additionalCompilerClasspath;
    private CompilationRequest compilationRequest;

    CompilingStatement(FrameworkMethod method, CompilationCache compilationCache) {
        this.method = method;
        this.compilationCache = compilationCache;

        this.compilationRequest = new CompilationRequest(getTestClasses(), getServices(), getProcessorOptions());
    }

    void setNextStatement(Statement next) {
        this.next = next;
    }

    @Override
    public void evaluate() throws Throwable {
        generateMapperImplementation();

        GeneratedSource.setCompilingStatement(this);
        next.evaluate();
        GeneratedSource.clearCompilingStatement();
    }

    String getSourceOutputDir() {
        return compilationCache.getLastSourceOutputDir();
    }

    protected void setupDirectories() throws Exception {
        String compilationRoot = getBasePath() + TARGET_COMPILATION_TESTS + method.getDeclaringClass().getName()
                + "/" + method.getName() + getPathSuffix();

        classOutputDir = compilationRoot + "/classes";
        sourceOutputDir = compilationRoot + "/generated-sources";
        additionalCompilerClasspath = compilationRoot + "/compiler";

        createOutputDirs();

        ((ModifiableURLClassLoader) Thread.currentThread().getContextClassLoader()).withPath(classOutputDir);
    }

    protected abstract String getPathSuffix();

    private static List<String> buildTestCompilationClasspath() {
        String[] whitelist = new String[] {
                // MapStruct annotations in multi-module reactor build or IDE
                "core" + File.separator + "target",
                // MapStruct annotations in single module build
                "org" + File.separator + "mapstruct" + File.separator + "mapstruct" + File.separator, "guava",
                "javax.inject", "spring-beans", "spring-context", "joda-time" };

        return filterBootClassPath(whitelist);
    }

    private static List<String> buildProcessorClasspath() {
        String[] whitelist = new String[] { "processor" + File.separator + "target", // the processor itself,
                "freemarker", "javax.inject", "spring-context", "joda-time" };

        return filterBootClassPath(whitelist);
    }

    protected static List<String> filterBootClassPath(String[] whitelist) {
        String[] bootClasspath = System.getProperty("java.class.path").split(File.pathSeparator);
        String testClasses = "target" + File.separator + "test-classes";

        List<String> classpath = new ArrayList<String>();
        for (String path : bootClasspath) {
            if (!path.contains(testClasses) && isWhitelisted(path, whitelist)) {
                classpath.add(path);
            }
        }

        return classpath;
    }

    private static boolean isWhitelisted(String path, String[] whitelist) {
        for (String whitelisted : whitelist) {
            if (path.contains(whitelisted)) {
                return true;
            }
        }
        return false;
    }

    protected void generateMapperImplementation() throws Exception {
        CompilationOutcomeDescriptor actualResult = compile();

        CompilationOutcomeDescriptor expectedResult = CompilationOutcomeDescriptor
                .forExpectedCompilationResult(method.getAnnotation(ExpectedCompilationOutcome.class));

        if (expectedResult.getCompilationResult() == CompilationResult.SUCCEEDED) {
            assertThat(actualResult.getCompilationResult())
                    .describedAs("Compilation failed. Diagnostics: " + actualResult.getDiagnostics())
                    .isEqualTo(CompilationResult.SUCCEEDED);
        } else {
            assertThat(actualResult.getCompilationResult())
                    .describedAs("Compilation succeeded but should have failed.")
                    .isEqualTo(CompilationResult.FAILED);
        }

        assertDiagnostics(actualResult.getDiagnostics(), expectedResult.getDiagnostics());

        assertCheckstyleRules();
    }

    private void assertCheckstyleRules() throws Exception {
        if (sourceOutputDir != null) {
            Properties properties = new Properties();
            properties.put("checkstyle.cache.file", classOutputDir + "/checkstyle.cache");

            final Checker checker = new Checker();
            checker.setModuleClassLoader(Checker.class.getClassLoader());
            checker.configure(ConfigurationLoader.loadConfiguration(
                    new InputSource(getClass().getClassLoader()
                            .getResourceAsStream("checkstyle-for-generated-sources.xml")),
                    new PropertiesExpander(properties), true));

            ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
            checker.addListener(new DefaultLogger(ByteStreams.nullOutputStream(), true, errorStream, true));

            int errors = checker.process(findGeneratedFiles(new File(sourceOutputDir)));
            if (errors > 0) {
                String errorLog = errorStream.toString("UTF-8");
                assertThat(true).describedAs("Expected checkstyle compliant output, but got errors:\n" + errorLog)
                        .isEqualTo(false);
            }
        }
    }

    private static List<File> findGeneratedFiles(File file) {
        final List<File> files = Lists.newLinkedList();

        if (file.canRead()) {
            if (file.isDirectory()) {
                for (File element : file.listFiles()) {
                    files.addAll(findGeneratedFiles(element));
                }
            } else if (file.isFile()) {
                files.add(file);
            }
        }
        return files;
    }

    private void assertDiagnostics(List<DiagnosticDescriptor> actualDiagnostics,
            List<DiagnosticDescriptor> expectedDiagnostics) {

        Collections.sort(actualDiagnostics, COMPARATOR);
        Collections.sort(expectedDiagnostics, COMPARATOR);
        expectedDiagnostics = filterExpectedDiagnostics(expectedDiagnostics);

        Iterator<DiagnosticDescriptor> actualIterator = actualDiagnostics.iterator();
        Iterator<DiagnosticDescriptor> expectedIterator = expectedDiagnostics.iterator();

        assertThat(actualDiagnostics)
                .describedAs(String.format(
                        "Numbers of expected and actual diagnostics are diffent. Actual:%s%s%sExpected:%s%s.",
                        LINE_SEPARATOR, actualDiagnostics.toString().replace(", ", LINE_SEPARATOR), LINE_SEPARATOR,
                        LINE_SEPARATOR, expectedDiagnostics.toString().replace(", ", LINE_SEPARATOR)))
                .hasSize(expectedDiagnostics.size());

        while (actualIterator.hasNext()) {

            DiagnosticDescriptor actual = actualIterator.next();
            DiagnosticDescriptor expected = expectedIterator.next();

            if (expected.getSourceFileName() != null) {
                assertThat(actual.getSourceFileName()).isEqualTo(expected.getSourceFileName());
            }
            if (expected.getLine() != null) {
                assertThat(actual.getLine()).isEqualTo(expected.getLine());
            }
            assertThat(actual.getKind()).isEqualTo(expected.getKind());
            assertThat(actual.getMessage())
                    .describedAs(String.format("Unexpected message for diagnostic %s:%s %s",
                            actual.getSourceFileName(), actual.getLine(), actual.getKind()))
                    .matches("(?ms).*" + expected.getMessage() + ".*");
        }
    }

    /**
     * @param expectedDiagnostics expected diagnostics
     * @return a possibly filtered list of expected diagnostics
     */
    protected List<DiagnosticDescriptor> filterExpectedDiagnostics(List<DiagnosticDescriptor> expectedDiagnostics) {
        return expectedDiagnostics;
    }

    /**
     * Returns the classes to be compiled for this test.
     *
     * @return A set containing the classes to be compiled for this test
     */
    private Set<Class<?>> getTestClasses() {
        Set<Class<?>> testClasses = new HashSet<Class<?>>();

        WithClasses withClasses = method.getAnnotation(WithClasses.class);
        if (withClasses != null) {
            testClasses.addAll(Arrays.asList(withClasses.value()));
        }

        withClasses = method.getMethod().getDeclaringClass().getAnnotation(WithClasses.class);
        if (withClasses != null) {
            testClasses.addAll(Arrays.asList(withClasses.value()));
        }

        if (testClasses.isEmpty()) {
            throw new IllegalStateException(
                    "The classes to be compiled during the test must be specified via @WithClasses.");
        }

        return testClasses;
    }

    /**
     * Returns the resources to be compiled for this test.
     *
     * @return A map containing the package were to look for a resource (key) and the resource (value) to be compiled
     * for this test
     */
    private Map<Class<?>, Class<?>> getServices() {
        Map<Class<?>, Class<?>> services = new HashMap<Class<?>, Class<?>>();

        addServices(services, method.getAnnotation(WithServiceImplementations.class));
        addService(services, method.getAnnotation(WithServiceImplementation.class));

        Class<?> declaringClass = method.getMethod().getDeclaringClass();
        addServices(services, declaringClass.getAnnotation(WithServiceImplementations.class));
        addService(services, declaringClass.getAnnotation(WithServiceImplementation.class));

        return services;
    }

    private void addServices(Map<Class<?>, Class<?>> services, WithServiceImplementations withImplementations) {
        if (withImplementations != null) {
            for (WithServiceImplementation resource : withImplementations.value()) {
                addService(services, resource);
            }
        }
    }

    private void addService(Map<Class<?>, Class<?>> services, WithServiceImplementation annoation) {
        if (annoation == null) {
            return;
        }

        Class<?> provides = annoation.provides();
        Class<?> implementor = annoation.value();
        if (provides == Object.class) {
            Class<?>[] implemented = implementor.getInterfaces();
            if (implemented.length != 1) {
                throw new IllegalArgumentException("The class " + implementor.getName()
                        + " either needs to implement exactly one interface, or \"provides\" needs to be specified"
                        + " as well in the annotation " + WithServiceImplementation.class.getSimpleName() + ".");
            }

            provides = implemented[0];
        }

        services.put(provides, implementor);
    }

    /**
     * Returns the processor options to be used this test.
     *
     * @return A list containing the processor options to be used for this test
     */
    private List<String> getProcessorOptions() {
        List<ProcessorOption> processorOptions = getProcessorOptions(method.getAnnotation(ProcessorOptions.class),
                method.getAnnotation(ProcessorOption.class));

        if (processorOptions.isEmpty()) {
            processorOptions = getProcessorOptions(
                    method.getMethod().getDeclaringClass().getAnnotation(ProcessorOptions.class),
                    method.getMethod().getDeclaringClass().getAnnotation(ProcessorOption.class));
        }

        List<String> result = new ArrayList<String>(processorOptions.size());
        for (ProcessorOption option : processorOptions) {
            result.add(asOptionString(option));
        }

        // Add all debugging info to class files
        result.add("-g:source,lines,vars");

        return result;
    }

    private List<ProcessorOption> getProcessorOptions(ProcessorOptions options, ProcessorOption option) {
        if (options != null) {
            return Arrays.asList(options.value());
        } else if (option != null) {
            return Arrays.asList(option);
        }

        return Collections.emptyList();
    }

    private String asOptionString(ProcessorOption processorOption) {
        return String.format("-A%s=%s", processorOption.name(), processorOption.value());
    }

    protected static Set<File> getSourceFiles(Collection<Class<?>> classes) {
        Set<File> sourceFiles = new HashSet<File>(classes.size());

        for (Class<?> clazz : classes) {
            sourceFiles.add(
                    new File(SOURCE_DIR + File.separator + clazz.getName().replace(".", File.separator) + ".java"));
        }

        return sourceFiles;
    }

    private CompilationOutcomeDescriptor compile() throws Exception {

        if (!needsRecompilation()) {
            return compilationCache.getLastResult();
        }

        setupDirectories();
        compilationCache.setLastSourceOutputDir(sourceOutputDir);

        boolean needsAdditionalCompilerClasspath = prepareServices();
        CompilationOutcomeDescriptor resultHolder;

        resultHolder = compileWithSpecificCompiler(compilationRequest, sourceOutputDir, classOutputDir,
                needsAdditionalCompilerClasspath ? additionalCompilerClasspath : null);

        compilationCache.update(compilationRequest, resultHolder);
        return resultHolder;
    }

    protected Object loadAndInstantiate(ClassLoader processorClassloader, Class<?> clazz) {
        try {
            return processorClassloader.loadClass(clazz.getName()).newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected abstract CompilationOutcomeDescriptor compileWithSpecificCompiler(
            CompilationRequest compilationRequest, String sourceOutputDir, String classOutputDir,
            String additionalCompilerClasspath);

    boolean needsRecompilation() {
        return !compilationRequest.equals(compilationCache.getLastRequest());
    }

    private static String getBasePath() {
        try {
            return new File(".").getCanonicalPath();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void createOutputDirs() {
        File directory = new File(classOutputDir);
        deleteDirectory(directory);
        directory.mkdirs();

        directory = new File(sourceOutputDir);
        deleteDirectory(directory);
        directory.mkdirs();

        directory = new File(additionalCompilerClasspath);
        deleteDirectory(directory);
        directory.mkdirs();
    }

    private void deleteDirectory(File path) {
        if (path.exists()) {
            File[] files = path.listFiles();
            for (int i = 0; i < files.length; i++) {
                if (files[i].isDirectory()) {
                    deleteDirectory(files[i]);
                } else {
                    files[i].delete();
                }
            }
        }
        path.delete();
    }

    private boolean prepareServices() {
        if (!compilationRequest.getServices().isEmpty()) {
            String servicesDir = additionalCompilerClasspath + File.separator + "META-INF" + File.separator
                    + "services";
            File directory = new File(servicesDir);
            deleteDirectory(directory);
            directory.mkdirs();
            for (Map.Entry<Class<?>, Class<?>> serviceEntry : compilationRequest.getServices().entrySet()) {
                try {
                    File file = new File(servicesDir + File.separator + serviceEntry.getKey().getName());
                    FileWriter fileWriter = new FileWriter(file);
                    fileWriter.append(serviceEntry.getValue().getName()).append("\n");
                    fileWriter.flush();
                    fileWriter.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            return true;
        }

        return false;
    }

    private static class DiagnosticDescriptorComparator implements Comparator<DiagnosticDescriptor> {

        @Override
        public int compare(DiagnosticDescriptor o1, DiagnosticDescriptor o2) {
            String sourceFileName1 = o1.getSourceFileName() != null ? o1.getSourceFileName() : "";
            String sourceFileName2 = o2.getSourceFileName() != null ? o2.getSourceFileName() : "";

            int result = sourceFileName1.compareTo(sourceFileName2);

            if (result != 0) {
                return result;
            }
            result = Long.valueOf(o1.getLine()).compareTo(o2.getLine());
            if (result != 0) {
                return result;
            }

            return o1.getKind().compareTo(o2.getKind());
        }
    }
}