com.google.code.play2.plugin.MavenPlay2Builder.java Source code

Java tutorial

Introduction

Here is the source code for com.google.code.play2.plugin.MavenPlay2Builder.java

Source

/*
 * Copyright 2013-2016 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.plugin;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.cli.event.ExecutionEventLogger;
import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.execution.DefaultMavenExecutionResult;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutor;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;

import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.util.DirectoryScanner;

import com.google.code.play2.provider.api.AssetCompilationException;
import com.google.code.play2.provider.api.Play2BuildError;
import com.google.code.play2.provider.api.Play2BuildFailure;
import com.google.code.play2.provider.api.Play2Builder;
import com.google.code.play2.provider.api.Play2BuildException;
import com.google.code.play2.provider.api.RoutesCompilationException;
import com.google.code.play2.provider.api.TemplateCompilationException;

import com.google.code.play2.spm.template.Play2TemplateGeneratedSource;
import com.google.code.play2.spm.template.Play2TemplateSourcePositionMapper;

import com.google.code.play2.watcher.api.FileWatchCallback;
import com.google.code.play2.watcher.api.FileWatchException;
import com.google.code.play2.watcher.api.FileWatchService;
import com.google.code.play2.watcher.api.FileWatcher;

import com.google.code.sbt.compiler.api.Analysis;
import com.google.code.sbt.compiler.api.AnalysisProcessor;
import com.google.code.sbt.compiler.api.CompilerException;
import com.google.code.sbt.compiler.api.Compilers;

public class MavenPlay2Builder implements Play2Builder, FileWatchCallback {
    private List<MavenProject> projects;

    private String sourceEncoding;

    private List<String> goals;

    private List<String> additionalGoals;

    private String assetsPrefix;

    private Log logger;

    private MavenSession session;

    private LifecycleExecutor lifecycleExecutor;

    private PlexusContainer container;

    private File templateCompilationOutputDirectory;

    private AnalysisProcessor sbtAnalysisProcessor;

    private FileWatchService playWatchService;

    private FileWatcher watcher = null; // created after first successful build

    // Flag to force a reload on the next request.
    // This is set if a compile error occurs, and also by the forceReload method on BuildLink, which is called for
    // example when evolutions have been applied.
    //    @volatile private var forceReloadNextTime = false
    private boolean forceReloadNextTime = true;
    // Whether any source files have changed since the last request.
    //    @volatile private var changed = false
    //    private volatile boolean changed = false;
    private /*?volatile*/ Map<String, Long> changedFiles = new HashMap<String, Long>(); //TODO - moze od razu moduy, a nie pliki
    private Object changedFilesLock = new Object();

    private boolean afterFirstSuccessfulBuild = false;
    private Map<MavenProject, Map<String, File>> currentSourceMaps;
    private Map<MavenProject, Long> currentClasspathTimestamps;
    private Map<MavenProject, Set<String>> currentClasspathFilePaths;

    public MavenPlay2Builder(List<MavenProject> projects, String sourceEncoding, List<String> goals,
            List<String> additionalGoals, String assetsPrefix, Log logger, MavenSession session,
            LifecycleExecutor lifecycleExecutor, PlexusContainer container, File templateCompilationOutputDirectory,
            AnalysisProcessor sbtAnalysisProcessor, FileWatchService playWatchService) {
        this.projects = projects;
        this.sourceEncoding = sourceEncoding;
        this.goals = goals;
        this.additionalGoals = additionalGoals;
        this.assetsPrefix = assetsPrefix;
        this.logger = logger;
        this.session = session;
        this.lifecycleExecutor = lifecycleExecutor;
        this.container = container;
        this.templateCompilationOutputDirectory = templateCompilationOutputDirectory;
        this.sbtAnalysisProcessor = sbtAnalysisProcessor;
        this.playWatchService = playWatchService;

        currentSourceMaps = new HashMap<MavenProject, Map<String, File>>(projects.size());
        currentClasspathTimestamps = new HashMap<MavenProject, Long>(projects.size());
        currentClasspathFilePaths = new HashMap<MavenProject, Set<String>>(projects.size());
        for (MavenProject p : projects) {
            currentClasspathTimestamps.put(p, Long.valueOf(0));
            currentClasspathFilePaths.put(p, Collections.<String>emptySet());
        }
    }

    @Override /* FileWatchCallback */
    public void close() throws IOException {
        if (watcher != null) {
            watcher.close();
        }
    }

    @Override /* FileWatchCallback */
    public void onChange(File changedFile) {
        String path = changedFile.getAbsolutePath();
        Long currentTimestamp = Long.valueOf(changedFile.lastModified());
        synchronized (changedFilesLock) {
            Long prevTimestamp = changedFiles.get(path);
            if (prevTimestamp == null || !prevTimestamp.equals(currentTimestamp)) {
                logger.debug("\"" + path + "\" file changed");
                changedFiles.put(path, currentTimestamp);
            }
        }
    }

    @Override /* Play2Builder */
    public Object[] findSource(String className, Integer line) {
        Object[] result = null;
        String topType = className.split("\\$")[0];
        for (Map<String, File> sourceMap : currentSourceMaps.values()) {
            File sourceFile = sourceMap.get(topType);
            if (sourceFile != null) {
                result = new Object[] { sourceFile, line };
                if (sourceFile.getAbsolutePath().startsWith(templateCompilationOutputDirectory.getAbsolutePath())) {
                    try {
                        Play2TemplateSourcePositionMapper mapper = new Play2TemplateSourcePositionMapper();
                        mapper.setCharsetName(sourceEncoding);
                        Play2TemplateGeneratedSource template = mapper.getGeneratedSource(sourceFile);
                        if (template != null) {
                            File originalSourceFile = new File(template.getSourceFileName());
                            Integer originalLine = null;
                            if (line != null) {
                                originalLine = Integer.valueOf(template.mapLine(line.intValue()));
                            }
                            result = new Object[] { originalSourceFile, originalLine };
                        }
                    } catch (IOException e) {
                        // ignore
                    } catch (RuntimeException e) {
                        // ignore
                    }
                }
                break;
            }
        }
        return result;
    }

    @Override /* Play2Builder */
    public boolean build() throws Play2BuildFailure, Play2BuildError/*Play2BuildException*/
    {
        Set<String> changedFilePaths = null;
        Map<String, Long> prevChangedFiles = new HashMap<String, Long>();
        synchronized (changedFilesLock) {
            if (!changedFiles.isEmpty()) {
                changedFilePaths = changedFiles.keySet();
                prevChangedFiles = changedFiles;
                changedFiles = new HashMap<String, Long>();
            }
            //TEST - more code inside synchronized block
        }

        if (!forceReloadNextTime && changedFilePaths == null /*&& afterFirstSuccessfulBuild*/ ) {
            return false;
        }

        List<MavenProject> projectsToBuild = projects;
        // - !afterFirstSuccessfulBuild => first build or no previous successful builds, build all modules
        // - currentSourceMaps.isEmpty() => first build, build all modules
        // - projects.size() == 1 => one-module project, just build it
        // - else => not the first build in multimodule-project, calculate modules subset to build
        if (afterFirstSuccessfulBuild
                /*!currentSourceMaps.isEmpty()*//*currentSourceMap != null*/ && projects.size() > 1) {
            projectsToBuild = calculateProjectsToBuild(changedFilePaths);
        }

        MavenExecutionRequest request = DefaultMavenExecutionRequest.copy(session.getRequest());
        request.setStartTime(new Date());
        request.setExecutionListener(new ExecutionEventLogger());
        request.setGoals(goals);

        MavenExecutionResult result = new DefaultMavenExecutionResult();

        MavenSession newSession = new MavenSession(container, session.getRepositorySession(), request, result);
        newSession.setProjects(projectsToBuild);
        newSession.setCurrentProject(session.getCurrentProject());
        newSession.setParallel(session.isParallel());
        newSession.setProjectDependencyGraph(session.getProjectDependencyGraph());

        lifecycleExecutor.execute(newSession);

        forceReloadNextTime = result.hasExceptions();

        if (!result.hasExceptions() && !additionalGoals.isEmpty()) {
            request = DefaultMavenExecutionRequest.copy(session.getRequest());
            request.setStartTime(new Date());
            request.setExecutionListener(new ExecutionEventLogger());
            request.setGoals(additionalGoals);

            result = new DefaultMavenExecutionResult();

            newSession = new MavenSession(container, session.getRepositorySession(), request, result);
            List<MavenProject> onlyMe = Arrays.asList(new MavenProject[] { session.getCurrentProject() });
            newSession.setProjects(onlyMe);
            newSession.setCurrentProject(session.getCurrentProject());
            newSession.setParallel(session.isParallel());
            newSession.setProjectDependencyGraph(session.getProjectDependencyGraph());

            lifecycleExecutor.execute(newSession);

            forceReloadNextTime = result.hasExceptions();
        }

        if (result.hasExceptions()) {
            synchronized (changedFilesLock) {
                changedFiles.putAll(prevChangedFiles); // restore previously changed paths, required for next rebuild
            }
            Throwable firstException = result.getExceptions().get(0);
            if (firstException.getCause() instanceof MojoFailureException) {
                MojoFailureException mfe = (MojoFailureException) firstException.getCause();
                Throwable mfeCause = mfe.getCause();
                if (mfeCause != null) {
                    try {
                        Play2BuildException pbe = null;
                        String causeName = mfeCause.getClass().getName();

                        // sbt-compiler exception
                        if (CompilerException.class.getName().equals(causeName)) {
                            pbe = getSBTCompilerBuildException(mfeCause);
                        } else if (AssetCompilationException.class.getName().equals(causeName)
                                || RoutesCompilationException.class.getName().equals(causeName)
                                || TemplateCompilationException.class.getName().equals(causeName)) {
                            pbe = getPlayBuildException(mfeCause);
                        }

                        if (pbe != null) {
                            throw new Play2BuildFailure(pbe, sourceEncoding);
                        }
                        throw new Play2BuildError("Build failed without reporting any problem!"/*?, ce*/ );
                    } catch (Play2BuildFailure e) {
                        throw e;
                    } catch (Play2BuildError e) {
                        throw e;
                    } catch (Exception e) {
                        throw new Play2BuildError(".... , check Maven console");
                    }
                }
            }
            throw new Play2BuildError("The compilation task failed, check Maven console"/*?, firstException*/ );
        }

        // no exceptions
        if (!afterFirstSuccessfulBuild) // this was first successful build
        {
            afterFirstSuccessfulBuild = true;

            if (playWatchService != null) {
                // Monitor all existing, not generated (inside output directory) source and resource roots
                List<File> monitoredDirectories = new ArrayList<File>();
                for (MavenProject p : projects) {
                    String targetDirectory = p.getBuild().getDirectory();
                    for (String sourceRoot : p.getCompileSourceRoots()) {
                        if (!sourceRoot.startsWith(targetDirectory) && new File(sourceRoot).isDirectory()) {
                            monitoredDirectories.add(new File(sourceRoot));
                        }
                    }
                    for (Resource resource : p.getResources()) {
                        String resourceRoot = resource.getDirectory();
                        if (!resourceRoot.startsWith(targetDirectory) && new File(resourceRoot).isDirectory()) {
                            monitoredDirectories.add(new File(resourceRoot));
                        }
                    }
                }
                //TODO - remove roots nested inside another roots (is it possible?)

                try {
                    watcher = playWatchService.watch(monitoredDirectories, this);
                } catch (FileWatchException e) {
                    logger.warn("File watcher initialization failed. Running without hot-reload functionality.", e);
                }
            }
        }

        Map<MavenProject, Map<String, File>> sourceMaps = new HashMap<MavenProject, Map<String, File>>(
                currentSourceMaps);
        for (MavenProject p : projectsToBuild) {
            Map<String, File> sourceMap = new HashMap<String, File>();
            File classesDirectory = new File(p.getBuild().getOutputDirectory());
            String classesDirectoryPath = classesDirectory.getAbsolutePath() + File.separator;
            File analysisCacheFile = defaultAnalysisCacheFile(p);
            Analysis analysis = sbtAnalysisProcessor.readFromFile(analysisCacheFile);
            for (File sourceFile : analysis.getSourceFiles()) {
                Set<File> sourceFileProducts = analysis.getProducts(sourceFile);
                for (File product : sourceFileProducts) {
                    String absolutePath = product.getAbsolutePath();
                    if (absolutePath.contains("$")) {
                        continue; // skip inner and object classes
                    }
                    String relativePath = absolutePath.substring(classesDirectoryPath.length());
                    //                    String name = product.getName();
                    String name = relativePath.substring(0, relativePath.length() - ".class".length());
                    /*if (name.indexOf( '$' ) > 0)
                    {
                    name = name.substring( 0, name.indexOf( '$' ) );
                    }*/
                    name = name.replace(File.separator, ".");
                    //System.out.println(sourceFile.getPath() + " -> " + name);
                    sourceMap.put(name, sourceFile);
                }
                /*String[] definitionNames = analysis.getDefinitionNames( sourceFile );
                Set<String> uniqueDefinitionNames = new HashSet<String>(definitionNames.length);
                for (String definitionName: definitionNames)
                {
                if ( !uniqueDefinitionNames.contains( definitionName ) )
                {
                    result.put( definitionName, sourceFile );
                //                        System.out.println( "definitionName:'" + definitionName + "', source:'"
                //                                        + sourceFile.getAbsolutePath() + "'" );
                    uniqueDefinitionNames.add( definitionName );
                }
                }*/
            }
            sourceMaps.put(p, sourceMap);
        }
        this.currentSourceMaps = sourceMaps;

        boolean reloadRequired = false;
        for (MavenProject p : projectsToBuild) {
            long lastModifiedTime = 0L;
            Set<String> outputFilePaths = new HashSet<String>();
            File outputDirectory = new File(p.getBuild().getOutputDirectory());
            if (outputDirectory.exists() && outputDirectory.isDirectory()) {
                DirectoryScanner classPathScanner = new DirectoryScanner();
                classPathScanner.setBasedir(outputDirectory);
                classPathScanner.setExcludes(new String[] { assetsPrefix + "**" });
                classPathScanner.scan();
                String[] files = classPathScanner.getIncludedFiles();
                for (String fileName : files) {
                    File f = new File(outputDirectory, fileName);
                    outputFilePaths.add(f.getAbsolutePath());
                    long lmf = f.lastModified();
                    if (lmf > lastModifiedTime) {
                        lastModifiedTime = lmf;
                    }
                }
            }
            if (!reloadRequired && (lastModifiedTime > currentClasspathTimestamps.get(p).longValue()
                    || !outputFilePaths.equals(currentClasspathFilePaths.get(p)))) {
                reloadRequired = true;
            }
            currentClasspathTimestamps.put(p, Long.valueOf(lastModifiedTime));
            currentClasspathFilePaths.put(p, outputFilePaths);
        }

        return reloadRequired;
    }

    private Play2BuildException getPlayBuildException(Throwable playException) {
        Play2BuildException result = null;
        try {
            String message = playException.getClass().getMethod("getMessage").invoke(playException).toString();
            Object file = playException.getClass().getMethod("source").invoke(playException);
            String fileAbsolutePath = file.getClass().getMethod("getAbsolutePath").invoke(file).toString();
            Integer line = (Integer) playException.getClass().getMethod("line").invoke(playException);
            Integer position = (Integer) playException.getClass().getMethod("position").invoke(playException);
            result = new Play2BuildException(new File(fileAbsolutePath), message, line.intValue(),
                    position.intValue());
        } catch (IllegalAccessException e) {
            // ignore
        } catch (InvocationTargetException e) {
            // ignore
        } catch (NoSuchMethodException e) {
            // ignore
        } catch (SecurityException e) {
            // ignore
        }
        return result;
    }

    private Play2BuildException getSBTCompilerBuildException(Throwable compilerException)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException {
        boolean hasGetProblemsMethod = false;
        for (java.lang.reflect.Method m : compilerException.getClass().getMethods()) {
            if ("getProblems".equals(m.getName())) {
                hasGetProblemsMethod = true;
                break;
            }
        }
        if (!hasGetProblemsMethod) {
            //TODO - add warning about too old sbt-compiler-maven-plugin version
            return null; //TODO - how to display meaningful message on the screen?
        }

        Play2BuildException result = null;
        Object problems = compilerException.getClass().getMethod("getProblems").invoke(compilerException);
        if (problems.getClass().isArray()) {
            int length = Array.getLength(problems);
            for (int i = 0; i < length; i++) {
                Object problem = Array.get(problems, i);
                String severity = problem.getClass().getMethod("getSeverity").invoke(problem).toString();
                if ("Error".equals(severity)) {
                    //String category = problem.getClass().getMethod( "getCategory"  ).invoke( problem ).toString();
                    String message = problem.getClass().getMethod("getMessage").invoke(problem).toString();
                    Object sourcePosition = problem.getClass().getMethod("getPosition").invoke(problem);
                    if (sourcePosition != null) {
                        Integer line = (Integer) sourcePosition.getClass().getMethod("getLine")
                                .invoke(sourcePosition);
                        //String lineContent = sourcePosition.getClass().getMethod( "getLineContent" ).invoke( sourcePosition ).toString();
                        //Object offset = sourcePosition.getClass().getMethod( "getOffset" ).invoke( sourcePosition );
                        Integer pointer = (Integer) sourcePosition.getClass().getMethod("getPointer")
                                .invoke(sourcePosition);
                        Object file = sourcePosition.getClass().getMethod("getFile").invoke(sourcePosition); //FIXME is it nullable?
                        String fileAbsolutePath = file.getClass().getMethod("getAbsolutePath").invoke(file)
                                .toString();
                        result = new Play2BuildException(new File(fileAbsolutePath), message, line.intValue(),
                                pointer.intValue());
                    } else {
                        result = new Play2BuildException(null, message, 0, 0);
                    }
                    break;
                }
            }
        }
        return result;
    }

    // === TEST ===================================================================================

    private File defaultAnalysisCacheFile(MavenProject p) {
        File classesDirectory = new File(p.getBuild().getOutputDirectory());
        return new File(Compilers.getCacheDirectory(classesDirectory), "compile");
    }

    // === TEST ===================================================================================

    private List<MavenProject> calculateProjectsToBuild(Set<String> changedFilePaths) {
        Set<MavenProject> changedProjects = new HashSet<MavenProject>(projects.size());
        for (String path : changedFilePaths) {
            MavenProject p = findProjectFor(path);
            changedProjects.add(p);
        }

        Set<MavenProject> changedAndDependentProjects = new HashSet<MavenProject>(projects.size());
        for (MavenProject p : changedProjects) {
            changedAndDependentProjects.add(p);
            List<MavenProject> deps = session.getProjectDependencyGraph().getDownstreamProjects(p,
                    true/*transitive*/ );
            for (MavenProject depP : deps) {
                if (projects.contains(depP)) {
                    changedAndDependentProjects.addAll(deps);
                }
            }
        }

        List<MavenProject> result = new ArrayList<MavenProject>(changedAndDependentProjects.size());
        for (MavenProject p : projects) {
            if (changedAndDependentProjects.contains(p)) {
                result.add(p);
            }
        }
        return result;
    }

    private MavenProject findProjectFor(String filePath) {
        MavenProject result = null;
        search: for (MavenProject p : projects) {
            for (String sourceRoot : p.getCompileSourceRoots()) {
                if (filePath.startsWith(sourceRoot)) {
                    result = p;
                    break search;
                }
            }
            for (Resource resource : p.getResources()) {
                if (filePath.startsWith(resource.getDirectory())) {
                    result = p;
                    break search;
                }
            }
        }
        return result;
    }

}