/*
* Copyright 2014 Lukas Krejci
*
* 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.revapi.standalone;
import static java.util.Collections.emptyList;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositoryException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.RemoteRepository;
import org.jboss.dmr.ModelNode;
import org.jboss.forge.furnace.Furnace;
import org.jboss.forge.furnace.addons.Addon;
import org.jboss.forge.furnace.addons.AddonId;
import org.jboss.forge.furnace.impl.FurnaceImpl;
import org.jboss.forge.furnace.impl.addons.AddonRepositoryImpl;
import org.jboss.forge.furnace.manager.AddonManager;
import org.jboss.forge.furnace.manager.impl.AddonManagerImpl;
import org.jboss.forge.furnace.manager.maven.MavenContainer;
import org.jboss.forge.furnace.manager.request.InstallRequest;
import org.jboss.forge.furnace.util.Addons;
import org.revapi.API;
import org.revapi.AnalysisContext;
import org.revapi.Revapi;
import org.revapi.maven.utils.ArtifactResolver;
import org.revapi.maven.utils.ScopeDependencySelector;
import org.revapi.maven.utils.ScopeDependencyTraverser;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;
/**
* @author Lukas Krejci
* @since 0.1
*/
public final class Main {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(Main.class);
private static void usage(@Nullable String progName) {
if (progName == null) {
progName = "revapi.(sh|bat)";
}
String pad = "";
for (int i = 0; i < progName.length(); ++i) {
pad += " ";
}
System.out.println(progName +
" [-u|-h] -e <GAV>[,<GAV>]* -o <FILE>[,<FILE>]* -n <FILE>[,<FILE>]* [-s <FILE>[,<FILE>]*] [-t <FILE>[,<FILE>]*] [-D<CONFIG_OPTION>=<VALUE>]* [-c <FILE>[,<FILE>]*] [-r <DIR>]");
System.out.println();
System.out.println(pad + " -u");
System.out.println(pad + " -h");
System.out.println(pad + " --usage");
System.out.println(pad + " --help");
System.out.println(pad + " Prints this message and exits.");
System.out.println(pad + " -e");
System.out.println(pad + " --extensions=<GAV>[,<GAV>]*");
System.out.println(pad + " Comma-separated list of maven GAVs of revapi extensions.");
System.out.println(pad + " -o");
System.out.println(pad + " --old=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of files of the old version of API");
System.out.println(pad + " -a");
System.out.println(pad + " --old-gavs=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of GAVs of the old version of API");
System.out.println(pad + " -s");
System.out.println(pad + " --old-supplementary=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of files that supplement the old version of API");
System.out.println(pad + " -n");
System.out.println(pad + " --new=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of files of the new version of API");
System.out.println(pad + " -b");
System.out.println(pad + " --new-gavs=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of GAVs of the new version of API");
System.out.println(pad + " -t");
System.out.println(pad + " --new-supplementary=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of files that supplement the new version of API");
System.out.println(pad + " -D");
System.out
.println(pad + " A key-value pair representing a single configuration option of revapi or one of " +
"the loaded extensions");
System.out.println(pad + " -c");
System.out.println(pad + " --config-files=<FILE>[,<FILE>]*");
System.out.println(pad + " Comma-separated list of configuration files in JSON format.");
System.out.println(pad + " -d");
System.out.println(pad + " --cache-dir=<DIR>");
System.out.println(pad + " The location of local cache of extensions to use to locate artifacts. " +
"Defaults to 'extensions' directory under revapi installation dir.");
System.out.println();
System.out.println("You can specify the old API either using -o and -s where you specify the filesystem paths" +
" to the archives and supplementary archives respectively or you can use -a to specify the GAVs of the" +
" old API archives and the supplementary archives (i.e. their transitive dependencies) will be determined" +
" automatically using Maven. But you cannot do both obviously.");
System.out.println();
System.out.println("Of course you can do the same for the new version of the API by using -n and -t for file" +
" paths or -b for GAVs.");
}
@SuppressWarnings("unchecked")
public static void main(String[] args) throws Exception {
if (args.length < 2) {
usage(null);
System.exit(1);
}
//redirect logging to slf4j - furnace is using jul
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
Logger.getLogger("").setLevel(Level.INFO);
//and now continue
String scriptFileName = args[0];
String baseDir = args[1];
String[] realArgs = new String[args.length - 2];
System.arraycopy(args, 2, realArgs, 0, realArgs.length);
String[] extensionGAVs = null;
String[] oldArchivePaths = null;
String[] oldGavs = null;
String[] newArchivePaths = null;
String[] newGavs = null;
String[] oldSupplementaryArchivePaths = null;
String[] newSupplementaryArchivePaths = null;
Map<String, String> additionalConfigOptions = new HashMap<>();
String[] configFiles = null;
File cacheDir = new File(baseDir, "extensions");
LongOpt[] longOpts = new LongOpt[12];
longOpts[0] = new LongOpt("usage", LongOpt.NO_ARGUMENT, null, 'u');
longOpts[1] = new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h');
longOpts[2] = new LongOpt("extensions", LongOpt.REQUIRED_ARGUMENT, null, 'e');
longOpts[3] = new LongOpt("old", LongOpt.REQUIRED_ARGUMENT, null, 'o');
longOpts[4] = new LongOpt("new", LongOpt.REQUIRED_ARGUMENT, null, 'n');
longOpts[5] = new LongOpt("old-supplementary", LongOpt.REQUIRED_ARGUMENT, null, 's');
longOpts[6] = new LongOpt("new-supplementary", LongOpt.REQUIRED_ARGUMENT, null, 't');
longOpts[7] = new LongOpt("D", LongOpt.REQUIRED_ARGUMENT, null, 'D');
longOpts[8] = new LongOpt("config-files", LongOpt.REQUIRED_ARGUMENT, null, 'c');
longOpts[9] = new LongOpt("cache-dir", LongOpt.REQUIRED_ARGUMENT, null, 'd');
longOpts[10] = new LongOpt("old-gavs", LongOpt.REQUIRED_ARGUMENT, null, 'a');
longOpts[11] = new LongOpt("new-gavs", LongOpt.REQUIRED_ARGUMENT, null, 'b');
Getopt opts = new Getopt(scriptFileName, realArgs, "uhe:o:n:s:t:D:c:d:a:b:", longOpts);
int c;
while ((c = opts.getopt()) != -1) {
switch (c) {
case 'u':
case 'h':
usage(scriptFileName);
System.exit(0);
case 'e':
extensionGAVs = opts.getOptarg().split(",");
break;
case 'o':
oldArchivePaths = opts.getOptarg().split(",");
break;
case 'n':
newArchivePaths = opts.getOptarg().split(",");
break;
case 's':
oldSupplementaryArchivePaths = opts.getOptarg().split(",");
break;
case 't':
newSupplementaryArchivePaths = opts.getOptarg().split(",");
break;
case 'c':
configFiles = opts.getOptarg().split(",");
break;
case 'D':
String[] keyValue = opts.getOptarg().split("=");
additionalConfigOptions.put(keyValue[0], keyValue.length > 1 ? keyValue[1] : null);
break;
case 'd':
cacheDir = new File(opts.getOptarg());
break;
case 'a':
oldGavs = opts.getOptarg().split(",");
break;
case 'b':
newGavs = opts.getOptarg().split(",");
break;
case ':':
System.err.println("Argument required for option " +
(char) opts.getOptopt());
break;
case '?':
System.err.println("The option '" + (char) opts.getOptopt() +
"' is not valid");
System.exit(1);
break;
default:
System.err.println("getopt() returned " + c);
System.exit(1);
break;
}
}
if (extensionGAVs == null || (oldArchivePaths == null && oldGavs == null) ||
(newArchivePaths == null && newGavs == null)) {
usage(scriptFileName);
System.exit(1);
}
List<FileArchive> oldArchives = null;
List<FileArchive> newArchives = null;
List<FileArchive> oldSupplementaryArchives = null;
List<FileArchive> newSupplementaryArchives = null;
if (oldArchivePaths == null) {
ArchivesAndSupplementaryArchives res = convertGavs(oldGavs, "Old API Maven artifact");
oldArchives = res.archives;
oldSupplementaryArchives = res.supplementaryArchives;
} else {
oldArchives = convertPaths(oldArchivePaths, "Old API files");
oldSupplementaryArchives = oldSupplementaryArchivePaths == null ? emptyList() :
convertPaths(oldSupplementaryArchivePaths, "Old API supplementary files");
}
if (newArchivePaths == null) {
ArchivesAndSupplementaryArchives res = convertGavs(newGavs, "New API Maven artifact");
newArchives = res.archives;
newSupplementaryArchives = res.supplementaryArchives;
} else {
newArchives = convertPaths(newArchivePaths, "New API files");
newSupplementaryArchives = newSupplementaryArchivePaths == null ? emptyList() :
convertPaths(newSupplementaryArchivePaths, "New API supplementary files");
}
try {
run(cacheDir, extensionGAVs, oldArchives, oldSupplementaryArchives, newArchives,
newSupplementaryArchives, configFiles, additionalConfigOptions);
} catch (Exception e) {
e.printStackTrace();
}
System.exit(0);
}
private static void run(File cacheDir, String[] extensionGAVs, List<FileArchive> oldArchives,
List<FileArchive> oldSupplementaryArchives, List<FileArchive> newArchives,
List<FileArchive> newSupplementaryArchives, String[] configFiles, Map<String, String> additionalConfig)
throws Exception {
ExtensionResolver.init();
Furnace furnace = new FurnaceImpl();
furnace.addRepository(AddonRepositoryImpl.forDirectory(furnace, cacheDir));
furnace.startAsync();
try {
AddonManager manager = new AddonManagerImpl(furnace, new ExtensionResolver());
if (extensionGAVs != null) {
for (String gav : extensionGAVs) {
DefaultArtifact artifact = new DefaultArtifact(gav);
String ga = artifact.getGroupId() + ":" + artifact.getArtifactId();
String v = artifact.getBaseVersion();
InstallRequest request = manager.install(AddonId.from(ga, v));
request.perform();
}
}
Revapi.Builder builder = Revapi.builder();
for (Addon addon : furnace.getAddonRegistry().getAddons()) {
Addons.waitUntilStarted(addon);
builder.withAllExtensionsFrom(addon.getClassLoader());
}
AnalysisContext.Builder ctxBld = AnalysisContext.builder()
.withOldAPI(API.of(oldArchives).supportedBy(oldSupplementaryArchives).build())
.withNewAPI(API.of(newArchives).supportedBy(newSupplementaryArchives).build());
if (configFiles != null) {
for (String cf : configFiles) {
File f = new File(cf);
checkCanRead(f, "Configuration file");
try (FileInputStream is = new FileInputStream(f)) {
ctxBld.mergeConfigurationFromJSONStream(is);
}
}
}
for (Map.Entry<String, String> e : additionalConfig.entrySet()) {
String[] keyPath = e.getKey().split("\\.");
ModelNode additionalNode = new ModelNode();
ModelNode key = additionalNode.get(keyPath);
String value = e.getValue();
if (value.startsWith("[") && value.endsWith("]")) {
String[] values = value.substring(1, value.length() - 1).split("\\s*,\\s*");
for(String v : values) {
key.add(v);
}
} else {
key.set(value);
}
ctxBld.mergeConfiguration(additionalNode);
}
try (Revapi revapi = builder.withAllExtensionsFromThreadContextClassLoader().build()) {
revapi.analyze(ctxBld.build());
}
} finally {
furnace.stop();
}
}
private static List<FileArchive> convertPaths(String[] paths, String errorMessagePrefix) {
List<FileArchive> archives = new ArrayList<>(paths.length);
for (String path : paths) {
File f = new File(path);
checkCanRead(f, errorMessagePrefix);
archives.add(new FileArchive(f));
}
return archives;
}
private static ArchivesAndSupplementaryArchives convertGavs(String[] gavs, String errorMessagePrefix) {
MavenContainer mvn = new MavenContainer();
RepositorySystem repositorySystem = mvn.getRepositorySystem();
DefaultRepositorySystemSession session = mvn.setupRepoSession(repositorySystem, mvn.getSettings());
session.setDependencySelector(new ScopeDependencySelector("compile", "provided"));
session.setDependencyTraverser(new ScopeDependencyTraverser("compile", "provided"));
List<RemoteRepository> remoteRepositories = mvn.getEnabledRepositoriesFromProfile(mvn.getSettings());
//RemoteRepository local = new RemoteRepository.Builder("@@forced-local@@", "default", System.getenv("M2_HOME")).build();
RemoteRepository mavenCentral = new RemoteRepository.Builder("@@forced-maven-central@@", "default",
"http://repo.maven.apache.org/maven2/").build();
if (remoteRepositories.isEmpty()) {
//remoteRepositories.add(local);
remoteRepositories.add(mavenCentral);
}
ArtifactResolver resolver = new ArtifactResolver(repositorySystem, session, remoteRepositories);
List<FileArchive> archives = new ArrayList<>();
List<FileArchive> supplementaryArchives = new ArrayList<>();
for (String gav : gavs) {
try {
archives.add(new FileArchive(resolver.resolveArtifact(gav).getFile()));
ArtifactResolver.CollectionResult res = resolver.collectTransitiveDeps(gav);
res.getResolvedArtifacts().
forEach(a -> supplementaryArchives.add(new FileArchive(a.getFile())));
if (!res.getFailures().isEmpty()) {
LOG.warn("Failed to resolve some transitive dependencies: " + res.getFailures().toString());
}
} catch (RepositoryException e) {
throw new IllegalArgumentException(errorMessagePrefix + " " + e.getMessage());
}
}
return new ArchivesAndSupplementaryArchives(archives, supplementaryArchives);
}
private static void checkCanRead(File f, String errorMessagePrefix) throws IllegalArgumentException {
if (!f.exists()) {
throw new IllegalArgumentException(errorMessagePrefix + " '" + f.getAbsolutePath() + "' does not exist.");
}
if (!f.isFile() || !f.canRead()) {
throw new IllegalArgumentException(
errorMessagePrefix + " '" + f.getAbsolutePath() + "' is not a file or cannot be read.");
}
}
private static class ArchivesAndSupplementaryArchives {
final List<FileArchive> archives;
final List<FileArchive> supplementaryArchives;
public ArchivesAndSupplementaryArchives(List<FileArchive> archives,
List<FileArchive> supplementaryArchives) {
this.archives = archives;
this.supplementaryArchives = supplementaryArchives;
}
}
}