/**
* Copyright 2017 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed 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.
*/
package org.moditect.commands;
import java.io.File;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.moditect.internal.analyzer.ServiceLoaderUseScanner;
import org.moditect.internal.command.ProcessExecutor;
import org.moditect.internal.compiler.ModuleInfoCompiler;
import org.moditect.model.DependencyDescriptor;
import org.moditect.spi.log.Log;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.modules.ModuleDeclaration;
import com.github.javaparser.ast.modules.ModuleExportsStmt;
import com.github.javaparser.ast.modules.ModuleRequiresStmt;
import com.github.javaparser.ast.modules.ModuleUsesStmt;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
public class GenerateModuleInfo {
private final Path inputJar;
private final String moduleName;
private final Set<DependencyDescriptor> dependencies;
private List<Pattern> exportExcludes;
private final Path workingDirectory;
private final Path outputDirectory;
private final boolean addServiceUses;
private final Log log;
public GenerateModuleInfo(Path inputJar, String moduleName, Set<DependencyDescriptor> dependencies, List<Pattern> exportExcludes, Path workingDirectory, Path outputDirectory, boolean addServiceUses, Log log) {
this.inputJar = inputJar;
this.moduleName = moduleName;
this.dependencies = dependencies;
this.exportExcludes = exportExcludes;
this.workingDirectory = workingDirectory;
this.outputDirectory = outputDirectory;
this.addServiceUses = addServiceUses;
this.log = log;
}
public void run() {
if ( Files.isDirectory( inputJar ) ) {
throw new IllegalArgumentException( "Input JAR must not be a directory" );
}
if ( !Files.exists( workingDirectory ) ) {
throw new IllegalArgumentException( "Working directory doesn't exist: " + workingDirectory );
}
if ( !Files.exists( outputDirectory ) ) {
throw new IllegalArgumentException( "Output directory doesn't exist: " + outputDirectory );
}
Map<String, Boolean> optionalityPerModule = runJdeps();
ModuleDeclaration moduleDeclaration = parseGeneratedModuleInfo();
updateModuleInfo( optionalityPerModule, moduleDeclaration );
writeModuleInfo( moduleDeclaration );
}
private void updateModuleInfo(Map<String, Boolean> optionalityPerModule, ModuleDeclaration moduleDeclaration) {
List<ModuleRequiresStmt> requiresStatements = moduleDeclaration.getNodesByType( ModuleRequiresStmt.class );
for ( ModuleRequiresStmt moduleRequiresStmt : requiresStatements ) {
if ( Boolean.TRUE.equals( optionalityPerModule.get( moduleRequiresStmt.getNameAsString() ) ) ) {
moduleRequiresStmt.addModifier( Modifier.STATIC );
}
}
List<ModuleExportsStmt> exportStatements = moduleDeclaration.getNodesByType( ModuleExportsStmt.class );
for ( ModuleExportsStmt moduleExportsStmt : exportStatements ) {
if ( isExcluded( moduleExportsStmt ) ) {
moduleDeclaration.remove( moduleExportsStmt );
}
}
if ( moduleName != null ) {
moduleDeclaration.setName( moduleName );
}
if ( addServiceUses ) {
Set<String> usedServices = ServiceLoaderUseScanner.getUsedServices( inputJar );
for ( String usedService : usedServices ) {
moduleDeclaration.getModuleStmts().add( new ModuleUsesStmt( getType( usedService ) ) );
}
}
}
private ClassOrInterfaceType getType(String fqn) {
String[] parts = fqn.split( "\\." );
ClassOrInterfaceType scope = null;
String name = null;
if ( parts.length == 1 ) {
scope = null;
name = parts[0];
}
else {
ClassOrInterfaceType parentScope = null;
for( int i = 0; i < parts.length - 1; i++ ) {
scope = new ClassOrInterfaceType( parentScope, parts[i] );
parentScope = scope;
}
name = parts[parts.length - 1];
}
return new ClassOrInterfaceType( scope, name );
}
private boolean isExcluded(ModuleExportsStmt moduleExportsStmt) {
return exportExcludes.stream()
.anyMatch( exclude -> exclude.matcher( moduleExportsStmt.getNameAsString() ).matches() );
}
private Map<String, Boolean> runJdeps() throws AssertionError {
Map<String, Boolean> optionalityPerModule = new HashMap<>();
String javaHome = System.getProperty("java.home");
String jdepsBin = javaHome +
File.separator + "bin" +
File.separator + "jdeps";
List<String> command = new ArrayList<>();
command.add( jdepsBin );
command.add( "--generate-module-info" );
command.add( workingDirectory.toString() );
if ( !dependencies.isEmpty() ) {
StringBuilder modules = new StringBuilder();
StringBuilder modulePath = new StringBuilder();
boolean isFirst = true;
for ( DependencyDescriptor dependency : dependencies ) {
if ( isFirst ) {
isFirst = false;
}
else {
modules.append( "," );
modulePath.append( File.pathSeparator );
}
ModuleDescriptor descriptor = ModuleFinder.of( dependency.getPath() )
.findAll()
.iterator()
.next()
.descriptor();
modules.append( descriptor.name() );
optionalityPerModule.put( descriptor.name(), dependency.isOptional() );
modulePath.append( dependency.getPath() );
}
command.add( "--add-modules" );
command.add( modules.toString() );
command.add( "--module-path" );
command.add( modulePath.toString() );
}
command.add( inputJar.toString() );
log.debug( "Running jdeps: " + String.join( " ", command ) );
ProcessExecutor.run( "jdeps", command, log );
return optionalityPerModule;
}
private ModuleDeclaration parseGeneratedModuleInfo() {
String generatedModuleName = ModuleFinder.of( inputJar )
.findAll()
.iterator()
.next()
.descriptor()
.name();
Path moduleDir = workingDirectory.resolve( generatedModuleName );
Path moduleInfo = moduleDir.resolve( "module-info.java" );
return ModuleInfoCompiler.parseModuleInfo( moduleInfo );
}
private void writeModuleInfo(ModuleDeclaration moduleDeclaration) {
Path outputModuleInfo = recreateDirectory( outputDirectory, moduleDeclaration.getNameAsString() )
.resolve( "module-info.java" );
try {
Files.write( outputModuleInfo, moduleDeclaration.toString().getBytes() );
log.info( "Created module descriptor at " + outputModuleInfo );
}
catch (IOException e) {
throw new RuntimeException( "Couldn't write module-info.java", e );
}
}
private Path recreateDirectory(Path parent, String directoryName) {
Path dir = parent.resolve( directoryName );
try {
if ( Files.exists( dir ) ) {
Files.walkFileTree( dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete( file );
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete( dir );
return FileVisitResult.CONTINUE;
}
});
}
Files.createDirectory( dir );
}
catch (IOException e) {
throw new RuntimeException( "Couldn't recreate directory " + dir, e );
}
return dir;
}
}