1 /* 2 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package com.sun.classanalyzer; 24 25 import com.sun.classanalyzer.ResourceFile.ServiceProviderConfigFile; 26 import com.sun.classanalyzer.ClassPath.*; 27 import java.io.*; 28 import java.util.*; 29 import java.util.jar.JarEntry; 30 import java.util.jar.JarFile; 31 32 /** 33 * Modularize classes and resources from legacy classpath to 34 * a module path containing a list of modules, one directory per module. 35 * 36 */ 37 public class Modularizer { 38 39 private final File modulepath; 40 private final Set<Module> modules; 41 private final ClassPath cpath; 42 public Modularizer(ClassPath cpath, File modulepath, Set<Module> modules) { 43 this.cpath = cpath; 44 this.modulepath = modulepath; 45 this.modules = modules; 46 } 47 48 /** 49 * Modularizes the legacy class path files into 50 * multiple modules. 51 * @param update true if only modules with newer classes 52 * resources are updated. 53 */ 54 void run(boolean update) throws IOException { 55 for (Module m : modules) { 56 File mdir = new File(modulepath, m.name()); 57 ModuleContent mc = new ModuleContent(m, mdir); 58 mc.copy(update); 59 if (mc.isUpdated()) { 60 mc.printStats(); 61 } 62 } 63 } 64 65 class ModuleContent { 66 int classes = 0; 67 int resources = 0; 68 long classBytes = 0; 69 long resourceBytes = 0; 70 final Module module; 71 final File classDir; // destination for classes and resources 72 final File mdir; 73 74 /** 75 * Module content. The 76 * 77 * @param m module 78 * @param dir directory of the module content 79 */ 80 ModuleContent(Module m, File dir) throws IOException { 81 this.module = m; 82 this.mdir = dir; 83 this.classDir = new File(dir, "classes"); 84 } 85 86 /** 87 * Tests if any file in this module content is updated. 88 */ 89 boolean isUpdated() { 90 return (classes + resources) > 0; 91 } 92 93 /** 94 * Copies the module content (classes and resources file) 95 * to the destination. 96 * 97 * @param update true if only modified files are copied; 98 * otherwise, all classes and resource files for this module 99 * are copied. 100 */ 101 void copy(boolean update) throws IOException { 102 if (!classDir.exists()) { 103 Files.mkdirs(classDir); 104 // override all files 105 update = false; 106 } 107 108 final boolean copyAll = update == false; 109 Module.Visitor<Void, File> visitor = new Module.Visitor<Void, File>() { 110 @Override 111 public Void visitClass(Klass k, File dir) { 112 String pathname = k.getClassFilePathname(); 113 Filter filter = copyAll ? null : new Filter(classDir, pathname); 114 long bytes; 115 try { 116 bytes = writeClass(k, filter); 117 if (bytes > 0) { 118 classes++; 119 classBytes += bytes; 120 } 121 } catch (IOException ex) { 122 throw new RuntimeException(ex); 123 } 124 return null; 125 } 126 127 @Override 128 public Void visitResource(ResourceFile r, File dir) { 129 String pathname = r.getPathname(); 130 Filter filter = copyAll ? null : new Filter(classDir, pathname); 131 try { 132 long bytes = writeResource(r, filter); 133 if (bytes > 0) { 134 resources++; 135 resourceBytes += bytes; 136 } 137 } catch (IOException ex) { 138 throw new RuntimeException(ex); 139 } 140 return null; 141 } 142 }; 143 144 module.visit(visitor, mdir); 145 } 146 147 void printStats() { 148 System.out.format("%s: %d classes (%d bytes) %d resource files (%d bytes) copied%n", 149 module.name(), classes, classBytes, resources, resourceBytes); 150 } 151 152 private ClassPathEntry lastVisitedClassPath = null; 153 /** 154 * Write the classfile of the given class if not filtered 155 * 156 * @param k a Klass 157 * @param filter a Filter 158 * @return the number of bytes copied 159 */ 160 long writeClass(Klass k, Filter filter) throws IOException { 161 String pathname = k.getClassFilePathname(); 162 Copier visitor = new Copier(classDir, filter); 163 if (lastVisitedClassPath != null) { 164 ClassPathEntry cp = lastVisitedClassPath.accept(visitor, pathname); 165 if (cp != null) { 166 assert cp == lastVisitedClassPath; 167 return visitor.bytes; 168 } 169 } 170 171 // locate the source of the given class from the classpath 172 for (ClassPathEntry cp : cpath.entries()) { 173 ClassPathEntry src = cp.accept(visitor, pathname); 174 if (src != null) { 175 // cache the ClassPathEntry from which this class is copied 176 // Most of the files in a module likely come from the 177 // same jar or directory. 178 lastVisitedClassPath = src; 179 return visitor.bytes; 180 } 181 } 182 return 0; 183 } 184 185 /** 186 * Write the resource file if not filtered 187 * 188 * @param res a ResourceFile 189 * @param filter a Filter 190 * @return the number of bytes copied 191 */ 192 long writeResource(ResourceFile res, Filter filter) throws IOException { 193 if (res.isService()) 194 return writeService(res, filter); 195 196 String pathname = res.getPathname(); 197 Copier visitor = new Copier(classDir, filter); 198 if (lastVisitedClassPath != null) { 199 ClassPathEntry cp = lastVisitedClassPath.accept(visitor, pathname); 200 if (cp != null) { 201 assert cp == lastVisitedClassPath; 202 return visitor.bytes; 203 } 204 } 205 206 // locate the source of the given resource file from the classpath 207 for (ClassPathEntry cp : cpath.entries()) { 208 ClassPathEntry src = cp.accept(visitor, pathname); 209 if (src != null) { 210 // cache the ClassPathEntry from which this class is copied 211 // Most of the files in a module likely come from the 212 // same jar or directory. 213 lastVisitedClassPath = src; 214 return visitor.bytes; 215 } 216 } 217 return 0; 218 } 219 220 /** 221 * Write the service descriptor file if not filtered 222 * 223 * @param res a ResourceFile 224 * @param filter a Filter 225 * @return the number of bytes copied 226 */ 227 long writeService(ResourceFile res, Filter filter) throws IOException { 228 String pathname = res.getPathname(); 229 Copier visitor = new Copier(classDir, filter); 230 boolean foundOne = false; 231 int bytes = 0; 232 233 // scan all class path entries for services 234 for (ClassPathEntry cp : cpath.entries()) { 235 ClassPathEntry src = cp.accept(visitor, pathname); 236 if (src != null) { 237 bytes += visitor.bytes; 238 if (foundOne == false) { 239 foundOne = true; 240 visitor = new Copier(classDir, null, true); // append subsequent 241 } 242 } 243 } 244 return bytes; 245 } 246 247 /** 248 * A ClassPathEntry visitor to copy a file to the given destination 249 * if not filtered. 250 */ 251 class Copier implements ClassPathEntry.Visitor<ClassPathEntry, String> { 252 final Filter filter; 253 final File dest; 254 final boolean append; 255 long bytes = 0; 256 257 Copier(File dest, Filter filter) { 258 this(dest, filter, false); 259 } 260 261 Copier(File dest, Filter filter, boolean append) { 262 this.filter = filter; 263 this.dest = dest; 264 this.append = append; 265 } 266 267 private boolean isService(String name) { 268 return name.startsWith("META-INF/services") ? true : false; 269 } 270 271 @Override 272 public ClassPathEntry visitFile(File src, ClassPathEntry cp, String pathname) throws IOException { 273 String name = pathname.replace(File.separatorChar, '/'); 274 if (cp.getName().endsWith(File.separator + pathname) 275 && matches(src, name)) { 276 if (filter == null || filter.accept(src)) { 277 File dst = new File(dest, pathname); 278 bytes += copy(src, dst); 279 } 280 return cp; 281 } else { 282 return null; 283 } 284 } 285 286 @Override 287 public ClassPathEntry visitDir(File dir, ClassPathEntry cp, String pathname) throws IOException { 288 File src = new File(cp.getFile(), pathname); 289 File dst = new File(dest, pathname); 290 String name = pathname.replace(File.separatorChar, '/'); 291 292 if (src.exists() && matches(src, name)) { 293 if (filter == null || filter.accept(src)) { 294 bytes += copy(src, dst); 295 } 296 return cp; 297 } else { 298 return null; 299 } 300 } 301 302 @Override 303 public ClassPathEntry visitJarFile(JarFile jf, ClassPathEntry cp, String pathname) throws IOException { 304 String name = pathname.replace(File.separatorChar, '/'); 305 JarEntry e = jf.getJarEntry(name); 306 if (e != null && matches(jf, e, name)) { 307 if (filter == null || filter.accept(jf, e)) { 308 bytes += copy(jf, e); 309 } 310 return cp; 311 } else { 312 return null; 313 } 314 } 315 316 boolean matches(File src, String name) throws IOException { 317 if (!isService(name)) { 318 return true; 319 } 320 321 try (FileInputStream fis = new FileInputStream(src); 322 BufferedInputStream in = new BufferedInputStream(fis)) { 323 return matches(in, name); 324 } 325 } 326 327 boolean matches(JarFile jf, JarEntry e, String name) throws IOException { 328 if (!isService(name)) { 329 return true; 330 } 331 return matches(jf.getInputStream(e), name); 332 } 333 334 boolean matches(InputStream in, String name) throws IOException { 335 ServiceProviderConfigFile sp = new ServiceProviderConfigFile(name, in); 336 for (String p : sp.providers) { 337 Klass k = Klass.findKlass(p); 338 if (k == null) { 339 Trace.trace("Service %s: provider class %s not found%n", sp.service, p); 340 continue; 341 } 342 if (module.contains(k)) { 343 return true; 344 } 345 } 346 // return true if no provider; otherwise false 347 return sp.providers.isEmpty(); 348 } 349 350 long copy(JarFile jf, JarEntry e) throws IOException { 351 File dst = new File(dest, e.getName().replace('/', File.separatorChar)); 352 if (!dst.exists()) { 353 Files.createFile(dst); 354 } 355 356 byte[] buf = new byte[8192]; 357 long bytes = 0; 358 try (InputStream in = jf.getInputStream(e); 359 FileOutputStream out = new FileOutputStream(dst, append)) { 360 int n; 361 while ((n = in.read(buf)) > 0) { 362 out.write(buf, 0, n); 363 bytes += n; 364 } 365 } 366 367 long lastModified = e.getTime(); 368 if (lastModified > 0) { 369 dst.setLastModified(lastModified); 370 } 371 return bytes; 372 } 373 374 long copy(File src, File dst) 375 throws IOException { 376 assert src.exists(); 377 378 if (!dst.exists()) { 379 Files.createFile(dst); 380 } 381 382 byte[] buf = new byte[8192]; 383 long bytes = 0; 384 try (InputStream fin = new FileInputStream(src); 385 BufferedInputStream in = new BufferedInputStream(fin); 386 FileOutputStream out = new FileOutputStream(dst, append)) { 387 int n; 388 while ((n = in.read(buf)) > 0) { 389 out.write(buf, 0, n); 390 bytes += n; 391 } 392 } 393 dst.setLastModified(src.lastModified()); 394 if (src.canExecute()) { 395 dst.setExecutable(true, false); 396 } 397 return bytes; 398 } 399 } 400 } 401 402 /** 403 * A filter that accepts files that don't exist in the given 404 * location or modified since it's copied. 405 */ 406 class Filter implements ClassPath.Filter { 407 private final long timestamp; 408 Filter(File dir, String pathname) { 409 File destfile = new File(dir, pathname); 410 this.timestamp = destfile.exists() ? destfile.lastModified() : -1L; 411 } 412 413 @Override 414 public boolean accept(File f) throws IOException { 415 if (f.isDirectory()) { 416 return true; 417 } 418 419 long ts = f.lastModified(); 420 return (timestamp < 0 || ts < 0 || timestamp < ts); 421 } 422 423 @Override 424 public boolean accept(JarFile jf, JarEntry e) throws IOException { 425 long ts = e.getTime(); 426 return timestamp < 0 || ts < 0 || timestamp < ts; 427 } 428 } 429 430 public static void main(String[] args) throws Exception { 431 String jdkhome = null; 432 String classpath = null; 433 String classlistDir = null; 434 String modulepath = null; 435 boolean update = false; 436 437 // process arguments 438 int i = 0; 439 while (i < args.length) { 440 String arg = args[i++]; 441 if (arg.equals("-jdkhome")) { 442 jdkhome = getOption(args, i++); 443 } else if (arg.equals("-classpath")) { 444 classpath = getOption(args, i++); 445 } else if (arg.equals("-modulepath")) { 446 modulepath = getOption(args, i++); 447 } else if (arg.equals("-classlistdir")) { 448 classlistDir = getOption(args, i++); 449 } else if (arg.equals("-update")) { 450 // copy new files only 451 update = true; 452 } else { 453 error("Invalid option: " + arg); 454 } 455 } 456 457 if (jdkhome == null && classpath == null) { 458 error("-jdkhome and -classpath not set"); 459 } 460 461 if (jdkhome != null && classpath != null) { 462 error("Both -jdkhome and -classpath are set"); 463 } 464 465 if (classlistDir == null || modulepath == null) { 466 error("-modulepath or -classlist not set"); 467 } 468 469 ClassPath cpath = null; 470 if (jdkhome != null) { 471 cpath = ClassPath.newJDKClassPath(jdkhome); 472 } else if (classpath != null) { 473 cpath = ClassPath.newInstance(classpath); 474 } 475 476 ClassListReader reader = new ClassListReader(classlistDir, "default"); 477 Set<Module> modules = reader.run(); 478 Modularizer modularizer = new Modularizer(cpath, new File(modulepath), modules); 479 modularizer.run(update); 480 } 481 482 private static String getOption(String[] args, int index) { 483 if (index < args.length) { 484 return args[index]; 485 } else { 486 error(args[index-1] + ": Missing argument"); 487 } 488 return null; 489 } 490 491 private static void error(String msg) { 492 System.err.println("ERROR: " + msg); 493 System.out.println(usage()); 494 System.exit(-1); 495 } 496 497 private static String usage() { 498 StringBuilder sb = new StringBuilder(); 499 sb.append("Usage: Modularizer <options>\n"); 500 sb.append("Options: \n"); 501 sb.append("\t-jdkhome <JDK home> where all jars will be parsed\n"); 502 sb.append("\t-classpath <classpath> where classes and jars will be parsed\n"); 503 sb.append("\t Either -jdkhome or -classpath option can be used.\n"); 504 sb.append("\t-classlistdir <classlist dir>\n"); 505 sb.append("\t-update update modules with newer files\n"); 506 sb.append("\t-modulepath <module-path> for writing modules\n"); 507 return sb.toString(); 508 } 509 }