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 }