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;
}
}