Index: trunk/src/org/openstreetmap/josm/data/Preferences.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 7248)
@@ -51,5 +51,5 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.preferences.ColorProperty;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.XmlWriter;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -1390,5 +1390,5 @@
     public void validateXML(Reader in) throws Exception {
         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
-        try (InputStream xsdStream = new MirroredInputStream("resource://data/preferences.xsd")) {
+        try (InputStream xsdStream = new CachedFile("resource://data/preferences.xsd").getInputStream()) {
             Schema schema = factory.newSchema(new StreamSource(xsdStream));
             Validator validator = schema.newValidator();
Index: trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java	(revision 7248)
@@ -16,5 +16,5 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryPreferenceEntry;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.imagery.ImageryReader;
 import org.xml.sax.SAXException;
@@ -78,5 +78,5 @@
         for (String source : Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES))) {
             if (clearCache) {
-                MirroredInputStream.cleanup(source);
+                CachedFile.cleanup(source);
             }
             try {
Index: trunk/src/org/openstreetmap/josm/data/projection/Projections.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/Projections.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/projection/Projections.java	(revision 7248)
@@ -32,5 +32,5 @@
 import org.openstreetmap.josm.gui.preferences.projection.ProjectionChoice;
 import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.Pair;
 
@@ -133,5 +133,5 @@
         Pattern epsgPattern = Pattern.compile("<(\\d+)>(.*)<>");
         try (
-            InputStream in = new MirroredInputStream("resource://data/projection/epsg");
+            InputStream in = new CachedFile("resource://data/projection/epsg").getInputStream();
             BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
         ) {
Index: trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileWrapper.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileWrapper.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileWrapper.java	(revision 7248)
@@ -4,5 +4,5 @@
 import java.io.InputStream;
 
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 
 /**
@@ -48,5 +48,5 @@
     public NTV2GridShiftFile getShiftFile() {
         if (instance == null) {
-            try (InputStream is = new MirroredInputStream(gridFileName)) {
+            try (InputStream is = new CachedFile(gridFileName).getInputStream()) {
                 instance = new NTV2GridShiftFile();
                 instance.loadGridShiftFile(is, false);
Index: trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(revision 7248)
@@ -6,4 +6,5 @@
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.Reader;
 import java.util.ArrayList;
@@ -45,5 +46,5 @@
 import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
 import org.openstreetmap.josm.gui.preferences.validator.ValidatorTagCheckerRulesPreference;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.UTFInputStreamReader;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -533,5 +534,5 @@
                     Main.info(tr("Adding {0} to tag checker", i));
                 }
-                try (MirroredInputStream s = new MirroredInputStream(i)) {
+                try (InputStream s = new CachedFile(i).getInputStream()) {
                     addMapCSS(new BufferedReader(UTFInputStreamReader.create(s)));
                 }
Index: trunk/src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/validation/tests/OpeningHourTest.java	(revision 7248)
@@ -24,5 +24,5 @@
 import org.openstreetmap.josm.data.validation.Test;
 import org.openstreetmap.josm.data.validation.TestError;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 
 /**
@@ -53,5 +53,5 @@
         if (ENGINE != null) {
             try (Reader reader = new InputStreamReader(
-                    new MirroredInputStream("resource://data/validator/opening_hours.js"), StandardCharsets.UTF_8)) {
+                    new CachedFile("resource://data/validator/opening_hours.js").getInputStream(), StandardCharsets.UTF_8)) {
                 ENGINE.eval(reader);
                 // fake country/state to not get errors on holidays
Index: trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java	(revision 7248)
@@ -11,4 +11,5 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -50,5 +51,5 @@
 import org.openstreetmap.josm.gui.tagging.TaggingPresets;
 import org.openstreetmap.josm.gui.widgets.EditableList;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.UTFInputStreamReader;
 import org.openstreetmap.josm.tools.GBC;
@@ -165,5 +166,5 @@
         for (String source : Main.pref.getCollection(PREF_SOURCES, DEFAULT_SOURCES)) {
             try (
-                MirroredInputStream s = new MirroredInputStream(source);
+                InputStream s = new CachedFile(source).getInputStream();
                 BufferedReader reader = new BufferedReader(UTFInputStreamReader.create(s));
             ) {
Index: trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java	(revision 7248)
@@ -30,5 +30,5 @@
 import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference.MapPaintPrefHelper;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Utils;
@@ -228,11 +228,11 @@
 
     private static StyleSource fromSourceEntry(SourceEntry entry) {
-        MirroredInputStream in = null;
+        CachedFile cf = null;
         try {
             Set<String> mimes = new HashSet<>();
             mimes.addAll(Arrays.asList(XmlStyleSource.XML_STYLE_MIME_TYPES.split(", ")));
             mimes.addAll(Arrays.asList(MapCSSStyleSource.MAPCSS_STYLE_MIME_TYPES.split(", ")));
-            in = new MirroredInputStream(entry.url, null, Utils.join(", ", mimes));
-            String zipEntryPath = in.findZipEntryPath("mapcss", "style");
+            cf = new CachedFile(entry.url).setHttpAccept(Utils.join(", ", mimes));
+            String zipEntryPath = cf.findZipEntryPath("mapcss", "style");
             if (zipEntryPath != null) {
                 entry.isZip = true;
@@ -240,5 +240,5 @@
                 return new MapCSSStyleSource(entry);
             }
-            zipEntryPath = in.findZipEntryPath("xml", "style");
+            zipEntryPath = cf.findZipEntryPath("xml", "style");
             if (zipEntryPath != null)
                 return new XmlStyleSource(entry);
@@ -248,5 +248,5 @@
                 return new XmlStyleSource(entry);
             else {
-                try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
+                try (InputStreamReader reader = new InputStreamReader(cf.getInputStream(), StandardCharsets.UTF_8)) {
                     WHILE: while (true) {
                         int c = reader.read();
@@ -272,6 +272,4 @@
             Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", entry.url, e.toString()));
             Main.error(e);
-        } finally {
-            Utils.close(in);
         }
         return null;
Index: trunk/src/org/openstreetmap/josm/gui/mappaint/StyleSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/mappaint/StyleSource.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/gui/mappaint/StyleSource.java	(revision 7248)
@@ -18,5 +18,5 @@
 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference;
 import org.openstreetmap.josm.gui.preferences.SourceEntry;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Utils;
@@ -82,10 +82,10 @@
 
     /**
-     * Returns a new {@code MirroredInputStream} to the local file containing style source (can be a text file or an archive).
-     * @return A new {@code MirroredInputStream} to the local file containing style source
+     * Returns a new {@code CachedFile} to the local file containing style source (can be a text file or an archive).
+     * @return A new {@code CachedFile} to the local file containing style source
      * @throws IOException if any I/O error occurs.
      * @since 7081
      */
-    public abstract MirroredInputStream getMirroredInputStream() throws IOException;
+    public abstract CachedFile getCachedFile() throws IOException;
 
     /**
Index: trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java	(revision 7248)
@@ -42,5 +42,5 @@
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
 import org.openstreetmap.josm.gui.preferences.SourceEntry;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.LanguageInfo;
@@ -275,8 +275,7 @@
             return new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8));
         }
-        MirroredInputStream in = getMirroredInputStream();
+        CachedFile cf = getCachedFile();
         if (isZip) {
-            File file = in.getFile();
-            Utils.close(in);
+            File file = cf.getFile();
             zipFile = new ZipFile(file, StandardCharsets.UTF_8);
             zipIcons = file;
@@ -286,11 +285,11 @@
             zipFile = null;
             zipIcons = null;
-            return in;
-        }
-    }
-
-    @Override
-    public MirroredInputStream getMirroredInputStream() throws IOException {
-        return new MirroredInputStream(url, null, MAPCSS_STYLE_MIME_TYPES);
+            return cf.getInputStream();
+        }
+    }
+
+    @Override
+    public CachedFile getCachedFile() throws IOException {
+        return new CachedFile(url).setHttpAccept(MAPCSS_STYLE_MIME_TYPES);
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/mappaint/xml/XmlStyleSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/mappaint/xml/XmlStyleSource.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/gui/mappaint/xml/XmlStyleSource.java	(revision 7248)
@@ -29,5 +29,5 @@
 import org.openstreetmap.josm.gui.mappaint.StyleSource;
 import org.openstreetmap.josm.gui.preferences.SourceEntry;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.Utils;
 import org.openstreetmap.josm.tools.XmlObjectParser;
@@ -104,18 +104,18 @@
     @Override
     public InputStream getSourceInputStream() throws IOException {
-        MirroredInputStream in = getMirroredInputStream();
-        InputStream zip = in.findZipEntryInputStream("xml", "style");
+        CachedFile cf = getCachedFile();
+        InputStream zip = cf.findZipEntryInputStream("xml", "style");
         if (zip != null) {
-            zipIcons = in.getFile();
+            zipIcons = cf.getFile();
             return zip;
         } else {
             zipIcons = null;
-            return in;
-        }
-    }
-
-    @Override
-    public MirroredInputStream getMirroredInputStream() throws IOException {
-        return new MirroredInputStream(url, null, XML_STYLE_MIME_TYPES);
+            return cf.getInputStream();
+        }
+    }
+
+    @Override
+    public CachedFile getCachedFile() throws IOException {
+        return new CachedFile(url).setHttpAccept(XML_STYLE_MIME_TYPES);
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/preferences/SourceEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/SourceEditor.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/SourceEditor.java	(revision 7248)
@@ -22,4 +22,5 @@
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.MalformedURLException;
@@ -89,5 +90,5 @@
 import org.openstreetmap.josm.gui.widgets.JFileChooserManager;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.OsmTransferException;
 import org.openstreetmap.josm.tools.GBC;
@@ -1044,5 +1045,5 @@
         @Override
         public void actionPerformed(ActionEvent e) {
-            MirroredInputStream.cleanup(url);
+            CachedFile.cleanup(url);
             reloadAvailableSources(url, sourceProviders);
         }
@@ -1280,5 +1281,5 @@
                 }
 
-                MirroredInputStream stream = new MirroredInputStream(url);
+                InputStream stream = new CachedFile(url).getInputStream();
                 reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
 
Index: trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPresetReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPresetReader.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPresetReader.java	(revision 7248)
@@ -25,5 +25,5 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.XmlObjectParser;
 import org.xml.sax.SAXException;
@@ -225,13 +225,13 @@
     public static Collection<TaggingPreset> readAll(String source, boolean validate) throws SAXException, IOException {
         Collection<TaggingPreset> tp;
+        CachedFile cf = new CachedFile(source).setHttpAccept(PRESET_MIME_TYPES);
         try (
-            MirroredInputStream s = new MirroredInputStream(source, null, PRESET_MIME_TYPES);
             // zip may be null, but Java 7 allows it: https://blogs.oracle.com/darcy/entry/project_coin_null_try_with
-            InputStream zip = s.findZipEntryInputStream("xml", "preset")
+            InputStream zip = cf.findZipEntryInputStream("xml", "preset")
         ) {
             if (zip != null) {
-                zipIcons = s.getFile();
-            }
-            try (InputStreamReader r = new InputStreamReader(zip == null ? s : zip, StandardCharsets.UTF_8)) {
+                zipIcons = cf.getFile();
+            }
+            try (InputStreamReader r = new InputStreamReader(zip == null ? cf.getInputStream() : zip, StandardCharsets.UTF_8)) {
                 tp = readAll(new BufferedReader(r), validate);
             }
Index: trunk/src/org/openstreetmap/josm/io/CachedFile.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/CachedFile.java	(revision 7248)
+++ trunk/src/org/openstreetmap/josm/io/CachedFile.java	(revision 7248)
@@ -0,0 +1,489 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.tools.Pair;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Downloads a file and caches it on disk in order to reduce network load.
+ * 
+ * Supports URLs, local files, and a custom scheme (<code>resource:</code>) to get
+ * resources from the current *.jar file. (Local caching is only done for URLs.)
+ * <p>
+ * The mirrored file is only downloaded if it has been more than 7 days since
+ * last download. (Time can be configured.)
+ * <p>
+ * The file content is normally accessed with {@link #getInputStream()}, but
+ * you can also get the mirrored copy with {@link #getFile()}.
+ */
+public class CachedFile {
+
+    /**
+     * Caching strategy.
+     */
+    public enum CachingStrategy {
+        /**
+         * If cached file on disk is older than a certain time (7 days by default),
+         * consider the cache stale and try to download the file again.
+         */
+        MaxAge, 
+        /**
+         * Similar to MaxAge, considers the cache stale when a certain age is
+         * exceeded. In addition, a If-Modified-Since HTTP header is added.
+         * When the server replies "304 Not Modified", this is considered the same
+         * as a full download.
+         */
+        IfModifiedSince 
+    }
+    protected String name;
+    protected long maxAge;
+    protected String destDir;
+    protected String httpAccept;
+    protected CachingStrategy cachingStrategy;
+    
+    protected File cacheFile = null;
+    boolean initialized = false;
+
+    public static final long DEFAULT_MAXTIME = -1L;
+    public static final long DAYS = 24*60*60; // factor to get caching time in days
+
+    /**
+     * Constructs a CachedFile object from a given filename, URL or internal resource.
+     *
+     * @param name can be:<ul>
+     *  <li>relative or absolute file name</li>
+     *  <li>{@code file:///SOME/FILE} the same as above</li>
+     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
+     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
+     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li></ul>
+     */
+    public CachedFile(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Set the name of the resource.
+     * @param name can be:<ul>
+     *  <li>relative or absolute file name</li>
+     *  <li>{@code file:///SOME/FILE} the same as above</li>
+     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
+     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
+     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li></ul>
+     * @return this object
+     */
+    public CachedFile setName(String name) {
+        this.name = name;
+        return this;
+    }
+    
+    /**
+     * Set maximum age of cache file. Only applies to URLs.
+     * When this time has passed after the last download of the file, the
+     * cache is considered stale and a new download will be attempted.
+     * @param maxAge the maximum cache age in seconds
+     * @return this object
+     */
+    public CachedFile setMaxAge(long maxAge) {
+        this.maxAge = maxAge;
+        return this;
+    }
+
+    /**
+     * Set the destination directory for the cache file. Only applies to URLs.
+     * @param destDir the destination directory
+     * @return this object
+     */
+    public CachedFile setDestDir(String destDir) {
+        this.destDir = destDir;
+        return this;
+    }
+
+    /**
+     * Set the accepted MIME types sent in the HTTP Accept header. Only applies to URLs.
+     * @param httpAccept the accepted MIME types
+     * @return this object
+     */
+    public CachedFile setHttpAccept(String httpAccept) {
+        this.httpAccept = httpAccept;
+        return this;
+    }
+
+    /**
+     * Set the caching strategy. Only applies to URLs.
+     * @param cachingStrategy
+     * @return this object
+     */
+    public CachedFile setCachingStrategy(CachingStrategy cachingStrategy) {
+        this.cachingStrategy = cachingStrategy;
+        return this;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public long getMaxAge() {
+        return maxAge;
+    }
+
+    public String getDestDir() {
+        return destDir;
+    }
+
+    public String getHttpAccept() {
+        return httpAccept;
+    }
+
+    public CachingStrategy getCachingStrategy() {
+        return cachingStrategy;
+    }
+
+    /**
+     * Get InputStream to the requested resource.
+     * @return the InputStream
+     * @throws IOException when the resource with the given name could not be retrieved
+     */
+    public InputStream getInputStream() throws IOException {
+        File file = getFile();
+        if (file == null) {
+            if (name.startsWith("resource://")) {
+                InputStream is = getClass().getResourceAsStream(
+                        name.substring("resource:/".length()));
+                if (is == null)
+                    throw new IOException(tr("Failed to open input stream for resource ''{0}''", name));
+                return is;
+            }
+        }
+        return new FileInputStream(file);
+    }
+
+    /**
+     * Get local file for the requested resource.
+     * @return The local cache file for URLs. If the resource is a local file,
+     * returns just that file.
+     * @throws IOException when the resource with the given name could not be retrieved
+     */
+    public File getFile() throws IOException {
+        if (initialized)
+            return cacheFile;
+        initialized = true;
+        URL url;
+        try {
+            url = new URL(name);
+            if ("file".equals(url.getProtocol())) {
+                cacheFile = new File(name.substring("file:/".length()));
+                if (!cacheFile.exists()) {
+                    cacheFile = new File(name.substring("file://".length()));
+                }
+            } else {
+                cacheFile = checkLocal(url);
+            }
+        } catch (java.net.MalformedURLException e) {
+            if (name.startsWith("resource://")) {
+                return null;
+            } else if (name.startsWith("josmdir://")) {
+                cacheFile = new File(Main.pref.getPreferencesDir(), name.substring("josmdir://".length()));
+            } else {
+                cacheFile = new File(name);
+            }
+        }
+        if (cacheFile == null)
+            throw new IOException();
+        return cacheFile;
+    }
+    
+    /**
+     * Looks for a certain entry inside a zip file and returns the entry path.
+     *
+     * Replies a file in the top level directory of the ZIP file which has an
+     * extension <code>extension</code>. If more than one files have this
+     * extension, the last file whose name includes <code>namepart</code>
+     * is opened.
+     *
+     * @param extension  the extension of the file we're looking for
+     * @param namepart the name part
+     * @return The zip entry path of the matching file. Null if this cached file
+     * doesn't represent a zip file or if there was no matching
+     * file in the ZIP file.
+     */
+    public String findZipEntryPath(String extension, String namepart) {
+        Pair<String, InputStream> ze = findZipEntryImpl(extension, namepart);
+        if (ze == null) return null;
+        return ze.a;
+    }
+
+    /**
+     * Like {@link #findZipEntryPath}, but returns the corresponding InputStream.
+     * @param extension  the extension of the file we're looking for
+     * @param namepart the name part
+     * @return InputStream to the matching file. Null if this cached file
+     * doesn't represent a zip file or if there was no matching
+     * file in the ZIP file.
+     * @since 6148
+     */
+    public InputStream findZipEntryInputStream(String extension, String namepart) {
+        Pair<String, InputStream> ze = findZipEntryImpl(extension, namepart);
+        if (ze == null) return null;
+        return ze.b;
+    }
+
+    @SuppressWarnings("resource")
+    private Pair<String, InputStream> findZipEntryImpl(String extension, String namepart) {
+        File file = null;
+        try {
+            file = getFile();
+        } catch (IOException ex) {
+        }
+        if (file == null)
+            return null;
+        Pair<String, InputStream> res = null;
+        try {
+            ZipFile zipFile = new ZipFile(file, StandardCharsets.UTF_8);
+            ZipEntry resentry = null;
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                ZipEntry entry = entries.nextElement();
+                if (entry.getName().endsWith("." + extension)) {
+                    /* choose any file with correct extension. When more than
+                        one file, prefer the one which matches namepart */
+                    if (resentry == null || entry.getName().indexOf(namepart) >= 0) {
+                        resentry = entry;
+                    }
+                }
+            }
+            if (resentry != null) {
+                InputStream is = zipFile.getInputStream(resentry);
+                res = Pair.create(resentry.getName(), is);
+            } else {
+                Utils.close(zipFile);
+            }
+        } catch (Exception e) {
+            if (file.getName().endsWith(".zip")) {
+                Main.warn(tr("Failed to open file with extension ''{2}'' and namepart ''{3}'' in zip file ''{0}''. Exception was: {1}",
+                        file.getName(), e.toString(), extension, namepart));
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Clear the cache for the given resource.
+     * This forces a fresh download.
+     * @param name the URL 
+     */
+    public static void cleanup(String name) {
+        cleanup(name, null);
+    }
+
+    /**
+     * Clear the cache for the given resource.
+     * This forces a fresh download.
+     * @param name the URL
+     * @param destDir the destination directory (see {@link #setDestDir(java.lang.String)})
+     */
+    public static void cleanup(String name, String destDir) {
+        URL url;
+        try {
+            url = new URL(name);
+            if (!"file".equals(url.getProtocol())) {
+                String prefKey = getPrefKey(url, destDir);
+                List<String> localPath = new ArrayList<>(Main.pref.getCollection(prefKey));
+                if (localPath.size() == 2) {
+                    File lfile = new File(localPath.get(1));
+                    if(lfile.exists()) {
+                        lfile.delete();
+                    }
+                }
+                Main.pref.putCollection(prefKey, null);
+            }
+        } catch (MalformedURLException e) {
+            Main.warn(e);
+        }
+    }
+
+    /**
+     * Get preference key to store the location and age of the cached file.
+     * 2 resources that point to the same url, but that are to be stored in different
+     * directories will not share a cache file.
+     */
+    private static String getPrefKey(URL url, String destDir) {
+        StringBuilder prefKey = new StringBuilder("mirror.");
+        if (destDir != null) {
+            prefKey.append(destDir);
+            prefKey.append(".");
+        }
+        prefKey.append(url.toString());
+        return prefKey.toString().replaceAll("=","_");
+    }
+
+    private File checkLocal(URL url) throws IOException {
+        String prefKey = getPrefKey(url, destDir);
+        long age = 0L;
+        long lMaxAge = maxAge;
+        Long ifModifiedSince = null;
+        File localFile = null;
+        List<String> localPathEntry = new ArrayList<>(Main.pref.getCollection(prefKey));
+        if (localPathEntry.size() == 2) {
+            localFile = new File(localPathEntry.get(1));
+            if(!localFile.exists())
+                localFile = null;
+            else {
+                if ( maxAge == DEFAULT_MAXTIME
+                        || maxAge <= 0 // arbitrary value <= 0 is deprecated
+                ) {
+                    lMaxAge = Main.pref.getInteger("mirror.maxtime", 7*24*60*60); // one week
+                }
+                age = System.currentTimeMillis() - Long.parseLong(localPathEntry.get(0));
+                if (age < lMaxAge*1000) {
+                    return localFile;
+                }
+                if (cachingStrategy == CachingStrategy.IfModifiedSince) {
+                    ifModifiedSince = Long.parseLong(localPathEntry.get(0));
+                }
+            }
+        }
+        if (destDir == null) {
+            destDir = Main.pref.getCacheDirectory().getPath();
+        }
+
+        File destDirFile = new File(destDir);
+        if (!destDirFile.exists()) {
+            destDirFile.mkdirs();
+        }
+        
+        String a = url.toString().replaceAll("[^A-Za-z0-9_.-]", "_");
+        String localPath = "mirror_" + a;
+        destDirFile = new File(destDir, localPath + ".tmp");
+        try {
+            HttpURLConnection con = connectFollowingRedirect(url, httpAccept, ifModifiedSince);
+            if (ifModifiedSince != null && con.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
+                Main.debug("304 Not Modified ("+url+")");
+                if (localFile == null) throw new AssertionError();
+                Main.pref.putCollection(prefKey, 
+                        Arrays.asList(Long.toString(System.currentTimeMillis()), localPathEntry.get(1)));
+                return localFile;
+            } 
+            try (
+                InputStream bis = new BufferedInputStream(con.getInputStream());
+                OutputStream fos = new FileOutputStream(destDirFile);
+                OutputStream bos = new BufferedOutputStream(fos)
+            ) {
+                byte[] buffer = new byte[4096];
+                int length;
+                while ((length = bis.read(buffer)) > -1) {
+                    bos.write(buffer, 0, length);
+                }
+            }
+            localFile = new File(destDir, localPath);
+            if(Main.platform.rename(destDirFile, localFile)) {
+                Main.pref.putCollection(prefKey, 
+                        Arrays.asList(Long.toString(System.currentTimeMillis()), localFile.toString()));
+            } else {
+                Main.warn(tr("Failed to rename file {0} to {1}.",
+                destDirFile.getPath(), localFile.getPath()));
+            }
+        } catch (IOException e) {
+            if (age >= lMaxAge*1000 && age < lMaxAge*1000*2) {
+                Main.warn(tr("Failed to load {0}, use cached file and retry next time: {1}", url, e));
+                return localFile;
+            } else {
+                throw e;
+            }
+        }
+
+        return localFile;
+    }
+
+    /**
+     * Opens a connection for downloading a resource.
+     * <p>
+     * Manually follows redirects because
+     * {@link HttpURLConnection#setFollowRedirects(boolean)} fails if the redirect
+     * is going from a http to a https URL, see <a href="https://bugs.openjdk.java.net/browse/JDK-4620571">bug report</a>.
+     * <p>
+     * This can causes problems when downloading from certain GitHub URLs.
+     *
+     * @param downloadUrl The resource URL to download
+     * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Can be {@code null}
+     * @param ifModifiedSince The download time of the cache file, optional
+     * @return The HTTP connection effectively linked to the resource, after all potential redirections
+     * @throws MalformedURLException If a redirected URL is wrong
+     * @throws IOException If any I/O operation goes wrong
+     * @since 6867
+     */
+    public static HttpURLConnection connectFollowingRedirect(URL downloadUrl, String httpAccept, Long ifModifiedSince) throws MalformedURLException, IOException {
+        HttpURLConnection con = null;
+        int numRedirects = 0;
+        while(true) {
+            con = Utils.openHttpConnection(downloadUrl);
+            if (ifModifiedSince != null) {
+                con.setIfModifiedSince(ifModifiedSince);
+            }
+            con.setInstanceFollowRedirects(false);
+            con.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
+            con.setReadTimeout(Main.pref.getInteger("socket.timeout.read",30)*1000);
+            Main.debug("GET "+downloadUrl);
+            if (httpAccept != null) {
+                Main.debug("Accept: "+httpAccept);
+                con.setRequestProperty("Accept", httpAccept);
+            }
+            try {
+                con.connect();
+            } catch (IOException e) {
+                Main.addNetworkError(downloadUrl, Utils.getRootCause(e));
+                throw e;
+            }
+            switch(con.getResponseCode()) {
+            case HttpURLConnection.HTTP_OK:
+                return con;
+            case HttpURLConnection.HTTP_NOT_MODIFIED:
+                if (ifModifiedSince != null)
+                    return con;
+            case HttpURLConnection.HTTP_MOVED_PERM:
+            case HttpURLConnection.HTTP_MOVED_TEMP:
+            case HttpURLConnection.HTTP_SEE_OTHER:
+                String redirectLocation = con.getHeaderField("Location");
+                if (downloadUrl == null) {
+                    /* 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());
+                    throw new IOException(msg);
+                }
+                downloadUrl = new URL(redirectLocation);
+                // keep track of redirect attempts to break a redirect loops if it happens
+                // to occur for whatever reason
+                numRedirects++;
+                if (numRedirects >= Main.pref.getInteger("socket.maxredirects", 5)) {
+                    String msg = tr("Too many redirects to the download URL detected. Aborting.");
+                    throw new IOException(msg);
+                }
+                Main.info(tr("Download redirected to ''{0}''", downloadUrl));
+                break;
+            default:
+                String msg = tr("Failed to read from ''{0}''. Server responded with status code {1}.", downloadUrl, con.getResponseCode());
+                throw new IOException(msg);
+            }
+        }
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/io/FileWatcher.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/FileWatcher.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/io/FileWatcher.java	(revision 7248)
@@ -59,21 +59,20 @@
             throw new IllegalStateException("File watcher is not available");
         }
-        try (MirroredInputStream mis = style.getMirroredInputStream()) {
-            // Get underlying file
-            File file = mis.getFile();
-            if (file == null) {
-                throw new IllegalArgumentException("Style "+style+" does not have a local file");
-            }
-            // Get parent directory as WatchService allows only to monitor directories, not single files
-            File dir = file.getParentFile();
-            if (dir == null) {
-                throw new IllegalArgumentException("Style "+style+" does not have a parent directory");
-            }
-            synchronized(this) {
-                // Register directory. Can be called several times for a same directory without problem
-                // (it returns the same key so it should not send events several times)
-                dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
-                styleMap.put(file.toPath(), style);
-            }
+        CachedFile cf = style.getCachedFile();
+        // Get underlying file
+        File file = cf.getFile();
+        if (file == null) {
+            throw new IllegalArgumentException("Style "+style+" does not have a local file");
+        }
+        // Get parent directory as WatchService allows only to monitor directories, not single files
+        File dir = file.getParentFile();
+        if (dir == null) {
+            throw new IllegalArgumentException("Style "+style+" does not have a parent directory");
+        }
+        synchronized(this) {
+            // Register directory. Can be called several times for a same directory without problem
+            // (it returns the same key so it should not send events several times)
+            dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
+            styleMap.put(file.toPath(), style);
         }
     }
Index: trunk/src/org/openstreetmap/josm/io/MirroredInputStream.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/MirroredInputStream.java	(revision 7247)
+++ 	(revision )
@@ -1,483 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.io;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.tools.Pair;
-import org.openstreetmap.josm.tools.Utils;
-
-/**
- * Mirrors a file to a local file.
- * <p>
- * The file mirrored is only downloaded if it has been more than 7 days since last download
- */
-public class MirroredInputStream extends InputStream {
-    
-    /**
-     * Caching strategy.
-     */
-    public enum CachingStrategy {
-        /**
-         * If cached file on disk is older than a certain time (7 days by default),
-         * consider the cache stale and try to download the file again.
-         */
-        MaxAge, 
-        /**
-         * Similar to MaxAge, considers the cache stale when a certain age is
-         * exceeded. In addition, a If-Modified-Since HTTP header is added.
-         * When the server replies "304 Not Modified", this is considered the same
-         * as a full download.
-         */
-        IfModifiedSince 
-    }
-    
-    InputStream fs = null;
-    File file = null;
-
-    public static final long DEFAULT_MAXTIME = -1L;
-    public static final long DAYS = 24*60*60; // factor to get caching time in days
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @throws IOException when the resource with the given name could not be retrieved
-     */
-    public MirroredInputStream(String name) throws IOException {
-        this(name, null, DEFAULT_MAXTIME, null);
-    }
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @param maxTime the maximum age of the cache file (in seconds)
-     * @throws IOException when the resource with the given name could not be retrieved
-     */
-    public MirroredInputStream(String name, long maxTime) throws IOException {
-        this(name, null, maxTime, null);
-    }
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @param destDir the destination directory for the cache file. Only applies for URLs.
-     * @throws IOException when the resource with the given name could not be retrieved
-     */
-    public MirroredInputStream(String name, String destDir) throws IOException {
-        this(name, destDir, DEFAULT_MAXTIME, null);
-    }
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @param destDir the destination directory for the cache file. Only applies for URLs.
-     * @param maxTime the maximum age of the cache file (in seconds)
-     * @throws IOException when the resource with the given name could not be retrieved
-     */
-    public MirroredInputStream(String name, String destDir, long maxTime) throws IOException {
-        this(name, destDir, maxTime, null);
-    }
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @param destDir the destination directory for the cache file. Only applies for URLs.
-     * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Only applies for URLs.
-     * @throws IOException when the resource with the given name could not be retrieved
-     * @since 6867
-     */
-    public MirroredInputStream(String name, String destDir, String httpAccept) throws IOException {
-        this(name, destDir, DEFAULT_MAXTIME, httpAccept);
-    }
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @param destDir the destination directory for the cache file. Only applies for URLs.
-     * @param maxTime the maximum age of the cache file (in seconds)
-     * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Only applies for URLs.
-     * @throws IOException when the resource with the given name could not be retrieved
-     * @since 6867
-     */
-    public MirroredInputStream(String name, String destDir, long maxTime, String httpAccept) throws IOException {
-        this(name, destDir, maxTime, httpAccept, CachingStrategy.MaxAge);
-    }
-
-    /**
-     * Constructs an input stream from a given filename, URL or internal resource.
-     *
-     * @param name can be:<ul>
-     *  <li>relative or absolute file name</li>
-     *  <li>{@code file:///SOME/FILE} the same as above</li>
-     *  <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
-     *  <li>{@code josmdir://SOME/FILE} file inside josm config directory (since r7058)</li>
-     *  <li>{@code http://...} a URL. It will be cached on disk.</li></ul>
-     * @param destDir the destination directory for the cache file. Only applies for URLs.
-     * @param maxTime the maximum age of the cache file (in seconds)
-     * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Only applies for URLs.
-     * @param caching the caching strategy
-     * @throws IOException when the resource with the given name could not be retrieved
-     * @since 6867
-     */
-    public MirroredInputStream(String name, String destDir, long maxTime, String httpAccept, CachingStrategy caching) throws IOException {
-        URL url;
-        try {
-            url = new URL(name);
-            if ("file".equals(url.getProtocol())) {
-                file = new File(name.substring("file:/".length()));
-                if (!file.exists()) {
-                    file = new File(name.substring("file://".length()));
-                }
-            } else {
-                file = checkLocal(url, destDir, maxTime, httpAccept, caching);
-            }
-        } catch (java.net.MalformedURLException e) {
-            if (name.startsWith("resource://")) {
-                fs = getClass().getResourceAsStream(
-                        name.substring("resource:/".length()));
-                if (fs == null)
-                    throw new IOException(tr("Failed to open input stream for resource ''{0}''", name));
-                return;
-            } else if (name.startsWith("josmdir://")) {
-                file = new File(Main.pref.getPreferencesDir(), name.substring("josmdir://".length()));
-            } else {
-                file = new File(name);
-            }
-        }
-        if (file == null)
-            throw new IOException();
-        fs = new FileInputStream(file);
-    }
-
-    /**
-     * Looks for a certain entry inside a zip file and returns the entry path.
-     *
-     * Replies a file in the top level directory of the ZIP file which has an
-     * extension <code>extension</code>. If more than one files have this
-     * extension, the last file whose name includes <code>namepart</code>
-     * is opened.
-     *
-     * @param extension  the extension of the file we're looking for
-     * @param namepart the name part
-     * @return The zip entry path of the matching file. Null if this mirrored
-     * input stream doesn't represent a zip file or if there was no matching
-     * file in the ZIP file.
-     */
-    public String findZipEntryPath(String extension, String namepart) {
-        Pair<String, InputStream> ze = findZipEntryImpl(extension, namepart);
-        if (ze == null) return null;
-        return ze.a;
-    }
-
-    /**
-     * Like {@link #findZipEntryPath}, but returns the corresponding InputStream.
-     * @since 6148
-     */
-    public InputStream findZipEntryInputStream(String extension, String namepart) {
-        Pair<String, InputStream> ze = findZipEntryImpl(extension, namepart);
-        if (ze == null) return null;
-        return ze.b;
-    }
-
-    @SuppressWarnings("resource")
-    private Pair<String, InputStream> findZipEntryImpl(String extension, String namepart) {
-        if (file == null)
-            return null;
-        Pair<String, InputStream> res = null;
-        try {
-            ZipFile zipFile = new ZipFile(file, StandardCharsets.UTF_8);
-            ZipEntry resentry = null;
-            Enumeration<? extends ZipEntry> entries = zipFile.entries();
-            while (entries.hasMoreElements()) {
-                ZipEntry entry = entries.nextElement();
-                if (entry.getName().endsWith("." + extension)) {
-                    /* choose any file with correct extension. When more than
-                        one file, prefer the one which matches namepart */
-                    if (resentry == null || entry.getName().indexOf(namepart) >= 0) {
-                        resentry = entry;
-                    }
-                }
-            }
-            if (resentry != null) {
-                InputStream is = zipFile.getInputStream(resentry);
-                res = Pair.create(resentry.getName(), is);
-            } else {
-                Utils.close(zipFile);
-            }
-        } catch (Exception e) {
-            if (file.getName().endsWith(".zip")) {
-                Main.warn(tr("Failed to open file with extension ''{2}'' and namepart ''{3}'' in zip file ''{0}''. Exception was: {1}",
-                        file.getName(), e.toString(), extension, namepart));
-            }
-        }
-        return res;
-    }
-
-    /**
-     * Replies the local file.
-     * @return The local file on disk
-     */
-    public File getFile() {
-        return file;
-    }
-
-    public static void cleanup(String name) {
-        cleanup(name, null);
-    }
-
-    public static void cleanup(String name, String destDir) {
-        URL url;
-        try {
-            url = new URL(name);
-            if (!"file".equals(url.getProtocol())) {
-                String prefKey = getPrefKey(url, destDir);
-                List<String> localPath = new ArrayList<>(Main.pref.getCollection(prefKey));
-                if (localPath.size() == 2) {
-                    File lfile = new File(localPath.get(1));
-                    if(lfile.exists()) {
-                        lfile.delete();
-                    }
-                }
-                Main.pref.putCollection(prefKey, null);
-            }
-        } catch (MalformedURLException e) {
-            Main.warn(e);
-        }
-    }
-
-    /**
-     * get preference key to store the location and age of the cached file.
-     * 2 resources that point to the same url, but that are to be stored in different
-     * directories will not share a cache file.
-     */
-    private static String getPrefKey(URL url, String destDir) {
-        StringBuilder prefKey = new StringBuilder("mirror.");
-        if (destDir != null) {
-            prefKey.append(destDir);
-            prefKey.append(".");
-        }
-        prefKey.append(url.toString());
-        return prefKey.toString().replaceAll("=","_");
-    }
-
-    private File checkLocal(URL url, String destDir, long maxTime, String httpAccept, CachingStrategy caching) throws IOException {
-        String prefKey = getPrefKey(url, destDir);
-        long age = 0L;
-        Long ifModifiedSince = null;
-        File localFile = null;
-        List<String> localPathEntry = new ArrayList<>(Main.pref.getCollection(prefKey));
-        if (localPathEntry.size() == 2) {
-            localFile = new File(localPathEntry.get(1));
-            if(!localFile.exists())
-                localFile = null;
-            else {
-                if ( maxTime == DEFAULT_MAXTIME
-                        || maxTime <= 0 // arbitrary value <= 0 is deprecated
-                ) {
-                    maxTime = Main.pref.getInteger("mirror.maxtime", 7*24*60*60); // one week
-                }
-                age = System.currentTimeMillis() - Long.parseLong(localPathEntry.get(0));
-                if (age < maxTime*1000) {
-                    return localFile;
-                }
-                if (caching == CachingStrategy.IfModifiedSince) {
-                    ifModifiedSince = Long.parseLong(localPathEntry.get(0));
-                }
-            }
-        }
-        if (destDir == null) {
-            destDir = Main.pref.getCacheDirectory().getPath();
-        }
-
-        File destDirFile = new File(destDir);
-        if (!destDirFile.exists()) {
-            destDirFile.mkdirs();
-        }
-        
-        String a = url.toString().replaceAll("[^A-Za-z0-9_.-]", "_");
-        String localPath = "mirror_" + a;
-        destDirFile = new File(destDir, localPath + ".tmp");
-        try {
-            HttpURLConnection con = connectFollowingRedirect(url, httpAccept, ifModifiedSince);
-            if (ifModifiedSince != null && con.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
-                Main.debug("304 Not Modified ("+url+")");
-                if (localFile == null) throw new AssertionError();
-                Main.pref.putCollection(prefKey, 
-                        Arrays.asList(Long.toString(System.currentTimeMillis()), localPathEntry.get(1)));
-                return localFile;
-            } 
-            try (
-                InputStream bis = new BufferedInputStream(con.getInputStream());
-                OutputStream fos = new FileOutputStream(destDirFile);
-                OutputStream bos = new BufferedOutputStream(fos)
-            ) {
-                byte[] buffer = new byte[4096];
-                int length;
-                while ((length = bis.read(buffer)) > -1) {
-                    bos.write(buffer, 0, length);
-                }
-            }
-            localFile = new File(destDir, localPath);
-            if(Main.platform.rename(destDirFile, localFile)) {
-                Main.pref.putCollection(prefKey, 
-                        Arrays.asList(Long.toString(System.currentTimeMillis()), localFile.toString()));
-            } else {
-                Main.warn(tr("Failed to rename file {0} to {1}.",
-                destDirFile.getPath(), localFile.getPath()));
-            }
-        } catch (IOException e) {
-            if (age >= maxTime*1000 && age < maxTime*1000*2) {
-                Main.warn(tr("Failed to load {0}, use cached file and retry next time: {1}", url, e));
-                return localFile;
-            } else {
-                throw e;
-            }
-        }
-
-        return localFile;
-    }
-
-    /**
-     * Opens a connection for downloading a resource.
-     * <p>
-     * Manually follows redirects because
-     * {@link HttpURLConnection#setFollowRedirects(boolean)} fails if the redirect
-     * is going from a http to a https URL, see <a href="https://bugs.openjdk.java.net/browse/JDK-4620571">bug report</a>.
-     * <p>
-     * This can causes problems when downloading from certain GitHub URLs.
-     *
-     * @param downloadUrl The resource URL to download
-     * @param httpAccept The accepted MIME types sent in the HTTP Accept header. Can be {@code null}
-     * @param ifModifiedSince The download time of the cache file, optional
-     * @return The HTTP connection effectively linked to the resource, after all potential redirections
-     * @throws MalformedURLException If a redirected URL is wrong
-     * @throws IOException If any I/O operation goes wrong
-     * @since 6867
-     */
-    public static HttpURLConnection connectFollowingRedirect(URL downloadUrl, String httpAccept, Long ifModifiedSince) throws MalformedURLException, IOException {
-        HttpURLConnection con = null;
-        int numRedirects = 0;
-        while(true) {
-            con = Utils.openHttpConnection(downloadUrl);
-            if (ifModifiedSince != null) {
-                con.setIfModifiedSince(ifModifiedSince);
-            }
-            con.setInstanceFollowRedirects(false);
-            con.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
-            con.setReadTimeout(Main.pref.getInteger("socket.timeout.read",30)*1000);
-            Main.debug("GET "+downloadUrl);
-            if (httpAccept != null) {
-                Main.debug("Accept: "+httpAccept);
-                con.setRequestProperty("Accept", httpAccept);
-            }
-            try {
-                con.connect();
-            } catch (IOException e) {
-                Main.addNetworkError(downloadUrl, Utils.getRootCause(e));
-                throw e;
-            }
-            switch(con.getResponseCode()) {
-            case HttpURLConnection.HTTP_OK:
-                return con;
-            case HttpURLConnection.HTTP_NOT_MODIFIED:
-                if (ifModifiedSince != null)
-                    return con;
-            case HttpURLConnection.HTTP_MOVED_PERM:
-            case HttpURLConnection.HTTP_MOVED_TEMP:
-            case HttpURLConnection.HTTP_SEE_OTHER:
-                String redirectLocation = con.getHeaderField("Location");
-                if (downloadUrl == null) {
-                    /* 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());
-                    throw new IOException(msg);
-                }
-                downloadUrl = new URL(redirectLocation);
-                // keep track of redirect attempts to break a redirect loops if it happens
-                // to occur for whatever reason
-                numRedirects++;
-                if (numRedirects >= Main.pref.getInteger("socket.maxredirects", 5)) {
-                    String msg = tr("Too many redirects to the download URL detected. Aborting.");
-                    throw new IOException(msg);
-                }
-                Main.info(tr("Download redirected to ''{0}''", downloadUrl));
-                break;
-            default:
-                String msg = tr("Failed to read from ''{0}''. Server responded with status code {1}.", downloadUrl, con.getResponseCode());
-                throw new IOException(msg);
-            }
-        }
-    }
-
-    @Override
-    public int available() throws IOException
-    { return fs.available(); }
-    @Override
-    public void close() throws IOException
-    { Utils.close(fs); }
-    @Override
-    public int read() throws IOException
-    { return fs.read(); }
-    @Override
-    public int read(byte[] b) throws IOException
-    { return fs.read(b); }
-    @Override
-    public int read(byte[] b, int off, int len) throws IOException
-    { return fs.read(b,off, len); }
-    @Override
-    public long skip(long n) throws IOException
-    { return fs.skip(n); }
-}
Index: trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 7248)
@@ -18,5 +18,5 @@
 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
 import org.openstreetmap.josm.data.imagery.Shape;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.UTFInputStreamReader;
 import org.xml.sax.Attributes;
@@ -50,6 +50,8 @@
             SAXParserFactory factory = SAXParserFactory.newInstance();
             factory.setNamespaceAware(true);
-            try (InputStream in = new MirroredInputStream(source, null, 1*MirroredInputStream.DAYS, null, 
-                    MirroredInputStream.CachingStrategy.IfModifiedSince)) {
+            try (InputStream in = new CachedFile(source)
+                    .setMaxAge(1*CachedFile.DAYS)
+                    .setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince)
+                    .getInputStream()) {
                 InputSource is = new InputSource(UTFInputStreamReader.create(in));
                 factory.newSAXParser().parse(is, parser);
Index: trunk/src/org/openstreetmap/josm/plugins/PluginDownloadTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginDownloadTask.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginDownloadTask.java	(revision 7248)
@@ -22,5 +22,5 @@
 import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.xml.sax.SAXException;
@@ -125,5 +125,5 @@
             URL url = new URL(pi.downloadlink);
             synchronized(this) {
-                downloadConnection = MirroredInputStream.connectFollowingRedirect(url, PLUGIN_MIME_TYPES, null);
+                downloadConnection = CachedFile.connectFollowingRedirect(url, PLUGIN_MIME_TYPES, null);
             }
             try (
Index: trunk/src/org/openstreetmap/josm/tools/ImageProvider.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/tools/ImageProvider.java	(revision 7248)
@@ -59,5 +59,4 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
-import org.openstreetmap.josm.io.MirroredInputStream;
 import org.openstreetmap.josm.plugins.PluginHandler;
 import org.w3c.dom.Element;
@@ -75,4 +74,5 @@
 import com.kitfox.svg.SVGException;
 import com.kitfox.svg.SVGUniverse;
+import org.openstreetmap.josm.io.CachedFile;
 
 /**
@@ -533,9 +533,10 @@
 
     private static ImageResource getIfAvailableHttp(String url, ImageType type) {
-        try (MirroredInputStream is = new MirroredInputStream(url,
-                    new File(Main.pref.getCacheDirectory(), "images").getPath())) {
+        CachedFile cf = new CachedFile(url)
+                .setDestDir(new File(Main.pref.getCacheDirectory(), "images").getPath());
+        try (InputStream is = cf.getInputStream()) {
             switch (type) {
             case SVG:
-                URI uri = getSvgUniverse().loadSVG(is, Utils.fileToURL(is.getFile()).toString());
+                URI uri = getSvgUniverse().loadSVG(is, Utils.fileToURL(cf.getFile()).toString());
                 SVGDiagram svg = getSvgUniverse().getDiagram(uri);
                 return svg == null ? null : new ImageResource(svg);
@@ -543,5 +544,5 @@
                 BufferedImage img = null;
                 try {
-                    img = read(Utils.fileToURL(is.getFile()), false, false);
+                    img = read(Utils.fileToURL(cf.getFile()), false, false);
                 } catch (IOException e) {
                     Main.warn("IOException while reading HTTP image: "+e.getMessage());
@@ -800,8 +801,6 @@
             });
 
-            try (InputStream is = new MirroredInputStream(
-                    base + fn,
-                    new File(Main.pref.getPreferencesDir(), "images").toString())
-            ) {
+            CachedFile cf = new CachedFile(base + fn).setDestDir(new File(Main.pref.getPreferencesDir(), "images").toString());
+            try (InputStream is = cf.getInputStream()) {
                 parser.parse(new InputSource(is));
             }
Index: trunk/src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/tools/RightAndLefthandTraffic.java	(revision 7248)
@@ -11,6 +11,6 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.IllegalDataException;
-import org.openstreetmap.josm.io.MirroredInputStream;
 import org.openstreetmap.josm.io.OsmReader;
 import org.openstreetmap.josm.tools.GeoPropertyIndex.GeoProperty;
@@ -67,5 +67,5 @@
     private static void initialize() {
         leftHandTrafficPolygons = new ArrayList<>();
-        try (InputStream is = new MirroredInputStream("resource://data/left-right-hand-traffic.osm")) {
+        try (InputStream is = new CachedFile("resource://data/left-right-hand-traffic.osm").getInputStream()) {
             DataSet data = OsmReader.parseDataSet(is, null);
             for (Way w : data.getWays()) {
Index: trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java	(revision 7247)
+++ trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java	(revision 7248)
@@ -28,5 +28,5 @@
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.io.CachedFile;
 import org.xml.sax.Attributes;
 import org.xml.sax.ContentHandler;
@@ -281,5 +281,5 @@
     public Iterable<Object> startWithValidation(final Reader in, String namespace, String schemaSource) throws SAXException {
         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
-        try (InputStream mis = new MirroredInputStream(schemaSource)) {
+        try (InputStream mis = new CachedFile(schemaSource).getInputStream()) {
             Schema schema = factory.newSchema(new StreamSource(mis));
             ValidatorHandler validator = schema.newValidatorHandler();
