1.1 --- a/src/share/classes/com/sun/tools/jdeps/JdepsTask.java Thu Oct 17 13:50:00 2013 +0200 1.2 +++ b/src/share/classes/com/sun/tools/jdeps/JdepsTask.java Thu Oct 17 13:19:48 2013 -0700 1.3 @@ -24,12 +24,18 @@ 1.4 */ 1.5 package com.sun.tools.jdeps; 1.6 1.7 +import com.sun.tools.classfile.AccessFlags; 1.8 import com.sun.tools.classfile.ClassFile; 1.9 import com.sun.tools.classfile.ConstantPoolException; 1.10 import com.sun.tools.classfile.Dependencies; 1.11 import com.sun.tools.classfile.Dependencies.ClassFileError; 1.12 import com.sun.tools.classfile.Dependency; 1.13 +import com.sun.tools.jdeps.PlatformClassPath.JDKArchive; 1.14 import java.io.*; 1.15 +import java.nio.file.DirectoryStream; 1.16 +import java.nio.file.Files; 1.17 +import java.nio.file.Path; 1.18 +import java.nio.file.Paths; 1.19 import java.text.MessageFormat; 1.20 import java.util.*; 1.21 import java.util.regex.Pattern; 1.22 @@ -67,11 +73,10 @@ 1.23 1.24 boolean matches(String opt) { 1.25 for (String a : aliases) { 1.26 - if (a.equals(opt)) { 1.27 + if (a.equals(opt)) 1.28 return true; 1.29 - } else if (opt.startsWith("--") && hasArg && opt.startsWith(a + "=")) { 1.30 + if (hasArg && opt.startsWith(a + "=")) 1.31 return true; 1.32 - } 1.33 } 1.34 return false; 1.35 } 1.36 @@ -96,62 +101,96 @@ 1.37 } 1.38 1.39 static Option[] recognizedOptions = { 1.40 - new Option(false, "-h", "-?", "--help") { 1.41 + new Option(false, "-h", "-?", "-help") { 1.42 void process(JdepsTask task, String opt, String arg) { 1.43 task.options.help = true; 1.44 } 1.45 }, 1.46 - new Option(false, "-s", "--summary") { 1.47 + new Option(true, "-dotoutput") { 1.48 + void process(JdepsTask task, String opt, String arg) throws BadArgs { 1.49 + Path p = Paths.get(arg); 1.50 + if (Files.exists(p) && (!Files.isDirectory(p) || !Files.isWritable(p))) { 1.51 + throw new BadArgs("err.dot.output.path", arg); 1.52 + } 1.53 + task.options.dotOutputDir = arg; 1.54 + } 1.55 + }, 1.56 + new Option(false, "-s", "-summary") { 1.57 void process(JdepsTask task, String opt, String arg) { 1.58 task.options.showSummary = true; 1.59 task.options.verbose = Analyzer.Type.SUMMARY; 1.60 } 1.61 }, 1.62 - new Option(false, "-v", "--verbose") { 1.63 - void process(JdepsTask task, String opt, String arg) { 1.64 - task.options.verbose = Analyzer.Type.VERBOSE; 1.65 - } 1.66 - }, 1.67 - new Option(true, "-V", "--verbose-level") { 1.68 + new Option(false, "-v", "-verbose", 1.69 + "-verbose:package", 1.70 + "-verbose:class") 1.71 + { 1.72 void process(JdepsTask task, String opt, String arg) throws BadArgs { 1.73 - if ("package".equals(arg)) { 1.74 - task.options.verbose = Analyzer.Type.PACKAGE; 1.75 - } else if ("class".equals(arg)) { 1.76 - task.options.verbose = Analyzer.Type.CLASS; 1.77 - } else { 1.78 - throw new BadArgs("err.invalid.arg.for.option", opt); 1.79 + switch (opt) { 1.80 + case "-v": 1.81 + case "-verbose": 1.82 + task.options.verbose = Analyzer.Type.VERBOSE; 1.83 + break; 1.84 + case "-verbose:package": 1.85 + task.options.verbose = Analyzer.Type.PACKAGE; 1.86 + break; 1.87 + case "-verbose:class": 1.88 + task.options.verbose = Analyzer.Type.CLASS; 1.89 + break; 1.90 + default: 1.91 + throw new BadArgs("err.invalid.arg.for.option", opt); 1.92 } 1.93 } 1.94 }, 1.95 - new Option(true, "-c", "--classpath") { 1.96 + new Option(true, "-cp", "-classpath") { 1.97 void process(JdepsTask task, String opt, String arg) { 1.98 task.options.classpath = arg; 1.99 } 1.100 }, 1.101 - new Option(true, "-p", "--package") { 1.102 + new Option(true, "-p", "-package") { 1.103 void process(JdepsTask task, String opt, String arg) { 1.104 task.options.packageNames.add(arg); 1.105 } 1.106 }, 1.107 - new Option(true, "-e", "--regex") { 1.108 + new Option(true, "-e", "-regex") { 1.109 void process(JdepsTask task, String opt, String arg) { 1.110 task.options.regex = arg; 1.111 } 1.112 }, 1.113 - new Option(false, "-P", "--profile") { 1.114 + new Option(true, "-include") { 1.115 + void process(JdepsTask task, String opt, String arg) throws BadArgs { 1.116 + task.options.includePattern = Pattern.compile(arg); 1.117 + } 1.118 + }, 1.119 + new Option(false, "-P", "-profile") { 1.120 void process(JdepsTask task, String opt, String arg) throws BadArgs { 1.121 task.options.showProfile = true; 1.122 - if (Profiles.getProfileCount() == 0) { 1.123 + if (Profile.getProfileCount() == 0) { 1.124 throw new BadArgs("err.option.unsupported", opt, getMessage("err.profiles.msg")); 1.125 } 1.126 } 1.127 }, 1.128 - new Option(false, "-R", "--recursive") { 1.129 + new Option(false, "-apionly") { 1.130 + void process(JdepsTask task, String opt, String arg) { 1.131 + task.options.apiOnly = true; 1.132 + } 1.133 + }, 1.134 + new Option(false, "-R", "-recursive") { 1.135 void process(JdepsTask task, String opt, String arg) { 1.136 task.options.depth = 0; 1.137 } 1.138 }, 1.139 - new HiddenOption(true, "-d", "--depth") { 1.140 + new Option(false, "-version") { 1.141 + void process(JdepsTask task, String opt, String arg) { 1.142 + task.options.version = true; 1.143 + } 1.144 + }, 1.145 + new HiddenOption(false, "-fullversion") { 1.146 + void process(JdepsTask task, String opt, String arg) { 1.147 + task.options.fullVersion = true; 1.148 + } 1.149 + }, 1.150 + new HiddenOption(true, "-depth") { 1.151 void process(JdepsTask task, String opt, String arg) throws BadArgs { 1.152 try { 1.153 task.options.depth = Integer.parseInt(arg); 1.154 @@ -160,16 +199,6 @@ 1.155 } 1.156 } 1.157 }, 1.158 - new Option(false, "--version") { 1.159 - void process(JdepsTask task, String opt, String arg) { 1.160 - task.options.version = true; 1.161 - } 1.162 - }, 1.163 - new HiddenOption(false, "--fullversion") { 1.164 - void process(JdepsTask task, String opt, String arg) { 1.165 - task.options.fullVersion = true; 1.166 - } 1.167 - }, 1.168 }; 1.169 1.170 private static final String PROGNAME = "jdeps"; 1.171 @@ -202,7 +231,7 @@ 1.172 if (options.version || options.fullVersion) { 1.173 showVersion(options.fullVersion); 1.174 } 1.175 - if (classes.isEmpty() && !options.wildcard) { 1.176 + if (classes.isEmpty() && options.includePattern == null) { 1.177 if (options.help || options.version || options.fullVersion) { 1.178 return EXIT_OK; 1.179 } else { 1.180 @@ -233,19 +262,51 @@ 1.181 } 1.182 } 1.183 1.184 - private final List<Archive> sourceLocations = new ArrayList<Archive>(); 1.185 + private final List<Archive> sourceLocations = new ArrayList<>(); 1.186 private boolean run() throws IOException { 1.187 findDependencies(); 1.188 Analyzer analyzer = new Analyzer(options.verbose); 1.189 analyzer.run(sourceLocations); 1.190 - if (options.verbose == Analyzer.Type.SUMMARY) { 1.191 - printSummary(log, analyzer); 1.192 + if (options.dotOutputDir != null) { 1.193 + Path dir = Paths.get(options.dotOutputDir); 1.194 + Files.createDirectories(dir); 1.195 + generateDotFiles(dir, analyzer); 1.196 } else { 1.197 - printDependencies(log, analyzer); 1.198 + printRawOutput(log, analyzer); 1.199 } 1.200 return true; 1.201 } 1.202 1.203 + private void generateDotFiles(Path dir, Analyzer analyzer) throws IOException { 1.204 + Path summary = dir.resolve("summary.dot"); 1.205 + try (PrintWriter sw = new PrintWriter(Files.newOutputStream(summary)); 1.206 + DotFileFormatter formatter = new DotFileFormatter(sw, "summary")) { 1.207 + for (Archive archive : sourceLocations) { 1.208 + analyzer.visitArchiveDependences(archive, formatter); 1.209 + } 1.210 + } 1.211 + if (options.verbose != Analyzer.Type.SUMMARY) { 1.212 + for (Archive archive : sourceLocations) { 1.213 + if (analyzer.hasDependences(archive)) { 1.214 + Path dotfile = dir.resolve(archive.getFileName() + ".dot"); 1.215 + try (PrintWriter pw = new PrintWriter(Files.newOutputStream(dotfile)); 1.216 + DotFileFormatter formatter = new DotFileFormatter(pw, archive)) { 1.217 + analyzer.visitDependences(archive, formatter); 1.218 + } 1.219 + } 1.220 + } 1.221 + } 1.222 + } 1.223 + 1.224 + private void printRawOutput(PrintWriter writer, Analyzer analyzer) { 1.225 + for (Archive archive : sourceLocations) { 1.226 + RawOutputFormatter formatter = new RawOutputFormatter(writer); 1.227 + analyzer.visitArchiveDependences(archive, formatter); 1.228 + if (options.verbose != Analyzer.Type.SUMMARY) { 1.229 + analyzer.visitDependences(archive, formatter); 1.230 + } 1.231 + } 1.232 + } 1.233 private boolean isValidClassName(String name) { 1.234 if (!Character.isJavaIdentifierStart(name.charAt(0))) { 1.235 return false; 1.236 @@ -259,27 +320,43 @@ 1.237 return true; 1.238 } 1.239 1.240 - private void findDependencies() throws IOException { 1.241 - Dependency.Finder finder = Dependencies.getClassDependencyFinder(); 1.242 - Dependency.Filter filter; 1.243 - if (options.regex != null) { 1.244 - filter = Dependencies.getRegexFilter(Pattern.compile(options.regex)); 1.245 + private Dependency.Filter getDependencyFilter() { 1.246 + if (options.regex != null) { 1.247 + return Dependencies.getRegexFilter(Pattern.compile(options.regex)); 1.248 } else if (options.packageNames.size() > 0) { 1.249 - filter = Dependencies.getPackageFilter(options.packageNames, false); 1.250 + return Dependencies.getPackageFilter(options.packageNames, false); 1.251 } else { 1.252 - filter = new Dependency.Filter() { 1.253 + return new Dependency.Filter() { 1.254 + @Override 1.255 public boolean accepts(Dependency dependency) { 1.256 return !dependency.getOrigin().equals(dependency.getTarget()); 1.257 } 1.258 }; 1.259 } 1.260 + } 1.261 1.262 - List<Archive> archives = new ArrayList<Archive>(); 1.263 - Deque<String> roots = new LinkedList<String>(); 1.264 + private boolean matches(String classname, AccessFlags flags) { 1.265 + if (options.apiOnly && !flags.is(AccessFlags.ACC_PUBLIC)) { 1.266 + return false; 1.267 + } else if (options.includePattern != null) { 1.268 + return options.includePattern.matcher(classname.replace('/', '.')).matches(); 1.269 + } else { 1.270 + return true; 1.271 + } 1.272 + } 1.273 + 1.274 + private void findDependencies() throws IOException { 1.275 + Dependency.Finder finder = 1.276 + options.apiOnly ? Dependencies.getAPIFinder(AccessFlags.ACC_PROTECTED) 1.277 + : Dependencies.getClassDependencyFinder(); 1.278 + Dependency.Filter filter = getDependencyFilter(); 1.279 + 1.280 + List<Archive> archives = new ArrayList<>(); 1.281 + Deque<String> roots = new LinkedList<>(); 1.282 for (String s : classes) { 1.283 - File f = new File(s); 1.284 - if (f.exists()) { 1.285 - archives.add(new Archive(f, ClassFileReader.newInstance(f))); 1.286 + Path p = Paths.get(s); 1.287 + if (Files.exists(p)) { 1.288 + archives.add(new Archive(p, ClassFileReader.newInstance(p))); 1.289 } else { 1.290 if (isValidClassName(s)) { 1.291 roots.add(s); 1.292 @@ -289,9 +366,8 @@ 1.293 } 1.294 } 1.295 1.296 - List<Archive> classpaths = new ArrayList<Archive>(); // for class file lookup 1.297 - if (options.wildcard) { 1.298 - // include all archives from classpath to the initial list 1.299 + List<Archive> classpaths = new ArrayList<>(); // for class file lookup 1.300 + if (options.includePattern != null) { 1.301 archives.addAll(getClassPathArchives(options.classpath)); 1.302 } else { 1.303 classpaths.addAll(getClassPathArchives(options.classpath)); 1.304 @@ -305,8 +381,8 @@ 1.305 // Work queue of names of classfiles to be searched. 1.306 // Entries will be unique, and for classes that do not yet have 1.307 // dependencies in the results map. 1.308 - Deque<String> deque = new LinkedList<String>(); 1.309 - Set<String> doneClasses = new HashSet<String>(); 1.310 + Deque<String> deque = new LinkedList<>(); 1.311 + Set<String> doneClasses = new HashSet<>(); 1.312 1.313 // get the immediate dependencies of the input files 1.314 for (Archive a : archives) { 1.315 @@ -318,16 +394,18 @@ 1.316 throw new ClassFileError(e); 1.317 } 1.318 1.319 - if (!doneClasses.contains(classFileName)) { 1.320 - doneClasses.add(classFileName); 1.321 - } 1.322 - for (Dependency d : finder.findDependencies(cf)) { 1.323 - if (filter.accepts(d)) { 1.324 - String cn = d.getTarget().getName(); 1.325 - if (!doneClasses.contains(cn) && !deque.contains(cn)) { 1.326 - deque.add(cn); 1.327 + if (matches(classFileName, cf.access_flags)) { 1.328 + if (!doneClasses.contains(classFileName)) { 1.329 + doneClasses.add(classFileName); 1.330 + } 1.331 + for (Dependency d : finder.findDependencies(cf)) { 1.332 + if (filter.accepts(d)) { 1.333 + String cn = d.getTarget().getName(); 1.334 + if (!doneClasses.contains(cn) && !deque.contains(cn)) { 1.335 + deque.add(cn); 1.336 + } 1.337 + a.addClass(d.getOrigin(), d.getTarget()); 1.338 } 1.339 - a.addClass(d.getOrigin(), d.getTarget()); 1.340 } 1.341 } 1.342 } 1.343 @@ -379,46 +457,10 @@ 1.344 } 1.345 } 1.346 unresolved = deque; 1.347 - deque = new LinkedList<String>(); 1.348 + deque = new LinkedList<>(); 1.349 } while (!unresolved.isEmpty() && depth-- > 0); 1.350 } 1.351 1.352 - private void printSummary(final PrintWriter out, final Analyzer analyzer) { 1.353 - Analyzer.Visitor visitor = new Analyzer.Visitor() { 1.354 - public void visit(String origin, String target, String profile) { 1.355 - if (options.showProfile) { 1.356 - out.format("%-30s -> %s%n", origin, target); 1.357 - } 1.358 - } 1.359 - public void visit(Archive origin, Archive target) { 1.360 - if (!options.showProfile) { 1.361 - out.format("%-30s -> %s%n", origin, target); 1.362 - } 1.363 - } 1.364 - }; 1.365 - analyzer.visitSummary(visitor); 1.366 - } 1.367 - 1.368 - private void printDependencies(final PrintWriter out, final Analyzer analyzer) { 1.369 - Analyzer.Visitor visitor = new Analyzer.Visitor() { 1.370 - private String pkg = ""; 1.371 - public void visit(String origin, String target, String profile) { 1.372 - if (!origin.equals(pkg)) { 1.373 - pkg = origin; 1.374 - out.format(" %s (%s)%n", origin, analyzer.getArchive(origin).getFileName()); 1.375 - } 1.376 - out.format(" -> %-50s %s%n", target, 1.377 - (options.showProfile && !profile.isEmpty()) 1.378 - ? profile 1.379 - : analyzer.getArchiveName(target, profile)); 1.380 - } 1.381 - public void visit(Archive origin, Archive target) { 1.382 - out.format("%s -> %s%n", origin, target); 1.383 - } 1.384 - }; 1.385 - analyzer.visit(visitor); 1.386 - } 1.387 - 1.388 public void handleOptions(String[] args) throws BadArgs { 1.389 // process options 1.390 for (int i=0; i < args.length; i++) { 1.391 @@ -427,7 +469,7 @@ 1.392 Option option = getOption(name); 1.393 String param = null; 1.394 if (option.hasArg) { 1.395 - if (name.startsWith("--") && name.indexOf('=') > 0) { 1.396 + if (name.startsWith("-") && name.indexOf('=') > 0) { 1.397 param = name.substring(name.indexOf('=') + 1, name.length()); 1.398 } else if (i + 1 < args.length) { 1.399 param = args[++i]; 1.400 @@ -447,11 +489,7 @@ 1.401 if (name.charAt(0) == '-') { 1.402 throw new BadArgs("err.option.after.class", name).showUsage(true); 1.403 } 1.404 - if (name.equals("*") || name.equals("\"*\"")) { 1.405 - options.wildcard = true; 1.406 - } else { 1.407 - classes.add(name); 1.408 - } 1.409 + classes.add(name); 1.410 } 1.411 } 1.412 } 1.413 @@ -518,13 +556,15 @@ 1.414 boolean showProfile; 1.415 boolean showSummary; 1.416 boolean wildcard; 1.417 - String regex; 1.418 + boolean apiOnly; 1.419 + String dotOutputDir; 1.420 String classpath = ""; 1.421 int depth = 1; 1.422 Analyzer.Type verbose = Analyzer.Type.PACKAGE; 1.423 - Set<String> packageNames = new HashSet<String>(); 1.424 + Set<String> packageNames = new HashSet<>(); 1.425 + String regex; // apply to the dependences 1.426 + Pattern includePattern; // apply to classes 1.427 } 1.428 - 1.429 private static class ResourceBundleHelper { 1.430 static final ResourceBundle versionRB; 1.431 static final ResourceBundle bundle; 1.432 @@ -547,9 +587,9 @@ 1.433 private List<Archive> getArchives(List<String> filenames) throws IOException { 1.434 List<Archive> result = new ArrayList<Archive>(); 1.435 for (String s : filenames) { 1.436 - File f = new File(s); 1.437 - if (f.exists()) { 1.438 - result.add(new Archive(f, ClassFileReader.newInstance(f))); 1.439 + Path p = Paths.get(s); 1.440 + if (Files.exists(p)) { 1.441 + result.add(new Archive(p, ClassFileReader.newInstance(p))); 1.442 } else { 1.443 warning("warn.file.not.exist", s); 1.444 } 1.445 @@ -558,18 +598,131 @@ 1.446 } 1.447 1.448 private List<Archive> getClassPathArchives(String paths) throws IOException { 1.449 - List<Archive> result = new ArrayList<Archive>(); 1.450 + List<Archive> result = new ArrayList<>(); 1.451 if (paths.isEmpty()) { 1.452 return result; 1.453 } 1.454 for (String p : paths.split(File.pathSeparator)) { 1.455 if (p.length() > 0) { 1.456 - File f = new File(p); 1.457 - if (f.exists()) { 1.458 - result.add(new Archive(f, ClassFileReader.newInstance(f))); 1.459 + List<Path> files = new ArrayList<>(); 1.460 + // wildcard to parse all JAR files e.g. -classpath dir/* 1.461 + int i = p.lastIndexOf(".*"); 1.462 + if (i > 0) { 1.463 + Path dir = Paths.get(p.substring(0, i)); 1.464 + try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.jar")) { 1.465 + for (Path entry : stream) { 1.466 + files.add(entry); 1.467 + } 1.468 + } 1.469 + } else { 1.470 + files.add(Paths.get(p)); 1.471 + } 1.472 + for (Path f : files) { 1.473 + if (Files.exists(f)) { 1.474 + result.add(new Archive(f, ClassFileReader.newInstance(f))); 1.475 + } 1.476 } 1.477 } 1.478 } 1.479 return result; 1.480 } 1.481 + 1.482 + 1.483 + /** 1.484 + * Returns the file name of the archive for non-JRE class or 1.485 + * internal JRE classes. It returns empty string for SE API. 1.486 + */ 1.487 + private static String getArchiveName(Archive source, String profile) { 1.488 + String name = source.getFileName(); 1.489 + if (source instanceof JDKArchive) 1.490 + return profile.isEmpty() ? "JDK internal API (" + name + ")" : ""; 1.491 + return name; 1.492 + } 1.493 + 1.494 + class RawOutputFormatter implements Analyzer.Visitor { 1.495 + private final PrintWriter writer; 1.496 + RawOutputFormatter(PrintWriter writer) { 1.497 + this.writer = writer; 1.498 + } 1.499 + 1.500 + private String pkg = ""; 1.501 + @Override 1.502 + public void visitDependence(String origin, Archive source, 1.503 + String target, Archive archive, String profile) { 1.504 + if (!origin.equals(pkg)) { 1.505 + pkg = origin; 1.506 + writer.format(" %s (%s)%n", origin, source.getFileName()); 1.507 + } 1.508 + String name = (options.showProfile && !profile.isEmpty()) 1.509 + ? profile 1.510 + : getArchiveName(archive, profile); 1.511 + writer.format(" -> %-50s %s%n", target, name); 1.512 + } 1.513 + 1.514 + @Override 1.515 + public void visitArchiveDependence(Archive origin, Archive target, String profile) { 1.516 + writer.format("%s -> %s", origin, target); 1.517 + if (options.showProfile && !profile.isEmpty()) { 1.518 + writer.format(" (%s)%n", profile); 1.519 + } else { 1.520 + writer.format("%n"); 1.521 + } 1.522 + } 1.523 + } 1.524 + 1.525 + class DotFileFormatter implements Analyzer.Visitor, AutoCloseable { 1.526 + private final PrintWriter writer; 1.527 + private final String name; 1.528 + DotFileFormatter(PrintWriter writer, String name) { 1.529 + this.writer = writer; 1.530 + this.name = name; 1.531 + writer.format("digraph \"%s\" {%n", name); 1.532 + } 1.533 + DotFileFormatter(PrintWriter writer, Archive archive) { 1.534 + this.writer = writer; 1.535 + this.name = archive.getFileName(); 1.536 + writer.format("digraph \"%s\" {%n", name); 1.537 + writer.format(" // Path: %s%n", archive.toString()); 1.538 + } 1.539 + 1.540 + @Override 1.541 + public void close() { 1.542 + writer.println("}"); 1.543 + } 1.544 + 1.545 + private final Set<String> edges = new HashSet<>(); 1.546 + private String node = ""; 1.547 + @Override 1.548 + public void visitDependence(String origin, Archive source, 1.549 + String target, Archive archive, String profile) { 1.550 + if (!node.equals(origin)) { 1.551 + edges.clear(); 1.552 + node = origin; 1.553 + } 1.554 + // if -P option is specified, package name -> profile will 1.555 + // be shown and filter out multiple same edges. 1.556 + if (!edges.contains(target)) { 1.557 + StringBuilder sb = new StringBuilder(); 1.558 + String name = options.showProfile && !profile.isEmpty() 1.559 + ? profile 1.560 + : getArchiveName(archive, profile); 1.561 + writer.format(" %-50s -> %s;%n", 1.562 + String.format("\"%s\"", origin), 1.563 + name.isEmpty() ? String.format("\"%s\"", target) 1.564 + : String.format("\"%s (%s)\"", target, name)); 1.565 + edges.add(target); 1.566 + } 1.567 + } 1.568 + 1.569 + @Override 1.570 + public void visitArchiveDependence(Archive origin, Archive target, String profile) { 1.571 + String name = options.showProfile && !profile.isEmpty() 1.572 + ? profile : ""; 1.573 + writer.format(" %-30s -> \"%s\";%n", 1.574 + String.format("\"%s\"", origin.getFileName()), 1.575 + name.isEmpty() 1.576 + ? target.getFileName() 1.577 + : String.format("%s (%s)", target.getFileName(), name)); 1.578 + } 1.579 + } 1.580 }