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