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.lang.module.*; 30 import java.util.*; 31 import java.net.*; 32 import java.nio.file.Files; 33 import java.nio.file.Path; 34 import java.nio.file.Paths; 35 import static java.nio.file.StandardCopyOption.*; 36 37 import static org.openjdk.jigsaw.Trace.*; 38 39 40 /** 41 * <p> A remote module repository, whose catalog is cached locally </p> 42 */ 43 44 public class RemoteRepository 45 extends Repository 46 { 47 48 private static final JigsawModuleSystem jms 49 = JigsawModuleSystem.instance(); 50 51 /* 52 public static interface ProgressWatcher { 53 public void start(int min, int max); 54 public void progress(int cur); 55 public void finish(); 56 } 57 */ 58 59 @Override 60 public String name() { return null; } 61 62 private final RemoteRepository parent; 63 @Override 64 public RemoteRepository parent() { return null; } 65 66 private URI uri; 67 68 @Override 69 public URI location() { 70 return uri; 71 } 72 73 static URI canonicalize(URI u) 74 throws IOException 75 { 76 String host = u.getHost(); 77 if (host != null) { 78 InetAddress ia = InetAddress.getByName(host); 79 String chn = ia.getCanonicalHostName().toLowerCase(); 80 String p = u.getPath(); 81 if (p == null) 82 p = "/"; 83 else if (!p.endsWith("/")) 84 p += "/"; 85 try { 86 return new URI(u.getScheme(), 87 u.getUserInfo(), 88 chn, 89 u.getPort(), 90 p, 91 u.getQuery(), 92 u.getFragment()); 93 } catch (URISyntaxException x) { 94 throw new AssertionError(x); 95 } 96 } else { 97 String s = u.toString(); 98 if (s.endsWith("/")) 99 return u; 100 return URI.create(s + "/"); 101 } 102 } 103 104 private File dir; 105 private long id; 106 private File metaFile; 107 private File catFile; 108 109 private RemoteRepository(File d, long i, RemoteRepository p) { 110 dir = d; 111 id = i; 112 parent = p; 113 metaFile = new File(dir, "meta"); 114 catFile = new File(dir, "catalog"); 115 } 116 117 public long id() { return id; } 118 119 private static int MAJOR_VERSION = 0; 120 private static int MINOR_VERSION = 0; 121 122 private static FileHeader fileHeader() { 123 return (new FileHeader() 124 .type(FileConstants.Type.REMOTE_REPO_META) 125 .majorVersion(MAJOR_VERSION) 126 .minorVersion(MINOR_VERSION)); 127 } 128 129 private long mtime = 0; 130 private String etag = null; 131 132 private void loadMeta() 133 throws IOException 134 { 135 if (!metaFile.exists()) 136 return; 137 FileInputStream fin = new FileInputStream(metaFile); 138 try (DataInputStream in = new DataInputStream(new BufferedInputStream(fin))) { 139 FileHeader fh = fileHeader(); 140 fh.read(in); 141 uri = URI.create(in.readUTF()); 142 mtime = in.readLong(); 143 String et = in.readUTF(); 144 etag = (et.length() == 0) ? null : et; 145 } 146 } 147 148 private void storeMeta() 149 throws IOException 150 { 151 File newfn = new File(dir, "meta.new"); 152 try (FileOutputStream fos = new FileOutputStream(newfn); 153 DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fos))) 154 { 155 fileHeader().write(out); 156 out.writeUTF(uri.toString()); 157 out.writeLong(mtime); 158 out.writeUTF(etag == null ? "" : etag); 159 160 } catch (IOException x) { 161 deleteAfterException(newfn, x); 162 throw x; 163 } 164 // move meta data into place 165 try { 166 Files.move(newfn.toPath(), metaFile.toPath(), ATOMIC_MOVE); 167 } catch (IOException x) { 168 deleteAfterException(newfn, x); 169 throw x; 170 } 171 } 172 173 public static RemoteRepository create(File dir, URI u, long id) 174 throws IOException 175 { 176 if (u.isOpaque()) 177 throw new IllegalArgumentException(u + ": Opaque URIs not supported"); 178 RemoteRepository rr = new RemoteRepository(dir, id, null); 179 rr.uri = canonicalize(u.normalize()); 180 if (dir.exists()) 181 throw new IllegalStateException(dir + ": Already exists"); 182 if (!dir.mkdir()) 183 throw new IOException(dir + ": Cannot create directory"); 184 try { 185 rr.storeMeta(); 186 } catch (IOException x) { 187 deleteAfterException(dir, x); 188 throw x; 189 } 190 return rr; 191 } 192 193 public static RemoteRepository create(File dir, URI u) 194 throws IOException 195 { 196 return create(dir, u, -1); 197 } 198 199 public static RemoteRepository open(File dir, long id, 200 RemoteRepository parent) 201 throws IOException 202 { 203 RemoteRepository rr = new RemoteRepository(dir, id, parent); 204 if (!dir.exists()) 205 throw new IllegalStateException(dir + ": No such directory"); 206 if (!dir.isDirectory()) 207 throw new IOException(dir + ": Not a directory"); 208 rr.loadMeta(); 209 return rr; 210 } 211 212 public static RemoteRepository open(File dir, long id) 213 throws IOException 214 { 215 return open(dir, id, null); 216 } 217 218 public static RemoteRepository open(File dir) 219 throws IOException 220 { 221 return open(dir, -1); 222 } 223 224 /** 225 * Deletes this remote repository, including the directory. 226 */ 227 public void delete() throws IOException { 228 Files.deleteIfExists(catFile.toPath()); 229 Files.deleteIfExists(metaFile.toPath()); 230 Files.deleteIfExists(dir.toPath()); 231 } 232 233 private RepositoryCatalog cat = null; 234 235 private boolean fetchCatalog(boolean head, boolean force) 236 throws IOException 237 { 238 239 URI u = uri.resolve("%25catalog"); 240 if (tracing) 241 trace(1, "fetching catalog %s (head %s, force %s)", u, head, force); 242 243 // special-case file protocol for faster copy 244 if (u.getScheme().equalsIgnoreCase("file")) { 245 Path newfn = dir.toPath().resolve("catalog.new"); 246 try { 247 Files.copy(Paths.get(u), newfn); 248 Files.move(newfn, catFile.toPath(), ATOMIC_MOVE); 249 } catch (IOException x) { 250 Files.deleteIfExists(newfn); 251 throw x; 252 } 253 } else { 254 URLConnection uc = u.toURL().openConnection(); 255 if (uc instanceof HttpURLConnection) { 256 HttpURLConnection http = (HttpURLConnection)uc; 257 http.setInstanceFollowRedirects(true); 258 if (!force) { 259 if (mtime != 0) 260 uc.setIfModifiedSince(mtime); 261 if (etag != null) 262 uc.setRequestProperty("If-None-Match", etag); 263 if (tracing) 264 trace(2, "old mtime %d, etag %s", mtime, etag); 265 } 266 if (head) 267 http.setRequestMethod("HEAD"); 268 http.connect(); 269 270 int rc = http.getResponseCode(); 271 if (tracing) 272 trace(2, "response: %s", http.getResponseMessage()); 273 if (rc == HttpURLConnection.HTTP_NOT_MODIFIED) { 274 return false; 275 } 276 if (rc != HttpURLConnection.HTTP_OK) 277 throw new IOException(u + ": " + http.getResponseMessage()); 278 } 279 280 Path newfn = dir.toPath().resolve("catalog.new"); 281 try (InputStream in = uc.getInputStream()) { 282 long t = Files.copy(in, newfn); 283 if (tracing) 284 trace(2, "%d catalog bytes read", t); 285 Files.move(newfn, catFile.toPath(), ATOMIC_MOVE); 286 } catch (IOException x) { 287 Files.deleteIfExists(newfn); 288 throw x; 289 } 290 291 mtime = uc.getHeaderFieldDate("Last-Modified", 0); 292 etag = uc.getHeaderField("ETag"); 293 if (tracing) 294 trace(2, "new mtime %d, etag %s", mtime, etag); 295 } 296 297 cat = null; 298 storeMeta(); 299 300 return true; 301 302 } 303 304 // HTTP HEAD 305 // 306 public boolean isCatalogStale() throws IOException { 307 if (!catFile.exists()) 308 return true; 309 return fetchCatalog(true, false); 310 } 311 312 // HTTP GET (conditional) 313 // 314 public boolean updateCatalog(boolean force) throws IOException { 315 return fetchCatalog(false, force); 316 } 317 318 private RepositoryCatalog catalog() 319 throws IOException 320 { 321 if (!catFile.exists()) 322 throw new IOException("No catalog yet"); 323 if (cat == null) 324 cat = RepositoryCatalog.load(new FileInputStream(catFile)); 325 return cat; 326 } 327 328 @Override 329 protected void gatherLocalModuleIds(String moduleName, 330 Set<ModuleId> mids) 331 throws IOException 332 { 333 catalog().gatherModuleIds(moduleName, mids); 334 } 335 336 /** 337 * Find all module views and aliases with the given name and module 338 * architecture in this catalog. 339 */ 340 public List<ModuleId> findlocalModuleIds(String moduleName, 341 ModuleArchitecture modArch) 342 throws IOException 343 { 344 Set<ModuleId> mids = new HashSet<>(); 345 gatherLocalModuleIds(moduleName, modArch, mids); 346 List<ModuleId> rv = new ArrayList<>(mids); 347 Collections.sort(rv); 348 return rv; 349 } 350 351 protected void gatherLocalModuleIds(String moduleName, 352 ModuleArchitecture modArch, 353 Set<ModuleId> mids) 354 throws IOException 355 { 356 catalog().gatherModuleIds(moduleName, modArch, mids); 357 } 358 359 @Override 360 protected void gatherLocalDeclaringModuleIds(Set<ModuleId> mids) 361 throws IOException 362 { 363 catalog().gatherDeclaringModuleIds(mids); 364 } 365 366 @Override 367 protected ModuleInfo readLocalModuleInfo(ModuleId mid) 368 throws IOException 369 { 370 byte[] bs = catalog().readModuleInfoBytes(mid); 371 return jms.parseModuleInfo(bs); 372 } 373 374 @Override 375 public InputStream fetch(ModuleId mid) throws IOException { 376 ModuleFileMetaData mfmd = fetchMetaData(mid); 377 URI u = uri.resolve(mid.toString() + mfmd.getType().getFileNameSuffix()); 378 if (tracing) 379 trace(1, "fetching module %s", u); 380 381 // special case file protocol for faster access 382 if (u.getScheme().equalsIgnoreCase("file")) { 383 return Files.newInputStream(Paths.get(u)); 384 } else { 385 URLConnection uc = u.toURL().openConnection(); 386 if (uc instanceof HttpURLConnection) { 387 HttpURLConnection http = (HttpURLConnection)uc; 388 http.setInstanceFollowRedirects(true); 389 http.connect(); 390 int rc = http.getResponseCode(); 391 if (tracing) 392 trace(2, "response: %s", http.getResponseMessage()); 393 if (rc != HttpURLConnection.HTTP_OK) 394 throw new IOException(u + ": " + http.getResponseMessage()); 395 } 396 return uc.getInputStream(); 397 } 398 } 399 400 @Override 401 public ModuleFileMetaData fetchMetaData(ModuleId mid) throws IOException { 402 RepositoryCatalog.Entry e = catalog().get(mid); 403 if (e == null) 404 throw new IllegalArgumentException(mid.toString()); 405 return new ModuleFileMetaData(e.type, e.modArch, e.csize, e.usize); 406 } 407 408 409 /** 410 * Attempts to delete {@code f}. If the delete fails then the exception is 411 * added as a suppressed exception to the given exception. 412 */ 413 private static void deleteAfterException(File f, Exception x) { 414 try { 415 Files.deleteIfExists(f.toPath()); 416 } catch (IOException x2) { 417 x.addSuppressed(x2); 418 } 419 } 420 } --- EOF ---