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             String pathname = res.getPathname();
 194             Copier visitor = new Copier(classDir, filter);
 195             if (lastVisitedClassPath != null) {
 196                 ClassPathEntry cp = lastVisitedClassPath.accept(visitor, pathname);
 197                 if (cp != null) {
 198                     assert cp == lastVisitedClassPath;
 199                     return visitor.bytes;
 200                 }
 201             }
 202 
 203             // locate the source of the given resource file from the classpath
 204             for (ClassPathEntry cp : cpath.entries()) {
 205                 ClassPathEntry src = cp.accept(visitor, pathname);
 206                 if (src != null) {
 207                     // cache the ClassPathEntry from which this class is copied
 208                     // Most of the files in a module likely come from the
 209                     // same jar or directory.
 210                     lastVisitedClassPath = src;
 211                     return visitor.bytes;
 212                 }
 213             }
 214             return 0;
 215         }
 216 
 217         /**
 218          * A ClassPathEntry visitor to copy a file to the given destination
 219          * if not filtered.
 220          */
 221         class Copier implements ClassPathEntry.Visitor<ClassPathEntry, String> {
 222             final Filter filter;
 223             final File dest;
 224             long bytes = 0;
 225 
 226             Copier(File dest, Filter filter) {
 227                 this.filter = filter;
 228                 this.dest = dest;
 229             }
 230 
 231             @Override
 232             public ClassPathEntry visitFile(File src, ClassPathEntry cp, String pathname) throws IOException {
 233                 String name = pathname.replace(File.separatorChar, '/');
 234                 if (cp.getName().endsWith(File.separator + pathname)
 235                         && matches(src, name)) {
 236                     if (filter == null || filter.accept(src)) {
 237                         File dst = new File(dest, pathname);
 238                         bytes += copy(src, dst);
 239                     }
 240                     return cp;
 241                 } else {
 242                     return null;
 243                 }
 244             }
 245 
 246             @Override
 247             public ClassPathEntry visitDir(File dir, ClassPathEntry cp, String pathname) throws IOException {
 248                 File src = new File(cp.getFile(), pathname);
 249                 File dst = new File(dest, pathname);
 250                 String name = pathname.replace(File.separatorChar, '/');
 251 
 252                 if (src.exists() && matches(src, name)) {
 253                     if (filter == null || filter.accept(src)) {
 254                         bytes += copy(src, dst);
 255                     }
 256                     return cp;
 257                 } else {
 258                     return null;
 259                 }
 260             }
 261 
 262             @Override
 263             public ClassPathEntry visitJarFile(JarFile jf, ClassPathEntry cp, String pathname) throws IOException {
 264                 String name = pathname.replace(File.separatorChar, '/');
 265                 JarEntry e = jf.getJarEntry(name);
 266                 if (e != null && matches(jf, e, name)) {
 267                     if (filter == null || filter.accept(jf, e)) {
 268                         bytes += copy(jf, e);
 269                     }
 270                     return cp;
 271                 } else {
 272                     return null;
 273                 }
 274             }
 275 
 276             boolean matches(File src, String name) throws IOException {
 277                 if (!name.startsWith("META-INF/services")) {
 278                     return true;
 279                 }
 280 
 281                 BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));
 282                 try {
 283                     return matches(in, name);
 284                 } finally {
 285                     in.close();
 286                 }
 287             }
 288 
 289             boolean matches(JarFile jf, JarEntry e, String name) throws IOException {
 290                 if (!name.startsWith("META-INF/services")) {
 291                     return true;
 292                 }
 293                 return matches(jf.getInputStream(e), name);
 294             }
 295 
 296             boolean matches(InputStream in, String name) throws IOException {
 297                 ServiceProviderConfigFile sp = new ServiceProviderConfigFile(name, in);
 298                 for (String p : sp.providers) {
 299                     Klass k = Klass.findKlass(p);
 300                     if (k == null) {
 301                         Trace.trace("Service %s: provider class %s not found%n", sp.service, p);
 302                         continue;
 303                     }
 304                     if (module.contains(k)) {
 305                         return true;
 306                     }
 307                 }
 308                 // return true if no provider; otherwise false
 309                 return sp.providers.isEmpty();
 310             }
 311 
 312             long copy(JarFile jf, JarEntry e) throws IOException {
 313                 File dst = new File(dest, e.getName().replace('/', File.separatorChar));
 314                 if (!dst.exists()) {
 315                     Files.createFile(dst);
 316                 }
 317 
 318                 byte[] buf = new byte[8192];
 319                 InputStream in = jf.getInputStream(e);
 320                 long bytes = 0;
 321                 try {
 322                     FileOutputStream out = new FileOutputStream(dst);
 323                     try {
 324                         int n;
 325                         while ((n = in.read(buf)) > 0) {
 326                             out.write(buf, 0, n);
 327                             bytes += n;
 328                         }
 329                     } finally {
 330                         out.close();
 331                     }
 332                 } finally {
 333                     in.close();
 334                 }
 335 
 336                 long lastModified = e.getTime();
 337                 if (lastModified > 0) {
 338                     dst.setLastModified(lastModified);
 339                 }
 340                 return bytes;
 341             }
 342 
 343             long copy(File src, File dst)
 344                     throws IOException {
 345                 assert src.exists();
 346 
 347                 if (!dst.exists()) {
 348                     Files.createFile(dst);
 349                 }
 350 
 351                 BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));
 352                 byte[] buf = new byte[8192];
 353                 long bytes = 0;
 354                 try {
 355                     FileOutputStream out = new FileOutputStream(dst);
 356                     try {
 357                         int n;
 358                         while ((n = in.read(buf)) > 0) {
 359                             out.write(buf, 0, n);
 360                             bytes += n;
 361                         }
 362                     } finally {
 363                         out.close();
 364                     }
 365                 } finally {
 366                     in.close();
 367                 }
 368                 dst.setLastModified(src.lastModified());
 369                 if (src.canExecute()) {
 370                     dst.setExecutable(true, false);
 371                 }
 372                 return bytes;
 373             }
 374         }
 375     }
 376 
 377     /**
 378      * A filter that accepts files that don't exist in the given
 379      * location or modified since it's copied.
 380      */
 381     class Filter implements ClassPath.Filter {
 382         private final long timestamp;
 383         Filter(File dir, String pathname) {
 384             File destfile = new File(dir, pathname);
 385             this.timestamp = destfile.exists() ? destfile.lastModified() : -1L;
 386         }
 387 
 388         @Override
 389         public boolean accept(File f) throws IOException {
 390             if (f.isDirectory()) {
 391                 return true;
 392             }
 393 
 394             long ts = f.lastModified();
 395             return (timestamp < 0 || ts < 0 || timestamp < ts);
 396         }
 397 
 398         @Override
 399         public boolean accept(JarFile jf, JarEntry e) throws IOException {
 400             long ts = e.getTime();
 401             return timestamp < 0 || ts < 0 || timestamp < ts;
 402         }
 403     }
 404 
 405     public static void main(String[] args) throws Exception {
 406         String jdkhome = null;
 407         String classpath = null;
 408         String classlistDir = null;
 409         String modulepath = null;
 410         boolean update = false;
 411 
 412         // process arguments
 413         int i = 0;
 414         while (i < args.length) {
 415             String arg = args[i++];
 416             if (arg.equals("-jdkhome")) {
 417                 jdkhome = getOption(args, i++);
 418             } else if (arg.equals("-classpath")) {
 419                 classpath = getOption(args, i++);
 420             } else if (arg.equals("-modulepath")) {
 421                 modulepath = getOption(args, i++);
 422             } else if (arg.equals("-classlistdir")) {
 423                 classlistDir = getOption(args, i++);
 424             } else if (arg.equals("-update")) {
 425                 // copy new files only
 426                 update = true;
 427             } else {
 428                 error("Invalid option: " + arg);
 429             }
 430         }
 431 
 432         if (jdkhome == null && classpath == null) {
 433             error("-jdkhome and -classpath not set");
 434         }
 435 
 436         if (jdkhome != null && classpath != null) {
 437             error("Both -jdkhome and -classpath are set");
 438         }
 439 
 440         if (classlistDir == null || modulepath == null) {
 441             error("-modulepath or -classlist not set");
 442         }
 443 
 444         ClassPath cpath = null;
 445         if (jdkhome != null) {
 446             cpath = ClassPath.newJDKClassPath(jdkhome);
 447         } else if (classpath != null) {
 448             cpath = ClassPath.newInstance(classpath);
 449         }
 450 
 451         ClassListReader reader = new ClassListReader(classlistDir, "default");
 452         Set<Module> modules = reader.run();
 453         Modularizer modularizer = new Modularizer(cpath, new File(modulepath), modules);
 454         modularizer.run(update);
 455     }
 456 
 457     private static String getOption(String[] args, int index) {
 458         if (index < args.length) {
 459             return args[index];
 460         } else {
 461             error(args[index-1] + ": Missing argument");
 462         }
 463         return null;
 464     }
 465 
 466     private static void error(String msg) {
 467         System.err.println("ERROR: " + msg);
 468         System.out.println(usage());
 469         System.exit(-1);
 470     }
 471 
 472     private static String usage() {
 473         StringBuilder sb = new StringBuilder();
 474         sb.append("Usage: Modularizer <options>\n");
 475         sb.append("Options: \n");
 476         sb.append("\t-jdkhome      <JDK home> where all jars will be parsed\n");
 477         sb.append("\t-classpath    <classpath> where classes and jars will be parsed\n");
 478         sb.append("\t              Either -jdkhome or -classpath option can be used.\n");
 479         sb.append("\t-classlistdir <classlist dir>\n");
 480         sb.append("\t-update       update modules with newer files\n");
 481         sb.append("\t-modulepath   <module-path> for writing modules\n");
 482         return sb.toString();
 483     }
 484 }