source: josm/trunk/src/org/openstreetmap/josm/io/MirroredInputStream.java@ 7070

Last change on this file since 7070 was 7058, checked in by stoecker, 10 years ago

support josmdir:// URI

  • Property svn:eol-style set to native
File size: 17.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedInputStream;
7import java.io.BufferedOutputStream;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.FileOutputStream;
11import java.io.IOException;
12import java.io.InputStream;
13import java.io.OutputStream;
14import java.net.HttpURLConnection;
15import java.net.MalformedURLException;
16import java.net.URL;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Enumeration;
20import java.util.List;
21import java.util.zip.ZipEntry;
22import java.util.zip.ZipFile;
23
24import org.openstreetmap.josm.Main;
25import org.openstreetmap.josm.tools.Pair;
26import org.openstreetmap.josm.tools.Utils;
27
28/**
29 * Mirrors a file to a local file.
30 * <p>
31 * The file mirrored is only downloaded if it has been more than 7 days since last download
32 */
33public class MirroredInputStream extends InputStream {
34 InputStream fs = null;
35 File file = null;
36
37 public static final long DEFAULT_MAXTIME = -1L;
38
39 /**
40 * Constructs an input stream from a given filename, URL or internal resource.
41 *
42 * @param name can be:<ul>
43 * <li>relative or absolute file name</li>
44 * <li>{@code file:///SOME/FILE} the same as above</li>
45 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
46 * <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
47 * <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
48 * @throws IOException when the resource with the given name could not be retrieved
49 */
50 public MirroredInputStream(String name) throws IOException {
51 this(name, null, DEFAULT_MAXTIME, null);
52 }
53
54 /**
55 * Constructs an input stream from a given filename, URL or internal resource.
56 *
57 * @param name can be:<ul>
58 * <li>relative or absolute file name</li>
59 * <li>{@code file:///SOME/FILE} the same as above</li>
60 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
61 * <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
62 * <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
63 * @param maxTime the maximum age of the cache file (in seconds)
64 * @throws IOException when the resource with the given name could not be retrieved
65 */
66 public MirroredInputStream(String name, long maxTime) throws IOException {
67 this(name, null, maxTime, null);
68 }
69
70 /**
71 * Constructs an input stream from a given filename, URL or internal resource.
72 *
73 * @param name can be:<ul>
74 * <li>relative or absolute file name</li>
75 * <li>{@code file:///SOME/FILE} the same as above</li>
76 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
77 * <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
78 * <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
79 * @param destDir the destination directory for the cache file. Only applies for URLs.
80 * @throws IOException when the resource with the given name could not be retrieved
81 */
82 public MirroredInputStream(String name, String destDir) throws IOException {
83 this(name, destDir, DEFAULT_MAXTIME, null);
84 }
85
86 /**
87 * Constructs an input stream from a given filename, URL or internal resource.
88 *
89 * @param name can be:<ul>
90 * <li>relative or absolute file name</li>
91 * <li>{@code file:///SOME/FILE} the same as above</li>
92 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
93 * <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
94 * <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
95 * @param destDir the destination directory for the cache file. Only applies for URLs.
96 * @param maxTime the maximum age of the cache file (in seconds)
97 * @throws IOException when the resource with the given name could not be retrieved
98 */
99 public MirroredInputStream(String name, String destDir, long maxTime) throws IOException {
100 this(name, destDir, maxTime, null);
101 }
102
103 /**
104 * Constructs an input stream from a given filename, URL or internal resource.
105 *
106 * @param name can be:<ul>
107 * <li>relative or absolute file name</li>
108 * <li>{@code file:///SOME/FILE} the same as above</li>
109 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
110 * <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
111 * <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
112 * @param destDir the destination directory for the cache file. Only applies for URLs.
113 * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Only applies for URLs.
114 * @throws IOException when the resource with the given name could not be retrieved
115 * @since 6867
116 */
117 public MirroredInputStream(String name, String destDir, String httpAccept) throws IOException {
118 this(name, destDir, DEFAULT_MAXTIME, httpAccept);
119 }
120
121 /**
122 * Constructs an input stream from a given filename, URL or internal resource.
123 *
124 * @param name can be:<ul>
125 * <li>relative or absolute file name</li>
126 * <li>{@code file:///SOME/FILE} the same as above</li>
127 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
128 * <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
129 * <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
130 * @param destDir the destination directory for the cache file. Only applies for URLs.
131 * @param maxTime the maximum age of the cache file (in seconds)
132 * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Only applies for URLs.
133 * @throws IOException when the resource with the given name could not be retrieved
134 * @since 6867
135 */
136 public MirroredInputStream(String name, String destDir, long maxTime, String httpAccept) throws IOException {
137 URL url;
138 try {
139 url = new URL(name);
140 if ("file".equals(url.getProtocol())) {
141 file = new File(name.substring("file:/".length()));
142 if (!file.exists()) {
143 file = new File(name.substring("file://".length()));
144 }
145 } else {
146 file = checkLocal(url, destDir, maxTime, httpAccept);
147 }
148 } catch (java.net.MalformedURLException e) {
149 if (name.startsWith("resource://")) {
150 fs = getClass().getResourceAsStream(
151 name.substring("resource:/".length()));
152 if (fs == null)
153 throw new IOException(tr("Failed to open input stream for resource ''{0}''", name));
154 return;
155 } else if (name.startsWith("josmdir://")) {
156 file = new File(Main.pref.getPreferencesDir(), name.substring("josmdir://".length()));
157 } else {
158 file = new File(name);
159 }
160 }
161 if (file == null)
162 throw new IOException();
163 fs = new FileInputStream(file);
164 }
165
166 /**
167 * Looks for a certain entry inside a zip file and returns the entry path.
168 *
169 * Replies a file in the top level directory of the ZIP file which has an
170 * extension <code>extension</code>. If more than one files have this
171 * extension, the last file whose name includes <code>namepart</code>
172 * is opened.
173 *
174 * @param extension the extension of the file we're looking for
175 * @param namepart the name part
176 * @return The zip entry path of the matching file. Null if this mirrored
177 * input stream doesn't represent a zip file or if there was no matching
178 * file in the ZIP file.
179 */
180 public String findZipEntryPath(String extension, String namepart) {
181 Pair<String, InputStream> ze = findZipEntryImpl(extension, namepart);
182 if (ze == null) return null;
183 return ze.a;
184 }
185
186 /**
187 * Like {@link #findZipEntryPath}, but returns the corresponding InputStream.
188 * @since 6148
189 */
190 public InputStream findZipEntryInputStream(String extension, String namepart) {
191 Pair<String, InputStream> ze = findZipEntryImpl(extension, namepart);
192 if (ze == null) return null;
193 return ze.b;
194 }
195
196 @SuppressWarnings("resource")
197 private Pair<String, InputStream> findZipEntryImpl(String extension, String namepart) {
198 if (file == null)
199 return null;
200 Pair<String, InputStream> res = null;
201 try {
202 ZipFile zipFile = new ZipFile(file);
203 ZipEntry resentry = null;
204 Enumeration<? extends ZipEntry> entries = zipFile.entries();
205 while (entries.hasMoreElements()) {
206 ZipEntry entry = entries.nextElement();
207 if (entry.getName().endsWith("." + extension)) {
208 /* choose any file with correct extension. When more than
209 one file, prefer the one which matches namepart */
210 if (resentry == null || entry.getName().indexOf(namepart) >= 0) {
211 resentry = entry;
212 }
213 }
214 }
215 if (resentry != null) {
216 InputStream is = zipFile.getInputStream(resentry);
217 res = Pair.create(resentry.getName(), is);
218 } else {
219 Utils.close(zipFile);
220 }
221 } catch (Exception e) {
222 if (file.getName().endsWith(".zip")) {
223 Main.warn(tr("Failed to open file with extension ''{2}'' and namepart ''{3}'' in zip file ''{0}''. Exception was: {1}",
224 file.getName(), e.toString(), extension, namepart));
225 }
226 }
227 return res;
228 }
229
230 public File getFile() {
231 return file;
232 }
233
234 public static void cleanup(String name) {
235 cleanup(name, null);
236 }
237
238 public static void cleanup(String name, String destDir) {
239 URL url;
240 try {
241 url = new URL(name);
242 if (!"file".equals(url.getProtocol())) {
243 String prefKey = getPrefKey(url, destDir);
244 List<String> localPath = new ArrayList<>(Main.pref.getCollection(prefKey));
245 if (localPath.size() == 2) {
246 File lfile = new File(localPath.get(1));
247 if(lfile.exists()) {
248 lfile.delete();
249 }
250 }
251 Main.pref.putCollection(prefKey, null);
252 }
253 } catch (MalformedURLException e) {
254 Main.warn(e);
255 }
256 }
257
258 /**
259 * get preference key to store the location and age of the cached file.
260 * 2 resources that point to the same url, but that are to be stored in different
261 * directories will not share a cache file.
262 */
263 private static String getPrefKey(URL url, String destDir) {
264 StringBuilder prefKey = new StringBuilder("mirror.");
265 if (destDir != null) {
266 prefKey.append(destDir);
267 prefKey.append(".");
268 }
269 prefKey.append(url.toString());
270 return prefKey.toString().replaceAll("=","_");
271 }
272
273 private File checkLocal(URL url, String destDir, long maxTime, String httpAccept) throws IOException {
274 String prefKey = getPrefKey(url, destDir);
275 long age = 0L;
276 File localFile = null;
277 List<String> localPathEntry = new ArrayList<>(Main.pref.getCollection(prefKey));
278 if (localPathEntry.size() == 2) {
279 localFile = new File(localPathEntry.get(1));
280 if(!localFile.exists())
281 localFile = null;
282 else {
283 if ( maxTime == DEFAULT_MAXTIME
284 || maxTime <= 0 // arbitrary value <= 0 is deprecated
285 ) {
286 maxTime = Main.pref.getInteger("mirror.maxtime", 7*24*60*60); // one week
287 }
288 age = System.currentTimeMillis() - Long.parseLong(localPathEntry.get(0));
289 if (age < maxTime*1000) {
290 return localFile;
291 }
292 }
293 }
294 if (destDir == null) {
295 destDir = Main.pref.getCacheDirectory().getPath();
296 }
297
298 File destDirFile = new File(destDir);
299 if (!destDirFile.exists()) {
300 destDirFile.mkdirs();
301 }
302
303 String a = url.toString().replaceAll("[^A-Za-z0-9_.-]", "_");
304 String localPath = "mirror_" + a;
305 destDirFile = new File(destDir, localPath + ".tmp");
306 try {
307 HttpURLConnection con = connectFollowingRedirect(url, httpAccept);
308 try (
309 InputStream bis = new BufferedInputStream(con.getInputStream());
310 OutputStream fos = new FileOutputStream(destDirFile);
311 OutputStream bos = new BufferedOutputStream(fos)
312 ) {
313 byte[] buffer = new byte[4096];
314 int length;
315 while ((length = bis.read(buffer)) > -1) {
316 bos.write(buffer, 0, length);
317 }
318 }
319 localFile = new File(destDir, localPath);
320 if(Main.platform.rename(destDirFile, localFile)) {
321 Main.pref.putCollection(prefKey, Arrays.asList(new String[]
322 {Long.toString(System.currentTimeMillis()), localFile.toString()}));
323 } else {
324 Main.warn(tr("Failed to rename file {0} to {1}.",
325 destDirFile.getPath(), localFile.getPath()));
326 }
327 } catch (IOException e) {
328 if (age >= maxTime*1000 && age < maxTime*1000*2) {
329 Main.warn(tr("Failed to load {0}, use cached file and retry next time: {1}", url, e));
330 return localFile;
331 } else {
332 throw e;
333 }
334 }
335
336 return localFile;
337 }
338
339 /**
340 * Opens a connection for downloading a resource.
341 * <p>
342 * Manually follows redirects because
343 * {@link HttpURLConnection#setFollowRedirects(boolean)} fails if the redirect
344 * is going from a http to a https URL, see <a href="https://bugs.openjdk.java.net/browse/JDK-4620571">bug report</a>.
345 * <p>
346 * This can causes problems when downloading from certain GitHub URLs.
347 *
348 * @param downloadUrl The resource URL to download
349 * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Can be {@code null}
350 * @return The HTTP connection effectively linked to the resource, after all potential redirections
351 * @throws MalformedURLException If a redirected URL is wrong
352 * @throws IOException If any I/O operation goes wrong
353 * @since 6867
354 */
355 public static HttpURLConnection connectFollowingRedirect(URL downloadUrl, String httpAccept) throws MalformedURLException, IOException {
356 HttpURLConnection con = null;
357 int numRedirects = 0;
358 while(true) {
359 con = Utils.openHttpConnection(downloadUrl);
360 con.setInstanceFollowRedirects(false);
361 con.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
362 con.setReadTimeout(Main.pref.getInteger("socket.timeout.read",30)*1000);
363 Main.debug("GET "+downloadUrl);
364 if (httpAccept != null) {
365 Main.debug("Accept: "+httpAccept);
366 con.setRequestProperty("Accept", httpAccept);
367 }
368 try {
369 con.connect();
370 } catch (IOException e) {
371 Main.addNetworkError(downloadUrl, Utils.getRootCause(e));
372 throw e;
373 }
374 switch(con.getResponseCode()) {
375 case HttpURLConnection.HTTP_OK:
376 return con;
377 case HttpURLConnection.HTTP_MOVED_PERM:
378 case HttpURLConnection.HTTP_MOVED_TEMP:
379 case HttpURLConnection.HTTP_SEE_OTHER:
380 String redirectLocation = con.getHeaderField("Location");
381 if (downloadUrl == null) {
382 /* I18n: argument is HTTP response code */ String msg = tr("Unexpected response from HTTP server. Got {0} response without ''Location'' header. Can''t redirect. Aborting.", con.getResponseCode());
383 throw new IOException(msg);
384 }
385 downloadUrl = new URL(redirectLocation);
386 // keep track of redirect attempts to break a redirect loops if it happens
387 // to occur for whatever reason
388 numRedirects++;
389 if (numRedirects >= Main.pref.getInteger("socket.maxredirects", 5)) {
390 String msg = tr("Too many redirects to the download URL detected. Aborting.");
391 throw new IOException(msg);
392 }
393 Main.info(tr("Download redirected to ''{0}''", downloadUrl));
394 break;
395 default:
396 String msg = tr("Failed to read from ''{0}''. Server responded with status code {1}.", downloadUrl, con.getResponseCode());
397 throw new IOException(msg);
398 }
399 }
400 }
401
402 @Override
403 public int available() throws IOException
404 { return fs.available(); }
405 @Override
406 public void close() throws IOException
407 { Utils.close(fs); }
408 @Override
409 public int read() throws IOException
410 { return fs.read(); }
411 @Override
412 public int read(byte[] b) throws IOException
413 { return fs.read(b); }
414 @Override
415 public int read(byte[] b, int off, int len) throws IOException
416 { return fs.read(b,off, len); }
417 @Override
418 public long skip(long n) throws IOException
419 { return fs.skip(n); }
420}
Note: See TracBrowser for help on using the repository browser.