org.codehaus.mojo.jsimport.AbstractGenerateHtmlMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.mojo.jsimport.AbstractGenerateHtmlMojo.java

Source

package org.codehaus.mojo.jsimport;

/*
 * 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 java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.filtering.MavenFileFilter;
import org.apache.maven.shared.filtering.MavenFileFilterRequest;
import org.apache.maven.shared.filtering.MavenFilteringException;
import org.codehaus.plexus.util.StringUtils;
import org.sonatype.plexus.build.incremental.BuildContext;

/**
 * Mojo for generating properties for filtering into html script elements. This uses the serialised file dependency
 * graph provided by the import mojo.
 */
public abstract class AbstractGenerateHtmlMojo extends AbstractMojo {
    /**
     * The current project scope.
     */
    protected enum Scope {
        /** */
        COMPILE,
        /** */
        TEST
    };

    /**
     * @parameter default-value="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    /**
     * @parameter default-value="${session}"
     * @readonly
     * @required
     */
    private MavenSession session;

    /**
     * The local repo.
     * 
     * @parameter default-value="${localRepository}"
     * @required
     * @readonly
     */
    private ArtifactRepository localRepository;

    /**
     * Files to include. Defaults to "**\/*.html,**\/*.htm".
     * 
     * @parameter
     */
    private List<String> includes;

    /**
     * Files to exclude. Nothing is excluded by default.
     * 
     * @parameter
     */
    private List<String> excludes;

    /**
     * The target path for js files.
     * 
     * @parameter default-value="js"
     * @required
     */
    private String targetJsPath;

    /**
     * The character encoding scheme to be applied when filtering resources.
     * 
     * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
     * @required
     */
    private String encoding;

    /**
     * The build context so that we can tell Maven certain files have changed if required.
     * 
     * @component
     */
    private BuildContext buildContext;

    /**
     * The Maven filtering to use.
     * 
     * @component
     */
    private MavenFileFilter mavenFileFilter;

    /**
     * A graph of filenames and their dependencies.
     */
    private final Map<String, LinkedHashSet<String>> fileDependencies = new HashMap<String, LinkedHashSet<String>>();

    /**
     * As a above but for compile time dependencies only.
     */
    private final Map<String, LinkedHashSet<String>> compileFileDependencies = new HashMap<String, LinkedHashSet<String>>();

    /**
     * Given a set of file paths, build a new set of any dependencies each of these paths may have, and any dependencies
     * that these dependencies have etc.
     * 
     * @param a set of nodes already visited so as to avoid overflow.
     * @param filePaths the set of file paths to iterate over.
     * @param allImports the set to build.
     * @return if not null then this represents a file path that revealed a cyclical depedency issue.
     */
    private String buildImportsRecursively(Set<String> visitedNodes, LinkedHashSet<String> filePaths,
            LinkedHashSet<String> allImports) {
        String cyclicFilePath = null;
        for (String filePath : filePaths) {
            if (!visitedNodes.contains(filePath)) {
                visitedNodes.add(filePath);

                LinkedHashSet<String> filePathDependencies = fileDependencies.get(filePath);
                if (filePathDependencies == null && compileFileDependencies != null) {
                    filePathDependencies = compileFileDependencies.get(filePath);
                }

                if (filePathDependencies != null) {
                    cyclicFilePath = buildImportsRecursively(visitedNodes, filePathDependencies, allImports);
                } else if (allImports.contains(filePath)) {
                    cyclicFilePath = filePath;
                }

                if (cyclicFilePath != null) {
                    break;
                }

                allImports.add(filePath);
            }
        }
        return cyclicFilePath;
    }

    /**
     * Copy files over from the local repo to the target folders. We only do the copy if the modification date of the
     * source file is greater than the destination file.
     * 
     * @param localRepoFilesToCopy the files to copy. This is a mapping of source file to target file.
     * @throws MojoExecutionException if there is an IO issue.
     */
    private void copyLocalRepoFilesToTarget(Map<String, String> localRepoFilesToCopy, File targetFolder)
            throws MojoExecutionException {
        for (Map.Entry<String, String> entry : localRepoFilesToCopy.entrySet()) {
            try {
                File sourceFile = new File(entry.getKey());
                File targetFile = new File(targetFolder, entry.getValue());
                if (sourceFile.lastModified() > targetFile.lastModified()) {
                    FileUtils.copyFile(sourceFile, targetFile);
                    if (getLog().isDebugEnabled()) {
                        getLog().debug("Copying file: " + sourceFile.getAbsolutePath());
                    }
                }
            } catch (IOException e) {
                throw new MojoExecutionException("While copying files: ", e);
            }
        }
    }

    /**
     * Perform the goal of this mojo.
     * 
     * @param sourceJsFolder the folder where the source js files reside.
     * @param mainSourceJsFolder the folder where the main source js files reside.
     * @param htmlResourceFolder the folder where the resource html files reside.
     * @param targetFolder where all files are going to end up.
     * @param mainTargetFolder where all main files are going to end up.
     * @param workFolder the folder where our work files can be found.
     * @param scope scope the scope of the dependencies we are to search for.
     * @throws MojoExecutionException if there is an execution failure.
     */
    public void doExecute(File sourceJsFolder, File mainSourceJsFolder, File htmlResourceFolder, //
            File targetFolder, File mainTargetFolder, File workFolder, Scope scope) throws MojoExecutionException {
        // Load the dependency graph. Note fileAssignedGlobals is unused by this
        // mojo but a the utility below is contracted to supply it.
        Map<String, String> fileAssignedGlobals = new HashMap<String, String>();
        long fileDependencyGraphModificationTime = FileDependencyPersistanceUtil.readFileDependencyGraph(workFolder,
                fileDependencies, fileAssignedGlobals);

        // If we are in test scope then also load in the compile scoped dependency information as we need to resolve
        // against this also.
        File mainWorkFolder;
        long mainFileDependencyGraphModificationTime;
        if (scope == Scope.TEST) {
            mainWorkFolder = new File(workFolder.getParentFile(), "main");
            Map<String, String> compileFileAssignedGlobals = new HashMap<String, String>();
            mainFileDependencyGraphModificationTime = FileDependencyPersistanceUtil.readFileDependencyGraph(
                    mainWorkFolder, compileFileDependencies, //
                    compileFileAssignedGlobals);
        } else {
            mainWorkFolder = workFolder;
            mainFileDependencyGraphModificationTime = fileDependencyGraphModificationTime;
        }

        // Generate properties relating to the file dependency graph and form a map between source and target files that
        // should be copied from the local repo.
        Map<String, String> localRepoFilesToCopy = new HashMap<String, String>();
        Map<String, String> mainLocalRepoFilesToCopy = new HashMap<String, String>();
        Properties fileDependencyProperties = generateProperties(sourceJsFolder, mainSourceJsFolder, workFolder,
                mainWorkFolder, localRepoFilesToCopy, mainLocalRepoFilesToCopy);

        // Copy local repo files to the target folder
        copyLocalRepoFilesToTarget(localRepoFilesToCopy, targetFolder);
        if (scope == Scope.TEST) {
            copyLocalRepoFilesToTarget(mainLocalRepoFilesToCopy, mainTargetFolder);
        }

        // Filter all of our HTML file's script statements and generate them into the target folder.
        generateHtmlWithProperties(fileDependencyProperties, fileDependencyGraphModificationTime,
                mainFileDependencyGraphModificationTime, htmlResourceFolder, targetFolder);

    }

    // The last step is to refresh all of the html files in the resource folder if our dependencies are more recent.
    // This is because we need to ensure that html files have their script elements re-generated correctly.
    private void generateHtmlWithProperties(Properties fileDependencyProperties,
            long fileDependencyGraphModificationTime, long mainFileDependencyGraphModificationTime,
            File htmlResourceFolder, File targetFolder) {
        FileCollector fileCollector = new FileCollector(buildContext, new String[] { "**/*.html", "**/*.htm" },
                new String[] {});
        List<String> includedFiles = fileCollector.collectPaths(htmlResourceFolder, includes, excludes);
        boolean htmlFilesFiltered = false;
        int lastNestedFolderCount = -1;

        MavenFileFilterRequest filterRequest = new MavenFileFilterRequest();
        filterRequest.setMavenProject(project);
        filterRequest.setMavenSession(session);
        filterRequest.setFiltering(true);

        for (String resourceFile : includedFiles) {
            File destinationFile = new File(targetFolder, resourceFile);
            File sourceFile = new File(htmlResourceFolder, resourceFile);

            if (htmlFilesFiltered || sourceFile.lastModified() > destinationFile.lastModified()
                    || destinationFile.lastModified() < fileDependencyGraphModificationTime
                    || destinationFile.lastModified() < mainFileDependencyGraphModificationTime) {
                if (getLog().isDebugEnabled()) {
                    getLog().debug("Applying import filter to: " + resourceFile);
                }

                // Conditionally apply a map of properties that has the correct relative path expression to each js
                // file.
                int nestedFolderCount = StringUtils.countMatches(resourceFile, File.separator);
                if (nestedFolderCount != lastNestedFolderCount) {
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < nestedFolderCount; ++i) {
                        sb.append("../");
                    }
                    String relativePath = sb.toString();
                    Properties relativeFileDependencyProperties = new Properties();
                    for (Map.Entry<Object, Object> keyValue : fileDependencyProperties.entrySet()) {
                        relativeFileDependencyProperties.put(keyValue.getKey(),
                                relativePath + ((String) keyValue.getValue())//
                                        .replaceAll("src=\"", "src=\"" + relativePath));
                    }
                    filterRequest.setAdditionalProperties(relativeFileDependencyProperties);

                    lastNestedFolderCount = nestedFolderCount;
                }

                // Copy the file with filtering.
                filterRequest.setFrom(sourceFile);

                destinationFile.getParentFile().mkdirs();
                filterRequest.setTo(destinationFile);

                try {
                    mavenFileFilter.copyFile(filterRequest);
                    buildContext.refresh(destinationFile);
                    htmlFilesFiltered = true;
                } catch (MavenFilteringException e) {
                    getLog().error(e);
                }
            }
        }
        if (htmlFilesFiltered) {
            getLog().info("Dependencies changed. " + includedFiles.size() + " file(s) re-filtered.");
        }
    }

    /**
     * Generate the file dependency properties and also populate a mapping of repository files to be copied into the
     * target folder.
     * 
     * @param sourceJsFolder the folder where the source js files reside.
     * @param mainSourceJsFolder the folder where the main source js files reside.
     * @param workFolder the folder where we can read/write work files that are durable between builds (and thus useful
     *            between builds).
     * @param mainWorkFolder as above but for a work folder belonging to the main scope.
     * @param scope scope the scope of the dependencies we are to search for.
     * @param localRepoFilesToCopy the mapping of source files that should be copied to the target folder
     * @param mainLocalRepoFilesToCopy as above but for repo files belonging to the main scope
     * @throws MojoExecutionException if there is an execution failure.
     */
    private Properties generateProperties(File sourceJsFolder, File mainSourceJsFolder, File workFolder,
            File mainWorkFolder, Map<String, String> localRepoFilesToCopy,
            Map<String, String> mainLocalRepoFilesToCopy) throws MojoExecutionException {
        Properties fileDependencyProperties = new Properties();

        String sourceFolderPath = sourceJsFolder.getAbsolutePath();
        if (sourceFolderPath.length() == 0) {
            return fileDependencyProperties;
        }
        String mainSourceFolderPath = mainSourceJsFolder.getAbsolutePath();
        if (mainSourceFolderPath.length() == 0) {
            return fileDependencyProperties;
        }

        LocalRepositoryCollector localRepositoryCollector = new LocalRepositoryCollector(project, localRepository,
                new File[] { new File(workFolder, "www-zip"), new File(mainWorkFolder, "www-zip") });

        for (Map.Entry<String, LinkedHashSet<String>> entry : fileDependencies.entrySet()) {
            String jsFile = entry.getKey();

            // Make our source files available for script elements.
            if (jsFile.startsWith(sourceFolderPath)) {
                // Build a new set of imports for this current js file and all
                // of its imports and their imports etc.
                Set<String> visitedNodes = new HashSet<String>();
                LinkedHashSet<String> allImports = new LinkedHashSet<String>();
                String cyclicFilePath = buildImportsRecursively(visitedNodes, entry.getValue(), allImports);
                if (cyclicFilePath == null && allImports.contains(jsFile)) {
                    cyclicFilePath = jsFile;
                }
                // Build a set of script statements for filtering into HTML
                // files.
                String closeOpenScriptDeclaration = "\"></script>\n<script type=\"text/javascript\" src=\"";

                StringBuilder propertyValue = new StringBuilder();
                for (String importFile : allImports) {
                    // Make the file path relative.
                    String relativeImportFile;
                    if (importFile.startsWith(sourceFolderPath)) {
                        relativeImportFile = targetJsPath + importFile.substring(sourceFolderPath.length());
                    } else if (importFile.startsWith(mainSourceFolderPath)) {
                        relativeImportFile = targetJsPath + importFile.substring(mainSourceFolderPath.length());
                    } else {
                        // We don't appear to be looking at a project source file here so it must belong to one of our
                        // local repositories.
                        String localRepositoryPath = localRepositoryCollector.findLocalRepository(importFile);
                        if (localRepositoryPath != null) {
                            relativeImportFile = targetJsPath + importFile.substring(localRepositoryPath.length());
                            // Flag this file for copying as long as the file belongs to our scope. If the compile time
                            // dependencies are null then we are in compile scope. Otherwise we are in test scope, in
                            // which case we only copy the file if the dependency belongs to the test scope.
                            if (compileFileDependencies == null || //
                                    (compileFileDependencies != null && //
                                            !compileFileDependencies.containsKey(importFile))) {
                                localRepoFilesToCopy.put(importFile, relativeImportFile);
                            } else {
                                mainLocalRepoFilesToCopy.put(importFile, relativeImportFile);
                            }
                        } else {
                            throw new MojoExecutionException("Unexpected import file path "
                                    + "(not project relative or local repo): " + importFile);
                        }
                    }

                    // We're now in a position of formatting the file path to the user in situations where we detected a
                    // problem earlier.
                    if (importFile.equals(cyclicFilePath)) {
                        throw new MojoExecutionException("Cyclic reference found in: " + relativeImportFile);
                    }

                    if (propertyValue.length() > 0) {
                        propertyValue.append(closeOpenScriptDeclaration);
                    }
                    propertyValue.append(relativeImportFile);
                }
                if (propertyValue.length() > 0) {
                    propertyValue.append(closeOpenScriptDeclaration);
                }

                // Properties are always site relative and expressed without a /js to make it simple for substitution.
                String propertyName;
                if (sourceFolderPath.length() > 1) {
                    propertyName = jsFile.substring(sourceFolderPath.length() + 1);
                } else {
                    propertyName = jsFile;
                }

                // Ensure that the properties are normalised to be OS independent.
                String normalisedPropertyName = propertyName.replace(File.separatorChar, '/');
                String normalisedPropertyValue = propertyValue.append(targetJsPath + "/" + normalisedPropertyName)
                        .toString() //
                        .replace(File.separatorChar, '/');

                // Finally, set the property.
                fileDependencyProperties.setProperty(normalisedPropertyName, normalisedPropertyValue);

                if (getLog().isDebugEnabled()) {
                    getLog().debug("Generating script statements for files: " + allImports
                            + " relating to filter property: " + normalisedPropertyName);
                }

            }
        }

        return fileDependencyProperties;
    }

    /**
     * @return property.
     */
    public BuildContext getBuildContext() {
        return buildContext;
    }

    /**
     * @return property.
     */
    public String getEncoding() {
        return encoding;
    }

    /**
     * @return property.
     */
    public List<String> getExcludes() {
        return excludes;
    }

    /**
     * @return property.
     */
    public Map<String, LinkedHashSet<String>> getFileDependencies() {
        return fileDependencies;
    }

    /**
     * @return property.
     */
    public List<String> getIncludes() {
        return includes;
    }

    /**
     * @return property.
     */
    public ArtifactRepository getLocalRepository() {
        return localRepository;
    }

    /**
     * @return property.
     */
    public MavenFileFilter getMavenFileFilter() {
        return mavenFileFilter;
    }

    /**
     * @return property.
     */
    public MavenProject getProject() {
        return project;
    }

    /**
     * @return property.
     */
    public MavenSession getSession() {
        return session;
    }

    /**
     * @return property.
     */
    public String getTargetJsPath() {
        return targetJsPath;
    }

    /**
     * @param buildContext set property.
     */
    public void setBuildContext(BuildContext buildContext) {
        this.buildContext = buildContext;
    }

    /**
     * @param encoding set property.
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /**
     * @param excludes set property.
     */
    public void setExcludes(List<String> excludes) {
        this.excludes = excludes;
    }

    /**
     * @param includes set property.
     */
    public void setIncludes(List<String> includes) {
        this.includes = includes;
    }

    /**
     * @param localRepository set property.
     */
    public void setLocalRepository(ArtifactRepository localRepository) {
        this.localRepository = localRepository;
    }

    /**
     * @param mavenFileFilter set property.
     */
    public void setMavenFileFilter(MavenFileFilter mavenFileFilter) {
        this.mavenFileFilter = mavenFileFilter;
    }

    /**
     * @param project property.
     */
    public void setProject(MavenProject project) {
        this.project = project;
    }

    /**
     * @param session property.
     */
    public void setSession(MavenSession session) {
        this.session = session;
    }

    /**
     * @param targetJsPath set property.
     */
    public void setTargetJsPath(String targetJsPath) {
        this.targetJsPath = targetJsPath;
    }

}