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 }