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 }