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.Arrays; 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.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; 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.Scanner; 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 }; /** * The local repo. * * @parameter default-value="${localRepository}" * @required * @readonly */ private ArtifactRepository localRepository; /** * HTML file extensions. * * @parameter default-value="**\/*.html,**\/*.htm" * @required */ private String htmlResourceExtensions; /** * 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. */ private void buildImportsRecursively( Set<String> visitedNodes, LinkedHashSet<String> filePaths, LinkedHashSet<String> allImports ) { 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 ) { buildImportsRecursively( visitedNodes, filePathDependencies, allImports ); } allImports.add( filePath ); } } } /** * 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 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 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. if ( scope == Scope.TEST ) { Map<String, String> compileFileAssignedGlobals = new HashMap<String, String>(); FileDependencyPersistanceUtil.readFileDependencyGraph( new File( workFolder.getParentFile(), "main" ), compileFileDependencies, // compileFileAssignedGlobals ); } // 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>(); Properties fileDependencyProperties = generateProperties( sourceJsFolder, mainSourceJsFolder, localRepoFilesToCopy ); // Copy local repo files to the target folder copyLocalRepoFilesToTarget( localRepoFilesToCopy, targetFolder ); // Filter all of our HTML file's script statements and generate them into the target folder. generateHtmlWithProperties( fileDependencyProperties, fileDependencyGraphModificationTime, 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, File htmlResourceFolder, File targetFolder ) { Scanner scanner = buildContext.newScanner( htmlResourceFolder, true ); scanner.setIncludes( htmlResourceExtensions.split( "," ) ); scanner.scan(); List<String> includedFiles = Arrays.asList( scanner.getIncludedFiles() ); boolean htmlFilesFiltered = false; int lastNestedFolderCount = -1; MavenFileFilterRequest filterRequest = new MavenFileFilterRequest(); 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 ) { 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, "/" ); 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 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 * @throws MojoExecutionException if there is an execution failure. */ private Properties generateProperties( File sourceJsFolder, File mainSourceJsFolder, Map<String, String> localRepoFilesToCopy ) 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; } String localRepositoryPath = localRepository.getBasedir(); 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>(); buildImportsRecursively( visitedNodes, entry.getValue(), allImports ); // Build a set of script statements for filtering into HTML // files. String closeOpenScriptDeclaration = "\"></script><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( localRepositoryPath ) ) { 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 if ( importFile.startsWith( mainSourceFolderPath ) ) { relativeImportFile = targetJsPath + importFile.substring( mainSourceFolderPath.length() ); } else { throw new MojoExecutionException( "Unexpected import file path " + "(not project relative or local repo): " + importFile ); } 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; } if ( getLog().isDebugEnabled() ) { getLog().debug( "Generating script statements for files: " + allImports + " relating to filter property: " + propertyName ); } propertyValue.append( targetJsPath + "/" + propertyName ); fileDependencyProperties.setProperty( propertyName, propertyValue.toString() ); } } return fileDependencyProperties; } /** * @return property. */ public BuildContext getBuildContext() { return buildContext; } /** * @return property. */ public String getEncoding() { return encoding; } /** * @return property. */ public Map<String, LinkedHashSet<String>> getFileDependencies() { return fileDependencies; } /** * @return property. */ public String getHtmlResourceExtensions() { return htmlResourceExtensions; } /** * @return property. */ public ArtifactRepository getLocalRepository() { return localRepository; } /** * @return property. */ public MavenFileFilter getMavenFileFilter() { return mavenFileFilter; } /** * @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 htmlResourceExtensions set property. */ public void setHtmlResourceExtensions( String htmlResourceExtensions ) { this.htmlResourceExtensions = htmlResourceExtensions; } /** * @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 targetJsPath set property. */ public void setTargetJsPath( String targetJsPath ) { this.targetJsPath = targetJsPath; } }