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.Map.Entry; import java.util.Set; import org.antlr.runtime.ANTLRFileStream; import org.antlr.runtime.CharStream; import org.antlr.runtime.CommonTokenStream; 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.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.model.Dependency; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.Scanner; 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 { /** * 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; /** * @parameter default-value="**\/*.js" * @required */ private String jsFileExtensions; /** * The project's artifact factory. * * @component */ private ArtifactFactory artifactFactory; /** * The project's meta data source. * * @component */ private ArtifactMetadataSource artifactMetadataSource; /** * 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 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, LinkedHashSet<File> processedFiles ) throws MojoExecutionException { boolean fileDependencyGraphUpdated = false; Scanner scanner = buildContext.newScanner( sourceJsFolder ); scanner.setIncludes( jsFileExtensions.split( "," ) ); scanner.scan(); String[] sources = scanner.getIncludedFiles(); for ( String source : sources ) { File sourceFile = new File( sourceJsFolder, source ); if ( processFileForImportsAndSymbols( sourceFile, fileDependencyGraphModificationTime, null ) ) { processedFiles.add( sourceFile ); getLog().info( "Processed: " + source ); fileDependencyGraphUpdated = true; } } return fileDependencyGraphUpdated; } /** * 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. * @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 ) throws MojoExecutionException { boolean fileDependencyGraphUpdated = false; String scopeStr = ( scope == Scope.COMPILE ? "compile" : "test" ); for ( Dependency dependency : dependencies ) { // Only process dependencies within the scope we're interested in. if ( !dependency.getScope().equals( scopeStr ) ) { continue; } // Process imports and symbols of this dependencies' transitives // first. ArtifactResolutionResult result; Artifact artifactToResolve = artifactFactory.createArtifactWithClassifier( dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), dependency.getType(), dependency.getClassifier() ); Set<Artifact> artifactsToResolve = new HashSet<Artifact>(); artifactsToResolve.add( artifactToResolve ); 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 ); } // Resolve just this dependencies' transitive dependencies first and // discount any that have been resolved previously. Set<?> transitiveArtifacts = result.getArtifacts(); transitiveArtifacts.removeAll( processedFiles ); transitiveArtifacts.remove( artifactToResolve ); LinkedHashSet<String> transitivesAsImports = new LinkedHashSet<String>( transitiveArtifacts.size() ); for ( Object transitiveArtifactObject : transitiveArtifacts ) { Artifact transitiveArtifact = (Artifact) transitiveArtifactObject; final File transtitiveArtifactFile = transitiveArtifact.getFile(); // Only process this dependency if we've not done so // already. if ( !processedFiles.contains( transtitiveArtifactFile ) ) { if ( processFileForImportsAndSymbols( transtitiveArtifactFile, fileDependencyGraphModificationTime, transitiveArtifacts ) ) { processedFiles.add( transtitiveArtifactFile ); fileDependencyGraphUpdated = true; } } // Add transitives to the artifacts set of dependencies - // as if they were @import statements themselves. transitivesAsImports.add( transtitiveArtifactFile.getPath() ); } // Now deal with the pom specified dependency. File artifactFile = artifactToResolve.getFile(); String artifactPath = artifactFile.getAbsolutePath(); // Process imports and symbols of this dependency if we've not // already done so. if ( !processedFiles.contains( artifactFile ) ) { if ( processFileForImportsAndSymbols( artifactFile, fileDependencyGraphModificationTime, null ) ) { processedFiles.add( artifactFile ); fileDependencyGraphUpdated = true; } } // 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; } /** * Perform the goal of this mojo. * * @param sourceJsFolder the folder where the source js 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. */ public void doExecute( File sourceJsFolder, 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 ); int fileDependencyGraphHashCode = fileDependencies.hashCode(); // 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, LinkedHashSet<String>> compileFileDependencies = new HashMap<String, LinkedHashSet<String>>(); FileDependencyPersistanceUtil.readFileDependencyGraph( new File( workFolder.getParentFile(), "main" ), compileFileDependencies, // compileFileAssignedGlobals ); } // Build dependency graph and symbol table against each js file declared // as a dependency. LinkedHashSet<File> processedFiles = new LinkedHashSet<File>(); boolean fileDependencyGraphUpdated = buildDependencyGraphForDependencies( scope, fileDependencyGraphModificationTime, processedFiles ); // Process all of our JS files and build their dependency // graphs and symbol tables. if ( buildDependencyGraphForChangedSourceFiles( fileDependencyGraphModificationTime, sourceJsFolder, 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 ) { 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 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 String getJsFileExtensions() { return jsFileExtensions; } /** * @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; } /** * 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 artifactFile 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 artifactFile, long fileDependencyGraphModificationTime, Set<?> transitiveArtifacts ) throws MojoExecutionException { String artifactPath = artifactFile.getPath(); // Quickly jump out if this particular artifact has not been updated // recently. if ( artifactFile.lastModified() <= fileDependencyGraphModificationTime ) { if ( getLog().isDebugEnabled() ) { getLog().info( "Skipping unchanged JS file: " + artifactPath ); } return false; } if ( getLog().isDebugEnabled() ) { getLog().info( "Parsing JS file: " + artifactPath ); } try { // Tokenise the JS file resulting in collections of assigned and // unassigned globals, and import statements. CharStream cs = new ANTLRFileStream( artifactFile.getPath() ); ECMAScriptLexer lexer = new ECMAScriptLexer( cs ); CommonTokenStream tokenStream = new CommonTokenStream(); tokenStream.setTokenSource( lexer ); tokenStream.getTokens(); 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, artifactPath ); } // 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() ); fileUnassignedGlobals.put( artifactPath, 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( artifactPath, 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: " + artifactPath ); } } } catch ( IOException e ) { throw new MojoExecutionException( "Problem opening file: " + artifactFile.getName(), 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 ); } 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 dependencies set property. */ public void setDependencies( List<Dependency> dependencies ) { this.dependencies = dependencies; } /** * @param jsFileExtensions set property. */ public void setJsFileExtensions( String jsFileExtensions ) { this.jsFileExtensions = jsFileExtensions; } /** * @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; } }