blob: 649e53e0a2c5989c4060fe4ce004f04d4d64efb2 [file] [log] [blame]
/*
* Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.jdeps;
import static com.sun.tools.jdeps.Analyzer.Type.*;
import static com.sun.tools.jdeps.JdepsWriter.*;
import static com.sun.tools.jdeps.ModuleAnalyzer.Graph;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.module.ResolutionException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Implementation for the jdeps tool for static class dependency analysis.
*/
class JdepsTask {
static class BadArgs extends Exception {
static final long serialVersionUID = 8765093759964640721L;
BadArgs(String key, Object... args) {
super(JdepsTask.getMessage(key, args));
this.key = key;
this.args = args;
}
BadArgs showUsage(boolean b) {
showUsage = b;
return this;
}
final String key;
final Object[] args;
boolean showUsage;
}
static abstract class Option {
Option(boolean hasArg, String... aliases) {
this.hasArg = hasArg;
this.aliases = aliases;
}
boolean isHidden() {
return false;
}
boolean matches(String opt) {
for (String a : aliases) {
if (a.equals(opt))
return true;
if (hasArg && opt.startsWith(a + "="))
return true;
}
return false;
}
boolean ignoreRest() {
return false;
}
abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
final boolean hasArg;
final String[] aliases;
}
static abstract class HiddenOption extends Option {
HiddenOption(boolean hasArg, String... aliases) {
super(hasArg, aliases);
}
boolean isHidden() {
return true;
}
}
static Option[] recognizedOptions = {
new Option(false, "-h", "-?", "-help") {
void process(JdepsTask task, String opt, String arg) {
task.options.help = true;
}
},
new Option(true, "-dotoutput") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Path p = Paths.get(arg);
if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
throw new BadArgs("err.invalid.path", arg);
}
task.options.dotOutputDir = arg;
}
},
new Option(false, "-s", "-summary") {
void process(JdepsTask task, String opt, String arg) {
task.options.showSummary = true;
task.options.verbose = SUMMARY;
}
},
new Option(false, "-v", "-verbose",
"-verbose:package",
"-verbose:class") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
switch (opt) {
case "-v":
case "-verbose":
task.options.verbose = VERBOSE;
task.options.filterSameArchive = false;
task.options.filterSamePackage = false;
break;
case "-verbose:package":
task.options.verbose = PACKAGE;
break;
case "-verbose:class":
task.options.verbose = CLASS;
break;
default:
throw new BadArgs("err.invalid.arg.for.option", opt);
}
}
},
new Option(true, "-p", "-package") {
void process(JdepsTask task, String opt, String arg) {
task.options.packageNames.add(arg);
}
},
new Option(true, "-e", "-regex") {
void process(JdepsTask task, String opt, String arg) {
task.options.regex = Pattern.compile(arg);
}
},
new Option(true, "-module") {
void process(JdepsTask task, String opt, String arg) {
task.options.requires.add(arg);
}
},
new Option(true, "-f", "-filter") {
void process(JdepsTask task, String opt, String arg) {
task.options.filterRegex = Pattern.compile(arg);
}
},
new Option(false, "-filter:package",
"-filter:archive", "-filter:module",
"-filter:none") {
void process(JdepsTask task, String opt, String arg) {
switch (opt) {
case "-filter:package":
task.options.filterSamePackage = true;
task.options.filterSameArchive = false;
break;
case "-filter:archive":
case "-filter:module":
task.options.filterSameArchive = true;
task.options.filterSamePackage = false;
break;
case "-filter:none":
task.options.filterSameArchive = false;
task.options.filterSamePackage = false;
break;
}
}
},
new Option(true, "-include") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.includePattern = Pattern.compile(arg);
}
},
new Option(false, "-P", "-profile") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.showProfile = true;
task.options.showModule = false;
}
},
new Option(false, "-M") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.showModule = true;
task.options.showProfile = false;
}
},
new Option(false, "-apionly") {
void process(JdepsTask task, String opt, String arg) {
task.options.apiOnly = true;
}
},
new Option(false, "-R", "-recursive") {
void process(JdepsTask task, String opt, String arg) {
task.options.depth = 0;
// turn off filtering
task.options.filterSameArchive = false;
task.options.filterSamePackage = false;
}
},
new Option(true, "-genmoduleinfo") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Path p = Paths.get(arg);
if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) {
throw new BadArgs("err.invalid.path", arg);
}
task.options.genModuleInfo = arg;
}
},
new Option(false, "-ct", "-compile-time") {
void process(JdepsTask task, String opt, String arg) {
task.options.compileTimeView = true;
task.options.filterSamePackage = true;
task.options.filterSameArchive = true;
task.options.depth = 0;
}
},
new Option(false, "-jdkinternals") {
void process(JdepsTask task, String opt, String arg) {
task.options.findJDKInternals = true;
task.options.verbose = CLASS;
if (task.options.includePattern == null) {
task.options.includePattern = Pattern.compile(".*");
}
}
},
new Option(true, "-cp", "-classpath") {
void process(JdepsTask task, String opt, String arg) {
task.options.classpath = arg;
}
},
new Option(true, "-mp", "-modulepath") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.modulePath = arg;
task.options.showModule = true;
}
},
new Option(true, "-upgrademodulepath") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.upgradeModulePath = arg;
task.options.showModule = true;
}
},
new Option(true, "-m") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.rootModule = arg;
task.options.includes.add(arg);
task.options.showModule = true;
}
},
new Option(false, "-check") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
task.options.checkModuleDeps = true;
}
},
new HiddenOption(true, "-include-modules") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Arrays.stream(arg.split(","))
.forEach(task.options.includes::add);
task.options.showModule = true;
}
},
new HiddenOption(true, "-exclude-modules") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
Arrays.stream(arg.split(","))
.forEach(task.options.excludes::add);
task.options.showModule = true;
}
},
new Option(false, "-q", "-quiet") {
void process(JdepsTask task, String opt, String arg) {
task.options.nowarning = true;
}
},
new Option(false, "-version") {
void process(JdepsTask task, String opt, String arg) {
task.options.version = true;
}
},
new HiddenOption(false, "-fullversion") {
void process(JdepsTask task, String opt, String arg) {
task.options.fullVersion = true;
}
},
new HiddenOption(false, "-showlabel") {
void process(JdepsTask task, String opt, String arg) {
task.options.showLabel = true;
}
},
new HiddenOption(true, "-depth") {
void process(JdepsTask task, String opt, String arg) throws BadArgs {
try {
task.options.depth = Integer.parseInt(arg);
} catch (NumberFormatException e) {
throw new BadArgs("err.invalid.arg.for.option", opt);
}
}
},
};
private static final String PROGNAME = "jdeps";
private final Options options = new Options();
private final List<String> classes = new ArrayList<>();
private PrintWriter log;
void setLog(PrintWriter out) {
log = out;
}
/**
* Result codes.
*/
static final int EXIT_OK = 0, // Completed with no errors.
EXIT_ERROR = 1, // Completed but reported errors.
EXIT_CMDERR = 2, // Bad command-line arguments
EXIT_SYSERR = 3, // System error or resource exhaustion.
EXIT_ABNORMAL = 4;// terminated abnormally
int run(String[] args) {
if (log == null) {
log = new PrintWriter(System.out);
}
try {
handleOptions(args);
if (options.help) {
showHelp();
}
if (options.version || options.fullVersion) {
showVersion(options.fullVersion);
}
if (options.rootModule != null && !classes.isEmpty()) {
reportError("err.invalid.module.option", options.rootModule, classes);
return EXIT_CMDERR;
}
if (options.checkModuleDeps && options.rootModule == null) {
reportError("err.root.module.not.set");
return EXIT_CMDERR;
}
if (classes.isEmpty() && options.rootModule == null && options.includePattern == null) {
if (options.help || options.version || options.fullVersion) {
return EXIT_OK;
} else {
showHelp();
return EXIT_CMDERR;
}
}
if (options.genModuleInfo != null) {
if (options.dotOutputDir != null || !options.classpath.isEmpty() || options.hasFilter()) {
showHelp();
return EXIT_CMDERR;
}
// default to compile time view analysis
options.compileTimeView = true;
for (String fn : classes) {
Path p = Paths.get(fn);
if (!Files.exists(p) || !fn.endsWith(".jar")) {
reportError("err.genmoduleinfo.not.jarfile", fn);
return EXIT_CMDERR;
}
}
}
if (options.numFilters() > 1) {
reportError("err.invalid.filters");
return EXIT_CMDERR;
}
if ((options.findJDKInternals) && (options.hasFilter() || options.showSummary)) {
showHelp();
return EXIT_CMDERR;
}
if (options.showSummary && options.verbose != SUMMARY) {
showHelp();
return EXIT_CMDERR;
}
boolean ok = run();
return ok ? EXIT_OK : EXIT_ERROR;
} catch (BadArgs e) {
reportError(e.key, e.args);
if (e.showUsage) {
log.println(getMessage("main.usage.summary", PROGNAME));
}
return EXIT_CMDERR;
} catch (ResolutionException e) {
reportError("err.exception.message", e.getMessage());
return EXIT_CMDERR;
} catch (IOException e) {
e.printStackTrace();
return EXIT_ABNORMAL;
} finally {
log.flush();
}
}
private ModulePaths modulePaths;
private boolean run() throws BadArgs, IOException {
DependencyFinder dependencyFinder =
new DependencyFinder(options.compileTimeView);
buildArchive(dependencyFinder);
if (options.rootModule != null &&
(options.checkModuleDeps || (options.dotOutputDir != null &&
options.verbose == SUMMARY))) {
// -dotfile -s prints the configuration of the given root
// -checkModuleDeps prints the suggested module-info.java
return analyzeModules(dependencyFinder);
}
// otherwise analyze the dependencies
if (options.genModuleInfo != null) {
return genModuleInfo(dependencyFinder);
} else {
return analyzeDeps(dependencyFinder);
}
}
private void buildArchive(DependencyFinder dependencyFinder)
throws BadArgs, IOException
{
// If -genmoduleinfo is specified, the input arguments must be JAR files
// Treat them as automatic modules for analysis
List<Path> jarfiles = options.genModuleInfo != null
? classes.stream().map(Paths::get)
.collect(Collectors.toList())
: Collections.emptyList();
// Set module paths
this.modulePaths = new ModulePaths(options.upgradeModulePath, options.modulePath, jarfiles);
// add modules to dependency finder for analysis
Map<String, Module> modules = modulePaths.getModules();
modules.values().stream()
.forEach(dependencyFinder::addModule);
// If -m option is set, add the specified module and its transitive dependences
// to the root set
if (options.rootModule != null) {
modulePaths.dependences(options.rootModule)
.forEach(dependencyFinder::addRoot);
}
// check if any module specified in -requires is missing
Optional<String> req = options.requires.stream()
.filter(mn -> !modules.containsKey(mn))
.findFirst();
if (req.isPresent()) {
throw new BadArgs("err.module.not.found", req.get());
}
// classpath
for (Path p : getClassPaths(options.classpath)) {
if (Files.exists(p)) {
dependencyFinder.addClassPathArchive(p);
}
}
// if -genmoduleinfo is not set, the input arguments are considered as
// unnamed module. Add them to the root set
if (options.genModuleInfo == null) {
// add root set
for (String s : classes) {
Path p = Paths.get(s);
if (Files.exists(p)) {
// add to the initial root set
dependencyFinder.addRoot(p);
} else {
if (isValidClassName(s)) {
dependencyFinder.addClassName(s);
} else {
warning("warn.invalid.arg", s);
}
}
}
}
}
private boolean analyzeDeps(DependencyFinder dependencyFinder) throws IOException {
JdepsFilter filter = dependencyFilter();
// parse classfiles and find all dependencies
findDependencies(dependencyFinder, filter, options.apiOnly);
// analyze the dependencies collected
Analyzer analyzer = new Analyzer(options.verbose, filter);
analyzer.run(dependencyFinder.archives());
// output result
final JdepsWriter writer;
if (options.dotOutputDir != null) {
Path dir = Paths.get(options.dotOutputDir);
Files.createDirectories(dir);
writer = new DotFileWriter(dir, options.verbose,
options.showProfile,
options.showModule,
options.showLabel);
} else {
writer = new SimpleWriter(log, options.verbose,
options.showProfile,
options.showModule);
}
// Targets for reporting - include the root sets and other analyzed archives
final List<Archive> targets;
if (options.rootModule == null) {
// no module as the root set
targets = dependencyFinder.archives()
.filter(filter::accept)
.filter(a -> !a.getModule().isNamed())
.collect(Collectors.toList());
} else {
// named modules in topological order
Stream<Module> modules = dependencyFinder.archives()
.filter(a -> a.getModule().isNamed())
.map(Archive::getModule);
Graph<Module> graph = ModuleAnalyzer.graph(modulePaths, modules.toArray(Module[]::new));
// then add unnamed module
targets = graph.orderedNodes()
.filter(filter::accept)
.collect(Collectors.toList());
// in case any reference not found
dependencyFinder.archives()
.filter(a -> !a.getModule().isNamed())
.forEach(targets::add);
}
writer.generateOutput(targets, analyzer);
if (options.findJDKInternals && !options.nowarning) {
showReplacements(targets, analyzer);
}
return true;
}
private JdepsFilter dependencyFilter() {
// Filter specified by -filter, -package, -regex, and -module options
JdepsFilter.Builder builder = new JdepsFilter.Builder();
// Exclude JDK modules from analysis and reporting if -m specified.
modulePaths.getModules().values().stream()
.filter(m -> m.isJDK())
.map(Module::name)
.forEach(options.excludes::add);
// source filters
builder.includePattern(options.includePattern);
builder.includeModules(options.includes);
builder.excludeModules(options.excludes);
builder.filter(options.filterSamePackage, options.filterSameArchive);
builder.findJDKInternals(options.findJDKInternals);
// -module
if (!options.requires.isEmpty()) {
Map<String, Module> modules = modulePaths.getModules();
builder.packages(options.requires.stream()
.map(modules::get)
.flatMap(m -> m.packages().stream())
.collect(Collectors.toSet()));
}
// -regex
if (options.regex != null)
builder.regex(options.regex);
// -package
if (!options.packageNames.isEmpty())
builder.packages(options.packageNames);
// -filter
if (options.filterRegex != null)
builder.filter(options.filterRegex);
return builder.build();
}
private void findDependencies(DependencyFinder dependencyFinder,
JdepsFilter filter,
boolean apiOnly)
throws IOException
{
dependencyFinder.findDependencies(filter, apiOnly, options.depth);
// print skipped entries, if any
for (Archive a : dependencyFinder.roots()) {
for (String name : a.reader().skippedEntries()) {
warning("warn.skipped.entry", name, a.getPathName());
}
}
}
private boolean genModuleInfo(DependencyFinder dependencyFinder) throws IOException {
ModuleInfoBuilder builder = new ModuleInfoBuilder(modulePaths, dependencyFinder);
boolean result = builder.run(options.verbose, options.nowarning);
builder.build(Paths.get(options.genModuleInfo));
return result;
}
private boolean analyzeModules(DependencyFinder dependencyFinder)
throws IOException
{
ModuleAnalyzer analyzer = new ModuleAnalyzer(modulePaths,
dependencyFinder,
options.rootModule);
if (options.checkModuleDeps) {
return analyzer.run();
}
if (options.dotOutputDir != null && options.verbose == SUMMARY) {
Path dir = Paths.get(options.dotOutputDir);
Files.createDirectories(dir);
analyzer.genDotFile(dir);
return true;
}
return false;
}
private boolean isValidClassName(String name) {
if (!Character.isJavaIdentifierStart(name.charAt(0))) {
return false;
}
for (int i=1; i < name.length(); i++) {
char c = name.charAt(i);
if (c != '.' && !Character.isJavaIdentifierPart(c)) {
return false;
}
}
return true;
}
public void handleOptions(String[] args) throws BadArgs {
// process options
for (int i=0; i < args.length; i++) {
if (args[i].charAt(0) == '-') {
String name = args[i];
Option option = getOption(name);
String param = null;
if (option.hasArg) {
if (name.startsWith("-") && name.indexOf('=') > 0) {
param = name.substring(name.indexOf('=') + 1, name.length());
} else if (i + 1 < args.length) {
param = args[++i];
}
if (param == null || param.isEmpty() || param.charAt(0) == '-') {
throw new BadArgs("err.missing.arg", name).showUsage(true);
}
}
option.process(this, name, param);
if (option.ignoreRest()) {
i = args.length;
}
} else {
// process rest of the input arguments
for (; i < args.length; i++) {
String name = args[i];
if (name.charAt(0) == '-') {
throw new BadArgs("err.option.after.class", name).showUsage(true);
}
classes.add(name);
}
}
}
}
private Option getOption(String name) throws BadArgs {
for (Option o : recognizedOptions) {
if (o.matches(name)) {
return o;
}
}
throw new BadArgs("err.unknown.option", name).showUsage(true);
}
private void reportError(String key, Object... args) {
log.println(getMessage("error.prefix") + " " + getMessage(key, args));
}
private void warning(String key, Object... args) {
log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
}
private void showHelp() {
log.println(getMessage("main.usage", PROGNAME));
for (Option o : recognizedOptions) {
String name = o.aliases[0].substring(1); // there must always be at least one name
name = name.charAt(0) == '-' ? name.substring(1) : name;
if (o.isHidden() || name.equals("h") || name.startsWith("filter:")) {
continue;
}
log.println(getMessage("main.opt." + name));
}
}
private void showVersion(boolean full) {
log.println(version(full ? "full" : "release"));
}
private String version(String key) {
// key=version: mm.nn.oo[-milestone]
// key=full: mm.mm.oo[-milestone]-build
if (ResourceBundleHelper.versionRB == null) {
return System.getProperty("java.version");
}
try {
return ResourceBundleHelper.versionRB.getString(key);
} catch (MissingResourceException e) {
return getMessage("version.unknown", System.getProperty("java.version"));
}
}
static String getMessage(String key, Object... args) {
try {
return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
} catch (MissingResourceException e) {
throw new InternalError("Missing message: " + key);
}
}
private static class Options {
boolean help;
boolean version;
boolean fullVersion;
boolean showProfile;
boolean showModule;
boolean showSummary;
boolean apiOnly;
boolean showLabel;
boolean findJDKInternals;
boolean nowarning = false;
// default is to show package-level dependencies
// and filter references from same package
Analyzer.Type verbose = PACKAGE;
boolean filterSamePackage = true;
boolean filterSameArchive = false;
Pattern filterRegex;
String dotOutputDir;
String genModuleInfo;
String classpath = "";
int depth = 1;
Set<String> requires = new HashSet<>();
Set<String> packageNames = new HashSet<>();
Pattern regex; // apply to the dependences
Pattern includePattern; // apply to classes
boolean compileTimeView = false;
boolean checkModuleDeps = false;
String upgradeModulePath;
String modulePath;
String rootModule;
// modules to be included or excluded
Set<String> includes = new HashSet<>();
Set<String> excludes = new HashSet<>();
boolean hasFilter() {
return numFilters() > 0;
}
int numFilters() {
int count = 0;
if (requires.size() > 0) count++;
if (regex != null) count++;
if (packageNames.size() > 0) count++;
return count;
}
boolean isRootModule() {
return rootModule != null;
}
}
private static class ResourceBundleHelper {
static final ResourceBundle versionRB;
static final ResourceBundle bundle;
static final ResourceBundle jdkinternals;
static {
Locale locale = Locale.getDefault();
try {
bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
} catch (MissingResourceException e) {
throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
}
try {
versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
} catch (MissingResourceException e) {
throw new InternalError("version.resource.missing");
}
try {
jdkinternals = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdkinternals");
} catch (MissingResourceException e) {
throw new InternalError("Cannot find jdkinternals resource bundle");
}
}
}
/*
* Returns the list of Archive specified in cpaths and not included
* initialArchives
*/
private List<Path> getClassPaths(String cpaths) throws IOException
{
if (cpaths.isEmpty()) {
return Collections.emptyList();
}
List<Path> paths = new ArrayList<>();
for (String p : cpaths.split(File.pathSeparator)) {
if (p.length() > 0) {
// wildcard to parse all JAR files e.g. -classpath dir/*
int i = p.lastIndexOf(".*");
if (i > 0) {
Path dir = Paths.get(p.substring(0, i));
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) {
for (Path entry : stream) {
paths.add(entry);
}
}
} else {
paths.add(Paths.get(p));
}
}
}
return paths;
}
/**
* Returns the recommended replacement API for the given classname;
* or return null if replacement API is not known.
*/
private String replacementFor(String cn) {
String name = cn;
String value = null;
while (value == null && name != null) {
try {
value = ResourceBundleHelper.jdkinternals.getString(name);
} catch (MissingResourceException e) {
// go up one subpackage level
int i = name.lastIndexOf('.');
name = i > 0 ? name.substring(0, i) : null;
}
}
return value;
};
private void showReplacements(List<Archive> archives, Analyzer analyzer) {
Map<String,String> jdkinternals = new TreeMap<>();
boolean useInternals = false;
for (Archive source : archives) {
useInternals = useInternals || analyzer.hasDependences(source);
for (String cn : analyzer.dependences(source)) {
String repl = replacementFor(cn);
if (repl != null) {
jdkinternals.putIfAbsent(cn, repl);
}
}
}
if (useInternals) {
log.println();
warning("warn.replace.useJDKInternals", getMessage("jdeps.wiki.url"));
}
if (!jdkinternals.isEmpty()) {
log.println();
log.format("%-40s %s%n", "JDK Internal API", "Suggested Replacement");
log.format("%-40s %s%n", "----------------", "---------------------");
for (Map.Entry<String,String> e : jdkinternals.entrySet()) {
log.format("%-40s %s%n", e.getKey(), e.getValue());
}
}
}
}