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.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonToken;
import org.antlr.runtime.CommonTokenStream;
import org.apache.commons.io.FileUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.metadata.ArtifactMetadataSource;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactCollector;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.artifact.resolver.filter.TypeArtifactFilter;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.sonatype.plexus.build.incremental.BuildContext;
/**
* Mojo for resolving dependencies either declared using an @import javadoc statement or by declaration of uninitialised
* variables.
*/
public abstract class AbstractImportMojo
extends AbstractMojo
{
/**
* Small structure that permits Artifacts to exist as keys in a hash.
*/
private class ArtifactId
{
private final String groupId;
private final String artifactId;
public ArtifactId( String groupId, String artifactId )
{
this.groupId = groupId;
this.artifactId = artifactId;
}
@Override
public boolean equals( Object obj )
{
if ( this == obj )
{
return true;
}
if ( obj == null )
{
return false;
}
if ( !( obj instanceof ArtifactId ) )
{
return false;
}
ArtifactId other = (ArtifactId) obj;
if ( !getOuterType().equals( other.getOuterType() ) )
{
return false;
}
if ( artifactId == null )
{
if ( other.artifactId != null )
{
return false;
}
}
else if ( !artifactId.equals( other.artifactId ) )
{
return false;
}
if ( groupId == null )
{
if ( other.groupId != null )
{
return false;
}
}
else if ( !groupId.equals( other.groupId ) )
{
return false;
}
return true;
}
private AbstractImportMojo getOuterType()
{
return AbstractImportMojo.this;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + ( ( artifactId == null ) ? 0 : artifactId.hashCode() );
result = prime * result + ( ( groupId == null ) ? 0 : groupId.hashCode() );
return result;
}
};
/**
* The current project scope.
*/
protected enum Scope
{
/** */
COMPILE,
/** */
TEST
}
/**
* The project.
*
* @parameter default-value="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The set of dependencies required by the project.
*
* @parameter default-value="${project.dependencies}"
* @required
* @readonly
*/
private List<Dependency> dependencies;
/**
* The local repository.
*
* @parameter default-value="${localRepository}"
* @required
* @readonly
*/
private ArtifactRepository localRepository;
/**
* The remote repositories.
*
* @parameter default-value="${project.remoteArtifactRepositories}"
* @required
* @readonly
*/
private List<?> remoteRepositories;
/**
* Files to include. Defaults to "**\/*.js".
*
* @parameter
*/
private List<String> includes;
/**
* Files to exclude. Nothing is excluded by default.
*
* @parameter
*/
private List<String> excludes;
/**
* true if the standard browser globals should be predefined. @see http://www.jslint.com/lint.html#browser TODO:
* Provide the other JSLint "assume" options.
*
* @parameter default-value="true"
* @required
*/
private boolean assumeABrowser;
/**
* The project's artifact factory.
*
* @component
*/
private ArtifactFactory artifactFactory;
/**
* The project's meta data source.
*
* @component
*/
private ArtifactMetadataSource artifactMetadataSource;
/**
* For determining the best versions of a set of artifacts.
*
* @component
*/
private ArtifactCollector artifactCollector;
/**
* The project's artifact resolver.
*
* @component
*/
private ArtifactResolver resolver;
/**
* A map of symbols to the script resource that they are defined in.
*/
private final Map<String, String> fileAssignedGlobals = new HashMap<String, String>();
/**
* A map of symbols to the script resource that they are defined in from a compile scope perspective.
*/
private final Map<String, String> compileFileAssignedGlobals = new HashMap<String, String>();
/**
* A map of symbols to the script resource that they are declared (but not initialised) in. This map is updated once
* per run for all of the parsed js files that are processed. For example if there are no files to process given
* that non have been updated since the last run then this map will be empty. If only one file was processed then
* this map will contain unassigned globals just for that one file. This approach results in
* processSourceFilesForUnassignedSymbolDeclarations to run efficiently as it is driven by this map.
*/
private final Map<String, Set<String>> fileUnassignedGlobals = new HashMap<String, Set<String>>();
/**
* A graph of filenames and their dependencies.
*/
private final Map<String, LinkedHashSet<String>> fileDependencies = new HashMap<String, LinkedHashSet<String>>();
/**
* The build context so that we can tell Maven certain files have changed if required.
*
* @component
*/
private BuildContext buildContext;
/**
* Build dependency graph from the source files.
*
* @param fileDependencyGraphModificationTime the time the graph read in was updated. Used for comparing file times.
* @param sourceJsFolder Where the source JS files live.
* @param targetFolder Where the target files live.
* @param processedFiles the files that have been processed as a consequence of this method.
* @return true if the graph has been updated by this method.
* @throws MojoExecutionException if something goes wrong.
*/
private boolean buildDependencyGraphForChangedSourceFiles( long fileDependencyGraphModificationTime,
File sourceJsFolder, File targetFolder,
LinkedHashSet<File> processedFiles )
throws MojoExecutionException
{
File targetJsFolder = new File( targetFolder, "js" );
boolean fileDependencyGraphUpdated = false;
FileCollector fileCollector = new FileCollector( buildContext, new String[] { "**/*.js" }, new String[] { "**/*.min.js" } );
for ( String source : fileCollector.collectPaths( sourceJsFolder, includes, excludes ) )
{
File sourceFile = new File( sourceJsFolder, source );
if ( processFileForImportsAndSymbols( sourceJsFolder, targetJsFolder, sourceFile,
fileDependencyGraphModificationTime, null ) )
{
processedFiles.add( sourceFile );
getLog().info( "Processed: " + source );
fileDependencyGraphUpdated = true;
}
}
try {
copyMinFilesToTarget(sourceJsFolder, targetJsFolder);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return fileDependencyGraphUpdated;
}
private void copyMinFilesToTarget(File sourceJsFolder, File targetJsFolder) throws MojoExecutionException, IOException {
FileCollector fileCollector = new FileCollector( buildContext, new String[] { "**/*.min.js" }, new String[] {});
for ( String source : fileCollector.collectPaths( sourceJsFolder, includes, excludes ) ) {
InputStream in = null;
OutputStream out = null;
try {
in =new FileInputStream(new File(sourceJsFolder.getPath() + "/" + source));
out = new FileOutputStream(new File(targetJsFolder.getPath() + "/" + source));
// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
/**
* Build up the dependency graph and global symbol table by parsing the project's dependencies.
*
* @param scope compile or test.
* @param fileDependencyGraphModificationTime the time that the dependency graph was updated. Used for file time
* comparisons to check the age of them.
* @param processedFiles an insert-ordered set of files that have been processed.
* @param targetFolder Where the target files live.
* @param workFolder Where we can create some long lived information that may be useful to subsequent builds.
* @param compileWorkFolder Ditto but in the case of testing it points to where the compile working folder is.
* @return true if the dependency graph has been updated.
* @throws MojoExecutionException if something bad happens.
*/
private boolean buildDependencyGraphForDependencies( Scope scope, long fileDependencyGraphModificationTime,
LinkedHashSet<File> processedFiles, File targetFolder,
File workFolder, File compileWorkFolder )
throws MojoExecutionException
{
File targetJsFolder = new File( targetFolder, "js" );
boolean fileDependencyGraphUpdated = false;
// Determine how we need to filter things both for direct filtering and transitive filtering.
String scopeStr = ( scope == Scope.COMPILE ? Artifact.SCOPE_COMPILE : Artifact.SCOPE_TEST );
AndArtifactFilter jsArtifactFilter = new AndArtifactFilter();
jsArtifactFilter.add( new ScopeArtifactFilter( scopeStr ) );
jsArtifactFilter.add( new TypeArtifactFilter( "js" ) );
AndArtifactFilter wwwZipArtifactFilter = new AndArtifactFilter();
wwwZipArtifactFilter.add( new ScopeArtifactFilter( scopeStr ) );
wwwZipArtifactFilter.add( new TypeArtifactFilter( "zip" ) );
wwwZipArtifactFilter.add( new ArtifactFilter()
{
public boolean include( Artifact artifact )
{
return artifact.hasClassifier() && artifact.getClassifier().equals( "www" );
}
} );
// Determine the artifacts to resolve and associate their transitive dependencies.
Map<Artifact, LinkedHashSet<Artifact>> directArtifactWithTransitives =
new HashMap<Artifact, LinkedHashSet<Artifact>>( dependencies.size() );
Set<Artifact> directArtifacts = new HashSet<Artifact>( dependencies.size() );
LinkedHashSet<Artifact> transitiveArtifacts = new LinkedHashSet<Artifact>();
for ( Dependency dependency : dependencies )
{
// Process imports and symbols of this dependencies' transitives
// first.
Artifact directArtifact =
artifactFactory.createDependencyArtifact( dependency.getGroupId(), dependency.getArtifactId(),
VersionRange.createFromVersion( dependency.getVersion() ),
dependency.getType(), dependency.getClassifier(),
dependency.getScope() );
if ( !jsArtifactFilter.include( directArtifact ) && !wwwZipArtifactFilter.include( directArtifact ) )
{
continue;
}
Set<Artifact> artifactsToResolve = new HashSet<Artifact>( 1 );
artifactsToResolve.add( directArtifact );
ArtifactResolutionResult result;
try
{
result =
resolver.resolveTransitively( artifactsToResolve, project.getArtifact(), remoteRepositories,
localRepository, artifactMetadataSource );
}
catch ( ArtifactResolutionException e )
{
throw new MojoExecutionException( "Problem resolving dependencies", e );
}
catch ( ArtifactNotFoundException e )
{
throw new MojoExecutionException( "Problem resolving dependencies", e );
}
// Associate the transitive dependencies with the direct dependency and aggregate all transitives for
// collection later.
LinkedHashSet<Artifact> directTransitiveArtifacts =
new LinkedHashSet<Artifact>( result.getArtifacts().size() );
for ( Object o : result.getArtifacts() )
{
Artifact resolvedArtifact = (Artifact) o;
if ( jsArtifactFilter.include( resolvedArtifact ) && //
!resolvedArtifact.equals( directArtifact ) )
{
directTransitiveArtifacts.add( resolvedArtifact );
}
}
directArtifacts.add( directArtifact );
transitiveArtifacts.addAll( directTransitiveArtifacts );
directArtifactWithTransitives.put( directArtifact, directTransitiveArtifacts );
}
// Resolve the best versions of the transitives to use by asking Maven to collect them.
Set<Artifact> collectedArtifacts = new HashSet<Artifact>( directArtifacts.size() + transitiveArtifacts.size() );
Map<ArtifactId, Artifact> indexedCollectedDependencies =
new HashMap<ArtifactId, Artifact>( collectedArtifacts.size() );
try
{
// Note that we must pass an insert-order set into the collector. The collector appears to assume that order
// is significant, even though it is undocumented.
LinkedHashSet<Artifact> collectableArtifacts = new LinkedHashSet<Artifact>( directArtifacts );
collectableArtifacts.addAll( transitiveArtifacts );
ArtifactResolutionResult resolutionResult =
artifactCollector.collect( collectableArtifacts, project.getArtifact(), localRepository,
remoteRepositories, artifactMetadataSource, null, //
Collections.EMPTY_LIST );
for ( Object o : resolutionResult.getArtifacts() )
{
Artifact collectedArtifact = (Artifact) o;
collectedArtifacts.add( collectedArtifact );
// Build up an index of of collected transitive dependencies so that we can we refer back to them as we
// process the direct dependencies.
ArtifactId collectedArtifactId =
new ArtifactId( collectedArtifact.getGroupId(), collectedArtifact.getArtifactId() );
indexedCollectedDependencies.put( collectedArtifactId, collectedArtifact );
}
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Dependencies collected: " + collectedArtifacts.toString() );
}
}
catch ( ArtifactResolutionException e )
{
throw new MojoExecutionException( "Cannot collect dependencies", e );
}
// Now go through direct artifacts and process their transitives.
LocalRepositoryCollector localRepositoryCollector =
new LocalRepositoryCollector( project, localRepository, new File[] {} );
for ( Entry<Artifact, LinkedHashSet<Artifact>> entry : directArtifactWithTransitives.entrySet() )
{
Artifact directArtifact = entry.getKey();
LinkedHashSet<Artifact> directArtifactTransitives = entry.getValue();
LinkedHashSet<String> transitivesAsImports = new LinkedHashSet<String>( directArtifactTransitives.size() );
for ( Object o : directArtifactTransitives )
{
Artifact directTransitiveArtifact = (Artifact) o;
// Get the transitive artifact that Maven decided was the best to use.
ArtifactId directTransitiveArtifactId =
new ArtifactId( directTransitiveArtifact.getGroupId(), directTransitiveArtifact.getArtifactId() );
Artifact transitiveArtifact = indexedCollectedDependencies.get( directTransitiveArtifactId );
List<File> transitiveArtifactFiles =
getArtifactFiles( transitiveArtifact, targetFolder, workFolder, compileWorkFolder,
localRepositoryCollector );
// Only process this dependency if we've not done so
// already.
for ( File transitiveArtifactFile : transitiveArtifactFiles )
{
if ( !processedFiles.contains( transitiveArtifactFile ) )
{
String localRepository =
localRepositoryCollector.findLocalRepository( transitiveArtifactFile.getAbsolutePath() );
if ( localRepository != null )
{
if ( processFileForImportsAndSymbols( new File( localRepository ), targetJsFolder,
transitiveArtifactFile,
fileDependencyGraphModificationTime,
directArtifactTransitives ) )
{
processedFiles.add( transitiveArtifactFile );
fileDependencyGraphUpdated = true;
}
}
else
{
throw new MojoExecutionException(
"Problem determining local repository for transitive file: "
+ transitiveArtifactFile );
}
}
// Add transitives to the artifacts set of dependencies -
// as if they were @import statements themselves.
transitivesAsImports.add( transitiveArtifactFile.getPath() );
}
}
// Now deal with the pom specified dependency.
List<File> artifactFiles =
getArtifactFiles( directArtifact, targetFolder, workFolder, compileWorkFolder, localRepositoryCollector );
for ( File artifactFile : artifactFiles )
{
String artifactPath = artifactFile.getAbsolutePath();
// Process imports and symbols of this dependency if we've not
// already done so.
if ( !processedFiles.contains( artifactFile ) )
{
String localRepository =
localRepositoryCollector.findLocalRepository( artifactFile.getAbsolutePath() );
if ( localRepository != null )
{
if ( processFileForImportsAndSymbols( new File( localRepository ), targetJsFolder,
artifactFile, fileDependencyGraphModificationTime, null ) )
{
processedFiles.add( artifactFile );
fileDependencyGraphUpdated = true;
}
}
else
{
throw new MojoExecutionException( "Problem determining local repository for file: "
+ artifactFile );
}
}
// Add in our transitives to the dependency graph if they're not
// already there.
LinkedHashSet<String> existingImports = fileDependencies.get( artifactPath );
if ( existingImports.addAll( transitivesAsImports ) )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Using transitives as import: " + transitivesAsImports + " for file: "
+ artifactPath );
}
fileDependencyGraphUpdated = true;
}
}
}
return fileDependencyGraphUpdated;
}
/**
* Convenience method for interacting with a JsFileArtifactHandler object.
*
* @param artifact the artifact to use.
* @param targetFolder the target folder.
* @param workFolder the work folder.
* @param compileWorkFolder Ditto but in the case of test scope this will point to the work folder for compile
* artifacts.
* @param localRepositoryCollector the repository collector.
* @return the list of files associated with the artifact.
* @throws MojoExecutionException if something goes wrong.
*/
private List<File> getArtifactFiles( Artifact artifact, File targetFolder, File workFolder, File compileWorkFolder,
LocalRepositoryCollector localRepositoryCollector )
throws MojoExecutionException
{
JsFileArtifactHandler handler;
try
{
boolean scopeCompile;
if ( artifact.getScope() == null )
{
scopeCompile = true;
}
else if ( artifact.getScope().equals( Artifact.SCOPE_COMPILE ) )
{
scopeCompile = true;
}
else
{
scopeCompile = false;
}
handler = new JsFileArtifactHandler( artifact, targetFolder, scopeCompile ? compileWorkFolder : workFolder );
File expansionFolder = handler.getExpansionFolder();
if ( expansionFolder != null )
{
localRepositoryCollector.addLocalRepositoryPath( expansionFolder.getAbsolutePath() );
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Cannot get js files from transitive artifact", e );
}
return handler.getFiles();
}
/**
* Perform the goal of this mojo.
*
* @param sourceJsFolder the folder where the source js files reside.
* @param targetFolder the folder where the target files reside.
* @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.
*/
protected void doExecute( File sourceJsFolder, File targetFolder, File workFolder, Scope scope )
throws MojoExecutionException
{
// Load in any existing dependency graph - we only build what we need
// to.
long fileDependencyGraphModificationTime =
FileDependencyPersistanceUtil.readFileDependencyGraph( workFolder, fileDependencies, fileAssignedGlobals );
// Build hashes of the dependency graph so that we can quickly compare whether they have changed.
int fileDependencyGraphHashCode = fileDependencies.hashCode();
int fileAssignedGlobalsHashCode = fileAssignedGlobals.hashCode();
// If we are in test scope then also load in the compile scoped dependency information as we need to resolve
// against this also.
File compileWorkFolder;
if ( scope == Scope.TEST )
{
compileWorkFolder = new File( workFolder.getParentFile(), "main" );
Map<String, LinkedHashSet<String>> compileFileDependencies = new HashMap<String, LinkedHashSet<String>>();
FileDependencyPersistanceUtil.readFileDependencyGraph( compileWorkFolder, compileFileDependencies, //
compileFileAssignedGlobals );
}
else
{
compileWorkFolder = workFolder;
}
// Keep a flag to signal whether the graph has been updated.
boolean fileDependencyGraphUpdated;
// Clear the dependency graph if it is invalid.
if ( !isDependencyGraphValid() )
{
fileDependencies.clear();
fileAssignedGlobals.clear();
fileDependencyGraphModificationTime = 0L;
fileDependencyGraphUpdated = true;
}
else
{
fileDependencyGraphUpdated = false;
}
// Build dependency graph and symbol table against each js file declared
// as a dependency.
LinkedHashSet<File> processedFiles = new LinkedHashSet<File>();
if ( buildDependencyGraphForDependencies( scope, fileDependencyGraphModificationTime, processedFiles,
targetFolder, workFolder, compileWorkFolder ) )
{
fileDependencyGraphUpdated = true;
}
// Process all of our JS files and build their dependency
// graphs and symbol tables.
if ( buildDependencyGraphForChangedSourceFiles( fileDependencyGraphModificationTime, sourceJsFolder,
targetFolder, processedFiles ) )
{
fileDependencyGraphUpdated = true;
}
// Given that we now have all of the symbols mapped by file we
// now need to go through our artifacts (dependencies and source files)
// again looking for those that reference them. We add to the file
// dependencies as a result.
processSourceFilesForUnassignedSymbolDeclarations();
// We have have a complete dependency graph. We will now persist the
// graph so that other phases can utilise it (if things have truly changed).
if ( fileDependencyGraphUpdated && ( fileDependencies.hashCode() != fileDependencyGraphHashCode || //
fileAssignedGlobals.hashCode() != fileAssignedGlobalsHashCode ) )
{
FileDependencyPersistanceUtil.writeFileDependencyGraph( workFolder, fileDependencies, fileAssignedGlobals );
}
}
/**
* @return property.
*/
public org.apache.maven.artifact.factory.ArtifactFactory getArtifactFactory()
{
return artifactFactory;
}
/**
* @return property.
*/
public ArtifactMetadataSource getArtifactMetadataSource()
{
return artifactMetadataSource;
}
/**
* @return property.
*/
public List<Dependency> getDependencies()
{
return dependencies;
}
/**
* @return property.
*/
public List<String> getExcludes()
{
return excludes;
}
/**
* @return property.
*/
public Map<String, String> getFileAssignedGlobals()
{
return fileAssignedGlobals;
}
/**
* @return property.
*/
public Map<String, LinkedHashSet<String>> getFileDependencies()
{
return fileDependencies;
}
/**
* @return property.
*/
public Map<String, Set<String>> getFileUnassignedGlobals()
{
return fileUnassignedGlobals;
}
/**
* @return property.
*/
public List<String> getIncludes()
{
return includes;
}
/**
* @return property.
*/
public ArtifactRepository getLocalRepository()
{
return localRepository;
}
/**
* @return property.
*/
public MavenProject getProject()
{
return project;
}
/**
* @return property.
*/
public List<?> getRemoteRepositories()
{
return remoteRepositories;
}
/**
* @return property.
*/
public ArtifactResolver getResolver()
{
return resolver;
}
/**
* @return property.
*/
public boolean isAssumeABrowser()
{
return assumeABrowser;
}
/**
* Check the integrity of the dependency graph. Essentially if any files that were there in a previous run but are
* no longer there then we must rebuild the graph.
*/
private boolean isDependencyGraphValid()
{
boolean valid = true;
for ( String fileDependencyFilename : fileDependencies.keySet() )
{
File fileDependencyFile = new File( fileDependencyFilename );
if ( !fileDependencyFile.exists() )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Found a file that has been removed so rebuilding graph. File: "
+ fileDependencyFilename );
}
valid = false;
break;
}
}
return valid;
}
/**
* Match a group and artifact against our list of dependencies.
*
* @param groupId the group id.
* @param artifactId the artifact id.
* @return the dependency or null if one cannot be found.
*/
private Dependency matchDirectDependency( String groupId, String artifactId )
{
Dependency dependencyFound = null;
for ( Dependency dependency : dependencies )
{
if ( dependency.getGroupId().equalsIgnoreCase( groupId )
&& dependency.getArtifactId().equalsIgnoreCase( artifactId )
&& dependency.getType().equalsIgnoreCase( "js" ) )
{
dependencyFound = dependency;
break;
}
}
return dependencyFound;
}
/**
* Find a dependency in our set of transitive dependencies.
*
* @param groupId the group to match.
* @param artifactId the artifact to match.
* @param transitiveArtifacts artifacts to match against.
* @return an artifact that has been matched or null if none can be found.
*/
private Artifact matchTransitiveDependency( String groupId, String artifactId, Set<?> transitiveArtifacts )
{
Artifact artifactFound = null;
for ( Object transitiveArtifactObject : transitiveArtifacts )
{
Artifact transitiveArtifact = (Artifact) transitiveArtifactObject;
if ( transitiveArtifact.getGroupId().equalsIgnoreCase( groupId )
&& transitiveArtifact.getArtifactId().equalsIgnoreCase( artifactId )
&& transitiveArtifact.getType().equalsIgnoreCase( "js" ) )
{
artifactFound = transitiveArtifact;
break;
}
}
return artifactFound;
}
/**
* Process a file for import declarations and for the symbols used.
*
* @param sourceFolder the base directory of the file being processed.
* @param targetFolder where to write files to.
* @param sourceFile the file to process.
* @param fileDependencyGraphModificationTime the last time the dependency graph was updated or 0 if we do not have
* one.
* @param transitiveArtifacts any transititive artifacts to match imports against, or null if no matching is to be
* done.
* @return true if processing occurred.
* @throws MojoExecutionException if something goes wrong.
*/
protected boolean processFileForImportsAndSymbols( File sourceFolder, File targetFolder, File sourceFile,
long fileDependencyGraphModificationTime,
Set<?> transitiveArtifacts )
throws MojoExecutionException
{
URI sourceFileRelUri = sourceFolder.toURI().relativize( sourceFile.toURI() );
File targetFile = new File( targetFolder, sourceFileRelUri.toString() );
String sourceFilePath = sourceFile.getAbsolutePath();
// Quickly jump out if this particular artifact has not been updated
// recently, or we don't have an entry for it in our dependency graph. The latter can happen if we build a multi
// module project from the parent folder and then build a specific module from its own folder. File dependencies
// in these scenarios can come from the module's target folder or the local m2 repo respectively.
if ( sourceFile.lastModified() <= fileDependencyGraphModificationTime
&& fileDependencies.containsKey( sourceFilePath ) )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Skipping unchanged JS file: " + sourceFileRelUri );
}
return false;
}
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Parsing JS file: " + sourceFileRelUri );
}
try
{
// Tokenise the JS file resulting in collections of assigned and
// unassigned globals, and import statements.
CharStream cs = new ANTLRFileStream( sourceFilePath );
ECMAScriptLexer lexer = new ECMAScriptLexer( cs );
lexer.setSourceFile( sourceFileRelUri, sourceFile.getName() );
CommonTokenStream tokenStream = new CommonTokenStream();
tokenStream.setTokenSource( lexer );
writeTokenStream( cs, tokenStream, targetFile );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Assigned globals: " + lexer.getAssignedGlobalVars().toString() );
getLog().debug( "Unassigned globals: " + lexer.getUnassignedGlobalVars().toString() );
getLog().debug( "Imports: " + lexer.getImportGavs().toString() );
}
// For each assigned variable map it against the file we're dealing
// with for later reference. An assigned variable indicates that
// unassigned declarations of the same variable want this file
// imported.
for ( String assignedGlobalVar : lexer.getAssignedGlobalVars() )
{
fileAssignedGlobals.put( assignedGlobalVar, sourceFilePath );
}
// For each unassigned variable map it against the file we're
// dealing with for later reference. An unassigned variable
// indicates that we want to import a file where the variable is
// assigned.
Set<String> vars = new HashSet<String>( lexer.getUnassignedGlobalVars() );
if ( assumeABrowser )
{
// If we assume a browser then take out all of the ones declared for it. Otherwise we'll be looking for
// dependencies that do not belong to the project.
vars.removeAll( Arrays.asList( new String[] { "clearInterval", "clearTimeout", "document", "exports",
"event", "frames", "history", "Image", "location", "module", "name", "navigator", "Option",
"parent", "require", "screen", "setInterval", "setTimeout", "window", "XMLHttpRequest" } ) );
}
fileUnassignedGlobals.put( sourceFilePath, vars );
// For each import found resolve its file name and then note it as a
// dependency of this particular js file. We update any existing
// dependency edges if they exist given that we've determined this
// part of the graph needs re-construction.
LinkedHashSet<String> importedDependencies = new LinkedHashSet<String>( lexer.getImportGavs().size() );
fileDependencies.put( sourceFilePath, importedDependencies );
for ( ECMAScriptLexer.GAV importGav : lexer.getImportGavs() )
{
Artifact artifactFound;
Dependency dependencyFound = matchDirectDependency( importGav.groupId, importGav.artifactId );
if ( dependencyFound == null )
{
if ( transitiveArtifacts != null )
{
artifactFound =
matchTransitiveDependency( importGav.groupId, importGav.artifactId, transitiveArtifacts );
}
else
{
artifactFound = null;
}
if ( artifactFound == null )
{
getLog().error( "Dependency not found: " + importGav.groupId + ":" + importGav.artifactId );
throw new MojoExecutionException( "Build stopping given dependency issue." );
}
}
else
{
artifactFound = resolveArtifact( dependencyFound );
}
/**
* Store the dependency as an edge against our dependency graph.
*/
importedDependencies.add( artifactFound.getFile().getPath() );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Found import: " + importGav.groupId + ":" + importGav.artifactId + " ("
+ artifactFound.getFile().getName() + ") for file: " + sourceFileRelUri );
}
}
}
catch ( IOException e )
{
throw new MojoExecutionException( "Problem opening file: " + sourceFileRelUri, e );
}
return true;
}
/**
* Go through all of unassigned globals and enhance the file dependencies collection given the file that they are
* declared in.
*
* @throws MojoExecutionException if something goes wrong.
*/
protected void processSourceFilesForUnassignedSymbolDeclarations()
throws MojoExecutionException
{
// For all of the js files containing unassigned vars...
Set<Entry<String, Set<String>>> entrySet = fileUnassignedGlobals.entrySet();
for ( Entry<String, Set<String>> entry : entrySet )
{
// For each of the unassigned vars...
String variableDeclFile = entry.getKey();
for ( String variableName : entry.getValue() )
{
// Resolve the file that contains the var's assignment and throw
// an exception if it cannot be found.
String variableAssignedFile = fileAssignedGlobals.get( variableName );
if ( variableAssignedFile == null && compileFileAssignedGlobals != null )
{
variableAssignedFile = compileFileAssignedGlobals.get( variableName );
}
// We've tried pretty hard, but we can't find a dependency. Time to barf.
if ( variableAssignedFile == null )
{
getLog().error( "Dependency not found: " + variableName + " in file: " + variableDeclFile );
throw new MojoExecutionException( "Build stopping given dependency issue." );
}
// Enhance the declaring file's graph of dependencies.
LinkedHashSet<String> variableDeclFileImports = fileDependencies.get( variableDeclFile );
if ( variableDeclFileImports == null )
{
variableDeclFileImports = new LinkedHashSet<String>();
fileDependencies.put( variableDeclFile, variableDeclFileImports );
}
variableDeclFileImports.add( variableAssignedFile );
}
}
}
/**
* Resolve an artifact given a dependency.
*
* @param dependency the dependency to resolve.
* @return the artifact.
* @throws MojoExecutionException if the dependency cannot be resolved.
*/
private Artifact resolveArtifact( Dependency dependency )
throws MojoExecutionException
{
Artifact artifact =
artifactFactory.createArtifactWithClassifier( dependency.getGroupId(), dependency.getArtifactId(),
dependency.getVersion(), dependency.getType(),
dependency.getClassifier() );
try
{
resolver.resolve( artifact, remoteRepositories, localRepository );
}
catch ( ArtifactResolutionException e )
{
throw new MojoExecutionException( dependency.toString(), e );
}
catch ( ArtifactNotFoundException e )
{
throw new MojoExecutionException( dependency.toString(), e );
}
return artifact;
}
/**
* @param artifactFactory set property.
*/
public void setArtifactFactory( ArtifactFactory artifactFactory )
{
this.artifactFactory = artifactFactory;
}
/**
* @param artifactMetadataSource set property.
*/
public void setArtifactMetadataSource( ArtifactMetadataSource artifactMetadataSource )
{
this.artifactMetadataSource = artifactMetadataSource;
}
/**
* @param assumeABrowser set property.
*/
public void setAssumeABrowser( boolean assumeABrowser )
{
this.assumeABrowser = assumeABrowser;
}
/**
* @param dependencies set property.
*/
public void setDependencies( List<Dependency> dependencies )
{
this.dependencies = dependencies;
}
/**
* @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 project set property.
*/
public void setProject( MavenProject project )
{
this.project = project;
}
/**
* @param remoteRepositories set property.
*/
public void setRemoteRepositories( List<?> remoteRepositories )
{
this.remoteRepositories = remoteRepositories;
}
/**
* @param resolver set property.
*/
public void setResolver( ArtifactResolver resolver )
{
this.resolver = resolver;
}
private void writeTokenStream( CharStream cs, CommonTokenStream tokenStream, File outputFile )
throws IOException
{
OutputStream os = new BufferedOutputStream( FileUtils.openOutputStream( outputFile ) );
try
{
List<?> tokens = tokenStream.getTokens();
cs.seek( 0 );
for ( Object tokenObject : tokens )
{
CommonToken token = (CommonToken) tokenObject;
if ( token.getType() == ECMAScriptLexer.MODULE_DECL || token.getType() == ECMAScriptLexer.REQUIRE_DECL )
{
int startIndex = token.getStartIndex();
while ( cs.index() < startIndex )
{
int streamChar = cs.LA( 1 );
if ( streamChar == CharStream.EOF )
{
break;
}
os.write( streamChar );
cs.consume();
}
CharacterIterator iter = new StringCharacterIterator( token.getText() );
for ( char tokenChar = iter.first(); tokenChar != CharacterIterator.DONE; tokenChar = iter.next() )
{
os.write( tokenChar );
}
cs.seek( token.getStopIndex() + 1 );
}
}
int streamChar;
while ( ( streamChar = cs.LA( 1 ) ) != CharStream.EOF )
{
os.write( streamChar );
cs.consume();
}
}
finally
{
os.close();
}
}
}