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 ModuleType getModuleType(Path modp) {
 177         for (ModuleType type: ModuleType.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, ModuleType type) throws IOException {
 192         return getModulePath(mid, type.getFileNameSuffix());
 193     }
 194 
 195     private Path getModulePath(ModuleId mid) throws IOException {
 196         for (ModuleType type: ModuleType.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, getModuleType(modp));
 207     }
 208       
 209     private Entry getModuleInfo(ModuleId mid) throws IOException {
 210         return readModuleInfo(getModulePath(mid));
 211     }
 212     
 213     private Entry readModuleInfo(Path modp, ModuleType type) throws IOException {
 214         switch(getModuleType(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(ModuleType.JMOD, 

 236                              toByteArray(parser.getContentStream()), 
 237                              mfh.getCSize(), 
 238                              mfh.getUSize(), 
 239                              mfh.getHashType(), 
 240                              mfh.getHash());
 241         }
 242     }
 243 
 244     private Entry readModuleInfoFromModularJarFile(Path modp) throws IOException {
 245         File jf = modp.toFile();
 246         try (JarFile j = new JarFile(jf)) {
 247             JarEntry moduleInfo = j.getJarEntry(JarFile.MODULEINFO_NAME);
 248             if (moduleInfo == null) {
 249                 throw new IllegalArgumentException(modp + ": not a modular JAR file");                
 250             }
 251                         
 252             long usize = 0;
 253             for (JarEntry je: Collections.list(j.entries())) {
 254                 if (je.isDirectory()) {
 255                     continue;
 256                 }
 257                 
 258                 usize += je.getSize();
 259             }
 260                         
 261             return new Entry(ModuleType.JAR, 

 262                             toByteArray(j, moduleInfo),
 263                             jf.length(), 
 264                             usize, 
 265                             HashType.SHA256,
 266                             digest(jf));   
 267         }
 268     }
 269     
 270     private byte[] digest(File f) throws IOException {
 271         MessageDigest md;
 272         try {
 273              md = MessageDigest.getInstance("SHA-256");
 274         } catch (NoSuchAlgorithmException ex) {
 275             // Cannot occur
 276             throw new AssertionError();
 277         }
 278 
 279         try (DigestInputStream in = new DigestInputStream(new FileInputStream(f), md)) {
 280             byte[] buf = new byte[4096];
 281             while (in.read(buf) != -1) {
 282             }
 283             
 284             return in.getMessageDigest().digest();
 285         }
 286     }
 287     
 288     private byte[] toByteArray(JarFile j, JarEntry je) throws IOException {
 289         try (InputStream in = j.getInputStream(je)) {    
 290             return toByteArray(in);            
 291         }
 292     }
 293     
 294     private byte[] toByteArray(InputStream in) throws IOException {
 295         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
 296         final byte buf[] = new byte[4096];
 297         int len;
 298         while ((len = in.read(buf)) != -1) {
 299             baos.write(buf, 0, len);
 300         }
 301         return baos.toByteArray();
 302     } 
 303 
 304     public void publish(Path modp) throws IOException {
 305         Entry e = readModuleInfo(modp);
 306         ModuleInfo mi = jms.parseModuleInfo(e.mibs);
 307         ModuleId mid = mi.id();
 308         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 309             lc.lock();
 310 
 311             // Update the module file first
 312             Path dstp = getModulePath(mid, e.type);
 313             Path newp = getModulePath(mid, e.type.getFileNameSuffix() + ".new");
 314             try {
 315                 Files.copy(modp, newp, REPLACE_EXISTING);
 316                 Files.move(newp, dstp, ATOMIC_MOVE);
 317             } catch (IOException x) {
 318                 Files.deleteIfExists(newp);
 319                 throw x;
 320             }
 321 
 322             // Then update the catalog
 323             StreamedRepositoryCatalog cat = loadCatalog();
 324             cat.add(e);
 325             storeCatalogWhileLocked(cat);
 326         }
 327     }
 328 
 329     @Override
 330     public InputStream fetch(ModuleId mid) throws IOException {
 331         RepositoryCatalog cat = loadCatalog();
 332         Entry e = cat.get(mid);
 333         if (e == null)
 334             throw new IllegalArgumentException(mid + ": No such module");
 335         return Files.newInputStream(getModulePath(mid, e.type));
 336     }
 337 
 338     @Override
 339     public ModuleMetaData fetchMetaData(ModuleId mid) throws IOException {
 340         RepositoryCatalog cat = loadCatalog();
 341         Entry e = cat.get(mid);
 342         if (e == null)
 343             throw new IllegalArgumentException(mid + ": No such module");
 344         return new ModuleMetaData(e.type, e.csize, e.usize);
 345     }
 346 
 347     public boolean remove(ModuleId mid) throws IOException {
 348         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 349             lc.lock();
 350 
 351             // Update catalog first
 352             StreamedRepositoryCatalog cat = loadCatalog();
 353             Entry e = cat.get(mid);
 354             if (!cat.remove(mid))
 355                 return false;
 356             storeCatalogWhileLocked(cat);
 357 
 358             // Then remove the file
 359             Files.delete(getModulePath(mid, e.type));
 360         }
 361 
 362         return true;
 363     }
 364 
 365     private <T> Set<T> del(Set<T> all, Set<T> todel) {
 366         Set<T> s = new HashSet<>(all);
 367         s.removeAll(todel);
 368         return s;
 369     }
 370 
 371     private void gatherModuleIdsFromDirectoryWhileLocked(Set<ModuleId> mids)
 372         throws IOException
 373     {
 374         // ## Change to use String joiner when lamda is merged into JDK8
 375         StringBuilder sb = new StringBuilder();
 376         sb.append("*.{");
 377         int l = sb.length();
 378         for (ModuleType type: ModuleType.values()) {
 379             if (sb.length() > l) {
 380                 sb.append(",");
 381             }
 382             sb.append(type.getFileNameExtension());
 383         }
 384         sb.append("}");
 385         try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, sb.toString())) { 
 386             for (Path modp : ds) {
 387                 ModuleType type = getModuleType(modp);
 388                 String fn = modp.getFileName().toString();
 389                 ModuleId mid
 390                     = jms.parseModuleId(fn.substring(0, fn.length() - type.getFileNameSuffix().length()));
 391                 mids.add(mid);
 392             }
 393         }
 394     }
 395 
 396     private static void msg(List<String> msgs, String fmt, Object ... args) {
 397         String msg = String.format(fmt, args);
 398         if (msgs != null)
 399             msgs.add(msg);
 400         else
 401             out.println(msg);
 402     }
 403 
 404     public boolean validate(List<String> msgs) throws IOException {
 405         int errors = 0;
 406         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 407             lc.lock();
 408 
 409             StreamedRepositoryCatalog cat = loadCatalog();
 410             Set<ModuleId> cmids = new HashSet<>();
 411             cat.gatherDeclaringModuleIds(cmids);
 412 
 413             Set<ModuleId> fmids = new HashSet<>();
 414             gatherModuleIdsFromDirectoryWhileLocked(fmids);
 415 
 416             if (!cmids.equals(fmids)) {
 417                 errors++;
 418                 msg(msgs, "%s: Catalog and directory do not match",
 419                     path);
 420                 if (!cmids.containsAll(fmids))
 421                     msg(msgs, "  Extra module files: %s", del(fmids, cmids));
 422                 if (!fmids.containsAll(cmids))
 423                     msg(msgs, "  Extra catalog entries: %s", del(cmids, fmids));
 424             }
 425 
 426             cmids.retainAll(fmids);
 427             for (ModuleId mid : cmids) {
 428                 byte[] cmibs = cat.readModuleInfoBytes(mid);
 429                 Entry e = getModuleInfo(mid);
 430                 if (!Arrays.equals(cmibs, e.mibs)) {
 431                     errors++;
 432                     msg(msgs, "%s: %s: Module-info files do not match",
 433                         path, mid);
 434                 }
 435             }
 436         }
 437 
 438         return errors == 0;
 439     }
 440 
 441     public void reCatalog() throws IOException {
 442         try (FileChannel lc = FileChannel.open(lockp, READ, WRITE)) {
 443             lc.lock();
 444 
 445             Set<ModuleId> mids = new HashSet<>();
 446             gatherModuleIdsFromDirectoryWhileLocked(mids);
 447 
 448             StreamedRepositoryCatalog cat = RepositoryCatalog.load(null);
 449             for (ModuleId mid : mids) {
 450                 Entry e = getModuleInfo(mid);
 451                 cat.add(e);
 452             }
 453             storeCatalogWhileLocked(cat);
 454         }
 455     }
 456 }
--- EOF ---