1 /*
   2  * Copyright (c) 2010, 2012, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package org.openjdk.jigsaw;
  27 
  28 import java.io.*;
  29 import java.nio.channels.*;
  30 import java.nio.file.Files;
  31 import java.nio.file.*;
  32 import java.util.*;
  33 import java.lang.module.*;
  34 import java.net.URI;
  35 
  36 import org.openjdk.jigsaw.ModuleFile.ModuleFileHeader;
  37 import org.openjdk.jigsaw.RepositoryCatalog.Entry;
  38 import org.openjdk.jigsaw.RepositoryCatalog.StreamedRepositoryCatalog;
  39 
  40 import static java.lang.System.out;
  41 
  42 import static java.nio.file.StandardOpenOption.*;
  43 import static java.nio.file.StandardCopyOption.*;
  44 import java.security.DigestInputStream;
  45 import java.security.MessageDigest;
  46 import java.security.NoSuchAlgorithmException;
  47 import java.util.jar.JarEntry;
  48 import java.util.jar.JarFile;
  49 import org.openjdk.jigsaw.FileConstants.ModuleFile.HashType;
  50 
  51 
  52 /**
  53  * <p> A local module repository, to which modules can be published </p>
  54  */
  55 
  56 public class PublishedRepository
  57     extends Repository
  58 {
  59 
  60     private static final JigsawModuleSystem jms
  61         = JigsawModuleSystem.instance();
  62 
  63     private final URI uri;
  64     private final Path path;
  65 
  66     private static final String CATALOG_FILE = "%catalog";
  67     private static final String LOCK_FILE = "%lock";
  68 
  69     private final Path catp;
  70     private final Path lockp;
  71 
  72     @Override
  73     public String name() {
  74         return path.toString();
  75     }
  76 
  77     @Override
  78     public String toString() {
  79         return name();
  80     }
  81 
  82     @Override
  83     public URI location() {
  84         return uri;
  85     }
  86 
  87     private StreamedRepositoryCatalog loadCatalog()
  88         throws IOException
  89     {
  90         return RepositoryCatalog.load(Files.newInputStream(catp, READ));
  91     }
  92 
  93     private void storeCatalogWhileLocked(StreamedRepositoryCatalog cat)
  94         throws IOException
  95     {
  96         Path newp = path.resolve(CATALOG_FILE + ".new");
  97         try (OutputStream os = Files.newOutputStream(newp, WRITE, CREATE_NEW)) {
  98             cat.store(os);
  99         }
 100         try {
 101             Files.move(newp, catp, ATOMIC_MOVE);
 102         } catch (IOException x) {
 103             Files.deleteIfExists(newp);
 104             throw x;
 105         }
 106     }
 107 
 108     private void storeCatalog(StreamedRepositoryCatalog cat)
 109         throws IOException
 110     {
 111         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 112             lc.lock();
 113             storeCatalogWhileLocked(cat);
 114         }
 115     }
 116 
 117     private PublishedRepository(Path p, boolean create) throws IOException {
 118         path = p;
 119         uri = path.toUri();
 120         catp = path.resolve(CATALOG_FILE);
 121         lockp = path.resolve(LOCK_FILE);
 122         if (Files.exists(path)) {
 123             loadCatalog();              // Just to validate
 124         } else if (create) {
 125             Files.createDirectory(path);
 126             Files.createFile(lockp);
 127             storeCatalog(RepositoryCatalog.load(null));
 128         } else {
 129             throw new NoSuchFileException(p.toString());
 130         }
 131     }
 132 
 133     public static PublishedRepository open(Path p, boolean create)
 134         throws IOException
 135     {
 136         return new PublishedRepository(p, create);
 137     }
 138 
 139     public static PublishedRepository open(File f, boolean create)
 140         throws IOException
 141     {
 142         return open(f.toPath(), create);
 143     }
 144 
 145     @Override
 146     public PublishedRepository parent() { return null; }
 147 
 148     @Override
 149     protected void gatherLocalModuleIds(String moduleName,
 150                                         Set<ModuleId> mids)
 151         throws IOException
 152     {
 153         RepositoryCatalog cat = loadCatalog();
 154         cat.gatherModuleIds(moduleName, mids);
 155     }
 156 
 157     @Override
 158     protected void gatherLocalDeclaringModuleIds(Set<ModuleId> mids)
 159         throws IOException
 160     {
 161         RepositoryCatalog cat = loadCatalog();
 162         cat.gatherDeclaringModuleIds(mids);
 163     }
 164 
 165     @Override
 166     protected ModuleInfo readLocalModuleInfo(ModuleId mid)
 167         throws IOException
 168     {
 169         RepositoryCatalog cat = loadCatalog();
 170         Entry e = cat.get(mid);
 171         if (e == null)
 172             throw new IllegalArgumentException(mid + ": No such module");
 173         return jms.parseModuleInfo(e.mibs);
 174     }
 175 
 176     private ModuleFileType getModuleFileType(Path modp) {
 177         for (ModuleFileType type: ModuleFileType.values()) {
 178             if (modp.getFileName().toString().endsWith(type.getFileNameSuffix())) {
 179                 return type;
 180             }
 181         }
 182 
 183         // ## check magic numbers?
 184         throw new IllegalArgumentException(modp + ": Unrecognized module file");
 185     }
 186 
 187     private Path getModulePath(ModuleId mid, String ext) throws IOException {
 188         return path.resolve(mid.toString() + ext);
 189     }
 190 
 191     private Path getModulePath(ModuleId mid, ModuleFileType type) throws IOException {
 192         return getModulePath(mid, type.getFileNameSuffix());
 193     }
 194 
 195     private Path getModulePath(ModuleId mid) throws IOException {
 196         for (ModuleFileType type: ModuleFileType.values()) {
 197             Path file = getModulePath(mid, type);
 198             if (Files.exists(file)) {
 199                 return file;
 200             }
 201         }
 202         throw new IllegalArgumentException(mid + ": No such module file");
 203     }
 204 
 205     private Entry readModuleInfo(Path modp) throws IOException {
 206         return readModuleInfo(modp, getModuleFileType(modp));
 207     }
 208 
 209     private Entry getModuleInfo(ModuleId mid) throws IOException {
 210         return readModuleInfo(getModulePath(mid));
 211     }
 212 
 213     private Entry readModuleInfo(Path modp, ModuleFileType type) throws IOException {
 214         switch(getModuleFileType(modp)) {
 215             case JAR:
 216                 return readModuleInfoFromModularJarFile(modp);
 217             case JMOD:
 218                 return readModuleInfoFromJmodFile(modp);
 219             default:
 220                 // Cannot occur;
 221                 throw new AssertionError();
 222         }
 223     }
 224 
 225     private Entry readModuleInfoFromJmodFile(Path modp) throws IOException {
 226         try (InputStream mfis = Files.newInputStream(modp)) {
 227             ValidatingModuleFileParser parser =
 228                     ModuleFile.newValidatingParser(mfis);
 229 
 230             ModuleFileHeader mfh = parser.fileHeader();
 231 
 232             // Move to the module info section
 233             parser.next();
 234 
 235             return new Entry(ModuleFileType.JMOD,
 236                              mfh.getArchitecture(),
 237                              toByteArray(parser.getContentStream()),
 238                              mfh.getCSize(),
 239                              mfh.getUSize(),
 240                              mfh.getHashType(),
 241                              mfh.getHash());
 242         }
 243     }
 244 
 245     private Entry readModuleInfoFromModularJarFile(Path modp) throws IOException {
 246         File jf = modp.toFile();
 247         try (JarFile j = new JarFile(jf)) {
 248             JarEntry moduleInfo = j.getJarEntry(JarFile.MODULEINFO_NAME);
 249             if (moduleInfo == null) {
 250                 throw new IllegalArgumentException(modp + ": not a modular JAR file");
 251             }
 252 
 253             long usize = 0;
 254             for (JarEntry je: Collections.list(j.entries())) {
 255                 if (je.isDirectory()) {
 256                     continue;
 257                 }
 258 
 259                 usize += je.getSize();
 260             }
 261 
 262             return new Entry(ModuleFileType.JAR,
 263                             ModuleArchitecture.ANY,
 264                             toByteArray(j, moduleInfo),
 265                             jf.length(),
 266                             usize,
 267                             HashType.SHA256,
 268                             digest(jf));
 269         }
 270     }
 271 
 272     private byte[] digest(File f) throws IOException {
 273         MessageDigest md;
 274         try {
 275              md = MessageDigest.getInstance("SHA-256");
 276         } catch (NoSuchAlgorithmException ex) {
 277             // Cannot occur
 278             throw new AssertionError();
 279         }
 280 
 281         try (DigestInputStream in = new DigestInputStream(new FileInputStream(f), md)) {
 282             byte[] buf = new byte[4096];
 283             while (in.read(buf) != -1) {
 284             }
 285 
 286             return in.getMessageDigest().digest();
 287         }
 288     }
 289 
 290     private byte[] toByteArray(JarFile j, JarEntry je) throws IOException {
 291         try (InputStream in = j.getInputStream(je)) {
 292             return toByteArray(in);
 293         }
 294     }
 295 
 296     private byte[] toByteArray(InputStream in) throws IOException {
 297         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
 298         final byte buf[] = new byte[4096];
 299         int len;
 300         while ((len = in.read(buf)) != -1) {
 301             baos.write(buf, 0, len);
 302         }
 303         return baos.toByteArray();
 304     }
 305 
 306     public void publish(Path modp) throws IOException {
 307         Entry e = readModuleInfo(modp);
 308         ModuleInfo mi = jms.parseModuleInfo(e.mibs);
 309         ModuleId mid = mi.id();
 310         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 311             lc.lock();
 312 
 313             // Update the module file first
 314             Path dstp = getModulePath(mid, e.type);
 315             Path newp = getModulePath(mid, e.type.getFileNameSuffix() + ".new");
 316             try {
 317                 Files.copy(modp, newp, REPLACE_EXISTING);
 318                 Files.move(newp, dstp, ATOMIC_MOVE);
 319             } catch (IOException x) {
 320                 Files.deleteIfExists(newp);
 321                 throw x;
 322             }
 323 
 324             // Then update the catalog
 325             StreamedRepositoryCatalog cat = loadCatalog();
 326             cat.add(e);
 327             storeCatalogWhileLocked(cat);
 328         }
 329     }
 330 
 331     @Override
 332     public InputStream fetch(ModuleId mid) throws IOException {
 333         RepositoryCatalog cat = loadCatalog();
 334         Entry e = cat.get(mid);
 335         if (e == null)
 336             throw new IllegalArgumentException(mid + ": No such module");
 337         return Files.newInputStream(getModulePath(mid, e.type));
 338     }
 339 
 340     @Override
 341     public ModuleFileMetaData fetchMetaData(ModuleId mid) throws IOException {
 342         RepositoryCatalog cat = loadCatalog();
 343         Entry e = cat.get(mid);
 344         if (e == null)
 345             throw new IllegalArgumentException(mid + ": No such module");
 346         return new ModuleFileMetaData(e.type, e.modArch, e.csize, e.usize);
 347     }
 348 
 349     public boolean remove(ModuleId mid) throws IOException {
 350         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 351             lc.lock();
 352 
 353             // Update catalog first
 354             StreamedRepositoryCatalog cat = loadCatalog();
 355             Entry e = cat.get(mid);
 356             if (!cat.remove(mid))
 357                 return false;
 358             storeCatalogWhileLocked(cat);
 359 
 360             // Then remove the file
 361             Files.delete(getModulePath(mid, e.type));
 362         }
 363 
 364         return true;
 365     }
 366 
 367     private <T> Set<T> del(Set<T> all, Set<T> todel) {
 368         Set<T> s = new HashSet<>(all);
 369         s.removeAll(todel);
 370         return s;
 371     }
 372 
 373     private void gatherModuleIdsFromDirectoryWhileLocked(Set<ModuleId> mids)
 374         throws IOException
 375     {
 376         // ## Change to use String joiner when lamda is merged into JDK8
 377         StringBuilder sb = new StringBuilder();
 378         sb.append("*.{");
 379         int l = sb.length();
 380         for (ModuleFileType type: ModuleFileType.values()) {
 381             if (sb.length() > l) {
 382                 sb.append(",");
 383             }
 384             sb.append(type.getFileNameExtension());
 385         }
 386         sb.append("}");
 387         try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, sb.toString())) {
 388             for (Path modp : ds) {
 389                 ModuleFileType type = getModuleFileType(modp);
 390                 String fn = modp.getFileName().toString();
 391                 ModuleId mid
 392                     = jms.parseModuleId(fn.substring(0, fn.length() - type.getFileNameSuffix().length()));
 393                 mids.add(mid);
 394             }
 395         }
 396     }
 397 
 398     private static void msg(List<String> msgs, String fmt, Object ... args) {
 399         String msg = String.format(fmt, args);
 400         if (msgs != null)
 401             msgs.add(msg);
 402         else
 403             out.println(msg);
 404     }
 405 
 406     public boolean validate(List<String> msgs) throws IOException {
 407         int errors = 0;
 408         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 409             lc.lock();
 410 
 411             StreamedRepositoryCatalog cat = loadCatalog();
 412             Set<ModuleId> cmids = new HashSet<>();
 413             cat.gatherDeclaringModuleIds(cmids);
 414 
 415             Set<ModuleId> fmids = new HashSet<>();
 416             gatherModuleIdsFromDirectoryWhileLocked(fmids);
 417 
 418             if (!cmids.equals(fmids)) {
 419                 errors++;
 420                 msg(msgs, "%s: Catalog and directory do not match",
 421                     path);
 422                 if (!cmids.containsAll(fmids))
 423                     msg(msgs, "  Extra module files: %s", del(fmids, cmids));
 424                 if (!fmids.containsAll(cmids))
 425                     msg(msgs, "  Extra catalog entries: %s", del(cmids, fmids));
 426             }
 427 
 428             cmids.retainAll(fmids);
 429             for (ModuleId mid : cmids) {
 430                 byte[] cmibs = cat.readModuleInfoBytes(mid);
 431                 Entry e = getModuleInfo(mid);
 432                 if (!Arrays.equals(cmibs, e.mibs)) {
 433                     errors++;
 434                     msg(msgs, "%s: %s: Module-info files do not match",
 435                         path, mid);
 436                 }
 437             }
 438         }
 439 
 440         return errors == 0;
 441     }
 442 
 443     public void reCatalog() throws IOException {
 444         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 445             lc.lock();
 446 
 447             Set<ModuleId> mids = new HashSet<>();
 448             gatherModuleIdsFromDirectoryWhileLocked(mids);
 449 
 450             StreamedRepositoryCatalog cat = RepositoryCatalog.load(null);
 451             for (ModuleId mid : mids) {
 452                 Entry e = getModuleInfo(mid);
 453                 cat.add(e);
 454             }
 455             storeCatalogWhileLocked(cat);
 456         }
 457     }
 458 }