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