Index: trunk/test/unit/org/openstreetmap/josm/TestUtils.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/TestUtils.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/TestUtils.java	(revision 13733)
@@ -15,4 +15,8 @@
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.Temporal;
 import java.util.Arrays;
 import java.util.Collection;
@@ -38,4 +42,7 @@
 import org.openstreetmap.josm.tools.JosmRuntimeException;
 import org.openstreetmap.josm.tools.Utils;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -380,3 +387,46 @@
         }
     }
+
+    /**
+     * Return WireMock server serving files under ticker directory
+     * @param ticketId Ticket numeric identifier
+     * @return WireMock HTTP server on dynamic port
+     */
+    public static WireMockServer getWireMockServer(int ticketId) {
+            return new WireMockServer(
+                    WireMockConfiguration.options()
+                        .dynamicPort()
+                        .usingFilesUnderDirectory(getRegressionDataDir(ticketId))
+                    );
+    }
+
+    /**
+     * Return WireMock server serving files under ticker directory
+     * @return WireMock HTTP server on dynamic port
+     */
+    public static WireMockServer getWireMockServer() {
+            return new WireMockServer(
+                    WireMockConfiguration.options()
+                        .dynamicPort()
+                    );
+    }
+    /**
+     * Renders Temporal to RFC 1123 Date Time
+     * @param time
+     * @return string representation according to RFC1123 of time
+     */
+    public static String getHTTPDate(Temporal time) {
+        return DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC).format(time);
+    }
+
+    /**
+     * Renders java time stamp to RFC 1123 Date Time
+     * @param time
+     * @return string representation according to RFC1123 of time
+     */
+    public static String getHTTPDate(long time) {
+        return getHTTPDate(Instant.ofEpochMilli(time));
+    }
+
+
 }
Index: trunk/test/unit/org/openstreetmap/josm/actions/AddImageryLayerActionTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/actions/AddImageryLayerActionTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/actions/AddImageryLayerActionTest.java	(revision 13733)
@@ -70,9 +70,16 @@
     @Test
     public void testActionPerformedEnabledWms() {
-        wireMockRule.stubFor(get(urlEqualTo("/wms?VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities"))
+        wireMockRule.stubFor(get(urlEqualTo("/wms?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.1.1"))
                 .willReturn(aResponse()
-                    .withStatus(200)
-                    .withHeader("Content-Type", "text/xml")
-                    .withBodyFile("imagery/wms-capabilities.xml")));
+                        .withStatus(200)
+                        .withHeader("Content-Type", "text/xml")
+                        .withBodyFile("imagery/wms-capabilities.xml")));
+        wireMockRule.stubFor(get(urlEqualTo("/wms?SERVICE=WMS&REQUEST=GetCapabilities"))
+                .willReturn(aResponse()
+                        .withStatus(404)));
+        wireMockRule.stubFor(get(urlEqualTo("/wms?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0"))
+                .willReturn(aResponse()
+                        .withStatus(404)));
+
         new AddImageryLayerAction(new ImageryInfo("localhost", "http://localhost:" + wireMockRule.port() + "/wms?",
                 "wms_endpoint", null, null)).actionPerformed(null);
Index: trunk/test/unit/org/openstreetmap/josm/data/cache/HostLimitQueueTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/cache/HostLimitQueueTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/data/cache/HostLimitQueueTest.java	(revision 13733)
@@ -14,4 +14,5 @@
 import org.junit.Rule;
 import org.junit.Test;
+import org.openstreetmap.josm.data.imagery.TileJobOptions;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.tools.Logging;
@@ -54,5 +55,5 @@
 
         Task(ICacheAccess<String, CacheEntry> cache, URL url, AtomicInteger counter) {
-            super(cache, 1, 1, null);
+            super(cache, new TileJobOptions(1, 1, null, 10));
             this.url = url;
             this.counter = counter;
Index: trunk/test/unit/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJobTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJobTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJobTest.java	(revision 13733)
@@ -2,6 +2,8 @@
 package org.openstreetmap.josm.data.cache;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
@@ -9,12 +11,21 @@
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
+import org.apache.commons.jcs.engine.behavior.ICacheElement;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
+import org.openstreetmap.josm.data.imagery.TileJobOptions;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.tools.Logging;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.matching.UrlPattern;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -25,14 +36,26 @@
 public class JCSCachedTileLoaderJobTest {
 
+    /**
+     * mocked tile server
+     */
+    @Rule
+    public WireMockRule tileServer = new WireMockRule(WireMockConfiguration.options()
+            .dynamicPort());
+
     private static class TestCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, CacheEntry> {
         private String url;
         private String key;
 
-        TestCachedTileLoaderJob(String url, String key) throws IOException {
-            super(getCache(), 30000, 30000, null);
+        TestCachedTileLoaderJob(String url, String key)  {
+            this(url, key, (int) TimeUnit.DAYS.toSeconds(1));
+        }
+
+        TestCachedTileLoaderJob(String url, String key, int minimumExpiry)  {
+            super(getCache(), new TileJobOptions(30000, 30000, null, minimumExpiry));
 
             this.url = url;
             this.key = key;
         }
+
 
         @Override
@@ -52,5 +75,5 @@
         @Override
         protected CacheEntry createCacheEntry(byte[] content) {
-            return new CacheEntry("dummy".getBytes(StandardCharsets.UTF_8));
+            return new CacheEntry(content);
         }
     }
@@ -60,4 +83,5 @@
         private boolean ready;
         private LoadResult result;
+        private byte[] data;
 
         @Override
@@ -66,4 +90,7 @@
             this.ready = true;
             this.result = result;
+            if (data != null) {
+                this.data = data.content;
+            }
             this.notifyAll();
         }
@@ -113,16 +140,5 @@
         String key = "key_unknown_host";
         TestCachedTileLoaderJob job = new TestCachedTileLoaderJob("http://unkownhost.unkownhost/unkown", key);
-        Listener listener = new Listener();
-        job.submit(listener, true);
-        synchronized (listener) {
-            while (!listener.ready) {
-                try {
-                    listener.wait();
-                } catch (InterruptedException e1) {
-                    // do nothing, still wait
-                    Logging.trace(e1);
-                }
-            }
-        }
+        Listener listener = submitJob(job);
         assertEquals(LoadResult.FAILURE, listener.result); // because response will be cached, and that is checked below
         assertEquals("java.net.UnknownHostException: unkownhost.unkownhost", listener.attributes.getErrorMessage());
@@ -135,38 +151,328 @@
 
         job = new TestCachedTileLoaderJob("http://unkownhost.unkownhost/unkown", key);
-        listener = new Listener();
-        job.submit(listener, true);
+        listener = submitJob(job);
+        assertEquals(LoadResult.SUCCESS, listener.result);
+        assertFalse(job.isCacheElementValid());
+    }
+
+    private void doTestStatusCode(int responseCode) throws IOException {
+        TestCachedTileLoaderJob job = getStatusLoaderJob(responseCode);
+        Listener listener = submitJob(job);
+        assertEquals(responseCode, listener.attributes.getResponseCode());
+    }
+
+    private Listener submitJob(TestCachedTileLoaderJob job) throws IOException {
+        return submitJob(job, true);
+    }
+
+    private Listener submitJob(TestCachedTileLoaderJob job, boolean force) throws IOException {
+        Listener listener = new Listener();
+        job.submit(listener, force);
         synchronized (listener) {
             while (!listener.ready) {
                 try {
                     listener.wait();
-                } catch (InterruptedException e1) {
+                } catch (InterruptedException e) {
                     // do nothing, wait
-                    Logging.trace(e1);
+                    Logging.trace(e);
                 }
             }
         }
-        assertEquals(LoadResult.SUCCESS, listener.result);
-        assertFalse(job.isCacheElementValid());
-    }
-
-    @SuppressFBWarnings(value = "WA_NOT_IN_LOOP")
-    private void doTestStatusCode(int responseCode) throws IOException, InterruptedException {
-        TestCachedTileLoaderJob job = getStatusLoaderJob(responseCode);
-        Listener listener = new Listener();
-        job.submit(listener, true);
-        synchronized (listener) {
-            if (!listener.ready) {
-                listener.wait();
-            }
-        }
-        assertEquals(responseCode, listener.attributes.getResponseCode());
-    }
-
-    private static TestCachedTileLoaderJob getStatusLoaderJob(int responseCode) throws IOException {
+        return listener;
+    }
+
+    /**
+     * That no requst is made when entry is in cache and force == false
+     * @throws IOException
+     */
+    @Test
+    public void testNoRequestMadeWhenEntryInCache() throws IOException {
+        ICacheAccess<String, CacheEntry> cache = getCache();
+        long expires = TimeUnit.DAYS.toMillis(1);
+        long testStart = System.currentTimeMillis();
+        cache.put("test",
+                new CacheEntry("cached entry".getBytes(StandardCharsets.UTF_8)),
+                createEntryAttributes(expires, 200, testStart, "eTag")
+                );
+        createHeadGetStub(WireMock.urlEqualTo("/test"), expires, testStart, "eTag", "mock entry");
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
+        Listener listener = submitJob(job, false);
+        tileServer.verify(0, WireMock.getRequestedFor(WireMock.anyUrl()));
+        assertArrayEquals("cached entry".getBytes(StandardCharsets.UTF_8), listener.data);
+    }
+
+    /**
+     * that request is made, when object is in cache, but force mode is used
+     * @throws IOException
+     */
+    @Test
+    public void testRequestMadeWhenEntryInCacheAndForce() throws IOException {
+        ICacheAccess<String, CacheEntry> cache = getCache();
+        long expires =  TimeUnit.DAYS.toMillis(1);
+        long testStart = System.currentTimeMillis();
+        cache.put("test",
+                new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
+                createEntryAttributes(expires, 200, testStart + expires, "eTag")
+                );
+        createHeadGetStub(WireMock.urlEqualTo("/test"), expires, testStart, "eTag", "mock entry");
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
+        Listener listener = submitJob(job, true);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+    }
+
+    /**
+     * Mock returns no cache-control / expires headers
+     * Expire time should be set to DEFAULT_EXPIRE_TIME
+     * @throws IOException
+     */
+    @Test
+    public void testSettingMinimumExpiryWhenNoExpires() throws IOException {
+        long testStart = System.currentTimeMillis();
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withBody("mock entry")
+                        )
+                );
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
+        Listener listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
+                JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME + " (DEFAULT_EXPIRE_TIME)",
+                listener.attributes.getExpirationTime() >= testStart + JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME);
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
+                JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME + " (DEFAULT_EXPIRE_TIME)",
+                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME);
+
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+    }
+
+    /**
+     * Mock returns expires headers, but Cache-Control
+     * Expire time should be set to max-age
+     * @throws IOException
+     */
+    @Test
+    public void testSettingExpireByMaxAge() throws IOException {
+        long testStart = System.currentTimeMillis();
+        long expires =  TimeUnit.DAYS.toSeconds(1);
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Cache-control", "max-age=" + expires)
+                        .withBody("mock entry")
+                        )
+                );
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
+        Listener listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
+                TimeUnit.SECONDS.toMillis(expires) + " (max-age)",
+                listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(expires));
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
+                TimeUnit.SECONDS.toMillis(expires) + " (max-age)",
+                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expires));
+
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+    }
+
+    /**
+     * mock returns expiration: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10
+     * minimum expire time: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 2
+     * @throws IOException
+     */
+    @Test
+    public void testSettingMinimumExpiryByMinimumExpiryTimeLessThanDefault() throws IOException {
+        long testStart = System.currentTimeMillis();
+        int minimumExpiryTimeSeconds = (int)(JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 2);
+
+        createHeadGetStub(WireMock.urlEqualTo("/test"), (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10), testStart, "eTag", "mock entry");
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test", minimumExpiryTimeSeconds);
+        Listener listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
+                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
+                listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) );
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
+                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
+                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds));
+    }
+
+    /**
+     * mock returns expiration: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10
+     * minimum expire time: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME * 2
+     * @throws IOException
+     */
+
+    @Test
+    public void testSettingMinimumExpiryByMinimumExpiryTimeGreaterThanDefault() throws IOException {
+        long testStart = System.currentTimeMillis();
+        int minimumExpiryTimeSeconds = (int)(JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME * 2);
+
+        createHeadGetStub(WireMock.urlEqualTo("/test"), (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10), testStart, "eTag", "mock entry");
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test", minimumExpiryTimeSeconds);
+        Listener listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
+                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
+                listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) );
+
+        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
+                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
+                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds));
+    }
+
+    /**
+     * Check if verifying cache entries using HEAD requests work properly
+     * @throws IOException
+     */
+    @Test
+    public void testCheckUsingHead() throws IOException {
+        ICacheAccess<String, CacheEntry> cache = getCache();
+        long expires = TimeUnit.DAYS.toMillis(1);
+        long testStart = System.currentTimeMillis();
+        cache.put("test",
+                new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
+                createEntryAttributes(-1 * expires, 200, testStart, "eTag--gzip") // Jetty adds --gzip to etags when compressing output
+                );
+
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
+                        .withHeader("Last-Modified", Long.toString(testStart))
+                        .withHeader("ETag", "eTag") // Jetty adds "--gzip" suffix for compressed content
+                        .withBody("mock entry")
+                        )
+                );
+        tileServer.stubFor(
+                WireMock.head(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
+                        .withHeader("Last-Modified", Long.toString(testStart))
+                        .withHeader("ETag", "eTag--gzip") // but doesn't add to uncompressed
+                        )
+                );
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
+        Listener listener = submitJob(job, false); // cache entry is expired, no need to force refetch
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+
+        // cache entry should be retrieved from cache
+        listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+
+        // invalidate entry in cache
+        ICacheElement<String, CacheEntry> cacheEntry = cache.getCacheElement("test");
+        CacheEntryAttributes attributes = (CacheEntryAttributes)cacheEntry.getElementAttributes();
+        attributes.setExpirationTime(testStart - TimeUnit.DAYS.toMillis(1));
+        cache.put("test", cacheEntry.getVal(), attributes);
+
+        // because cache entry is invalid - HEAD request shall be made
+        tileServer.verify(0, WireMock.headRequestedFor(WireMock.urlEqualTo("/test"))); // no head requests were made until now
+        listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.headRequestedFor(WireMock.urlEqualTo("/test"))); // verify head requests were made
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test"))); // verify no more get requests were made
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+        assertTrue(listener.attributes.getExpirationTime() >= testStart + expires);
+
+        // cache entry should be retrieved from cache
+        listener = submitJob(job, false); // cache entry is expired, no need to force refetch
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
+    }
+
+    /**
+     * Check if server returns 304 - it will update cache attributes and not ask again for it
+     * @throws IOException
+     */
+    @Test
+    public void testCheckUsing304() throws IOException {
+        ICacheAccess<String, CacheEntry> cache = getCache();
+        long expires = TimeUnit.DAYS.toMillis(1);
+        long testStart = System.currentTimeMillis();
+        cache.put("test",
+                new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
+                createEntryAttributes(-1 * expires, 200, testStart, "eTag")
+                );
+
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.status(304)
+                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
+                        .withHeader("Last-Modified", Long.toString(testStart))
+                        .withHeader("ETag", "eTag")
+                        )
+                );
+
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
+        Listener listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("cached dummy".getBytes(StandardCharsets.UTF_8), listener.data);
+        assertTrue(testStart + expires <= listener.attributes.getExpirationTime());
+        listener = submitJob(job, false);
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test"))); // no more requests were made
+    }
+
+    private void createHeadGetStub(UrlPattern url, long expires, long lastModified, String eTag, String body) {
+        tileServer.stubFor(
+                WireMock.get(url)
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Expires", TestUtils.getHTTPDate(lastModified + expires))
+                        .withHeader("Last-Modified", Long.toString(lastModified))
+                        .withHeader("ETag", eTag)
+                        .withBody(body)
+                        )
+                );
+        tileServer.stubFor(
+                WireMock.head(url)
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Expires", TestUtils.getHTTPDate(lastModified + expires))
+                        .withHeader("Last-Modified", Long.toString(lastModified))
+                        .withHeader("ETag", eTag)
+                        )
+                );
+    }
+
+    private CacheEntryAttributes createEntryAttributes(long maxAge, int responseCode, String eTag) {
+        long validTo = maxAge + System.currentTimeMillis();
+        return createEntryAttributes(maxAge, responseCode, validTo, eTag);
+    }
+
+    private CacheEntryAttributes createEntryAttributes(long expirationTime, int responseCode, long lastModification, String eTag) {
+        CacheEntryAttributes entryAttributes = new CacheEntryAttributes();
+        entryAttributes.setExpirationTime(lastModification + expirationTime);
+        entryAttributes.setResponseCode(responseCode);
+        entryAttributes.setLastModification(lastModification);
+        entryAttributes.setEtag(eTag);
+        return entryAttributes;
+    }
+
+    private static TestCachedTileLoaderJob getStatusLoaderJob(int responseCode)  {
         return new TestCachedTileLoaderJob("http://httpstat.us/" + responseCode, "key_" + responseCode);
     }
 
-    private static ICacheAccess<String, CacheEntry> getCache() throws IOException {
+    private static ICacheAccess<String, CacheEntry> getCache() {
         return JCSCacheManager.getCache("test");
     }
Index: trunk/test/unit/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJobTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJobTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJobTest.java	(revision 13733)
@@ -2,13 +2,35 @@
 package org.openstreetmap.josm.data.imagery;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 
+import org.apache.commons.jcs.access.behavior.ICacheAccess;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
+import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
+import org.openstreetmap.josm.data.cache.JCSCacheManager;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -24,5 +46,97 @@
     @Rule
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
+    public JOSMTestRules test = new JOSMTestRules().preferences();
+
+    /**
+     * mocked tile server
+     */
+    @Rule
+    public WireMockRule tileServer = new WireMockRule(WireMockConfiguration.options()
+            .dynamicPort());
+
+    @Before
+    public void clearCache() throws Exception {
+        getCache().clear();
+    }
+
+    private static ICacheAccess<String, BufferedImageCacheEntry> getCache() {
+        return JCSCacheManager.getCache("test");
+    }
+
+    private static class TestCachedTileLoaderJob extends TMSCachedTileLoaderJob {
+        private String url;
+        private String key;
+
+        TestCachedTileLoaderJob(TileLoaderListener listener, Tile tile, String key) throws IOException  {
+            this(listener, tile, key,  (int) TimeUnit.DAYS.toSeconds(1));
+        }
+
+        TestCachedTileLoaderJob(TileLoaderListener listener, Tile tile, String key, int minimumExpiry) throws IOException  {
+            super(listener, tile, getCache(), new TileJobOptions(30000, 30000, null, minimumExpiry),
+                    (ThreadPoolExecutor) Executors.newFixedThreadPool(1));
+
+            this.url = tile.getUrl();
+            this.key = key;
+        }
+
+        @Override
+        public URL getUrl() {
+            try {
+                return new URL(url);
+            } catch (MalformedURLException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
+            return new BufferedImageCacheEntry(content);
+        }
+
+        public CacheEntryAttributes getAttributes() {
+            return attributes;
+        }
+
+        @Override
+        public boolean isObjectLoadable() {
+            // use implementation from grand parent, to avoid calling getImage on dummy data
+            if (cacheData == null) {
+                return false;
+            }
+            return cacheData.getContent().length > 0;        }
+    }
+
+    private static class Listener implements TileLoaderListener {
+        private CacheEntryAttributes attributes;
+        private boolean ready;
+        private byte[] data;
+
+
+        @Override
+        public synchronized void tileLoadingFinished(Tile tile, boolean success) {
+            ready = true;
+            this.notifyAll();
+        }
+    }
+
+    private static class MockTile extends Tile {
+        MockTile(String url) {
+            super(new MockTileSource(url), 0, 0, 0);
+        }
+    }
+
+    private static class MockTileSource extends TMSTileSource {
+        private final String url;
+
+        public MockTileSource(String url) {
+            super(new ImageryInfo("mock"));
+            this.url = url;
+        }
+
+        @Override
+        public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
+            return url;
+        }
+    }
 
     /**
@@ -53,3 +167,148 @@
         assertEquals(expected, Utils.strip(m.group(1)));
     }
+
+    private TestCachedTileLoaderJob submitJob(MockTile tile, String key, boolean force) throws IOException {
+        return submitJob(tile, key, 0, force);
+    }
+
+    private TestCachedTileLoaderJob submitJob(MockTile tile, String key, int minimumExpiry, boolean force) throws IOException {
+        Listener listener = new Listener();
+        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(listener, tile, key, minimumExpiry);
+        job.submit(force);
+        synchronized (listener) {
+            while (!listener.ready) {
+                try {
+                    listener.wait();
+                } catch (InterruptedException e) {
+                    // do nothing, wait
+                    Logging.trace(e);
+                }
+            }
+        }
+        return job;
+    }
+
+    /**
+     * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least MINIMUM_EXPIRES
+     * @throws IOException
+     */
+    @Test
+    public void testNoCacheHeaders() throws IOException {
+        long testStart = System.currentTimeMillis();
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withBody("mock entry")
+                        )
+                );
+
+        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
+        assertExpirationAtLeast(testStart + TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get(), job);
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
+        // only one request to tile server should be made, second should come from cache
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+    }
+
+    /**
+     * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least minimumExpires parameter
+     * @throws IOException
+     */
+    @Test
+    public void testNoCacheHeadersMinimumExpires() throws IOException {
+        noCacheHeadersMinimumExpires((int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get() * 2));
+    }
+
+    /**
+     * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least minimumExpires parameter,
+     * which is larger than MAXIMUM_EXPIRES
+     * @throws IOException
+     */
+
+    @Test
+    public void testNoCacheHeadersMinimumExpiresLargerThanMaximum() throws IOException {
+        noCacheHeadersMinimumExpires((int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() * 2));
+    }
+
+    private void noCacheHeadersMinimumExpires(int minimumExpires) throws IOException {
+        long testStart = System.currentTimeMillis();
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withBody("mock entry")
+                        )
+                );
+        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", minimumExpires, false);
+        assertExpirationAtLeast(testStart + minimumExpires, job);
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
+        // only one request to tile server should be made, second should come from cache
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+    }
+
+    /**
+     * When tile server returns Expires header shorter than MINIMUM_EXPIRES, we should cache if for at least MINIMUM_EXPIRES
+     * @throws IOException
+     */
+    @Test
+    public void testShortExpire() throws IOException {
+        long testStart = System.currentTimeMillis();
+        long expires = TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get() / 2;
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
+                        .withBody("mock entry")
+                        )
+                );
+        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
+        assertExpirationAtLeast(testStart + TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get(), job);
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
+        // only one request to tile server should be made, second should come from cache
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+    }
+
+    private void assertExpirationAtLeast(long duration, TestCachedTileLoaderJob job) {
+        assertTrue(
+                "Expiration time shorter by " +
+                        -1 * (job.getAttributes().getExpirationTime() - duration) +
+                        " than expected",
+                job.getAttributes().getExpirationTime() >= duration);
+    }
+
+    private void assertExpirationAtMost(long duration, TestCachedTileLoaderJob job) {
+        assertTrue(
+                "Expiration time longer by " +
+                        (job.getAttributes().getExpirationTime() - duration) +
+                        " than expected",
+                job.getAttributes().getExpirationTime() <= duration);
+    }
+
+
+    @Test
+    public void testLongExpire() throws IOException {
+        long testStart = System.currentTimeMillis();
+        long expires = TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() * 2;
+        tileServer.stubFor(
+                WireMock.get(WireMock.urlEqualTo("/test"))
+                .willReturn(WireMock.aResponse()
+                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
+                        .withBody("mock entry")
+                        )
+                );
+        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
+        // give 1 second margin
+        assertExpirationAtMost(testStart + TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() + TimeUnit.SECONDS.toMillis(1), job);
+
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
+        // only one request to tile server should be made, second should come from cache
+        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
+        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
+    }
+
 }
Index: trunk/test/unit/org/openstreetmap/josm/data/imagery/WMSEndpointTileSourceTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/imagery/WMSEndpointTileSourceTest.java	(revision 13733)
+++ trunk/test/unit/org/openstreetmap/josm/data/imagery/WMSEndpointTileSourceTest.java	(revision 13733)
@@ -0,0 +1,72 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import static org.junit.Assert.assertEquals;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.projection.Projections;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+public class WMSEndpointTileSourceTest {
+    /**
+     * Setup test
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().platform().projection();
+
+    @Test
+    public void testDefaultLayerSetInMaps() throws Exception {
+        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
+        String getCapabilitiesBody = new String(Files.readAllBytes(Paths.get(TestUtils.getTestDataRoot() + "wms/geofabrik-osm-inspector.xml")), "UTF-8");
+        // do not use withFileBody as it needs different directory layout :(
+        getCapabilitiesMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(getCapabilitiesBody)));
+        getCapabilitiesMock.start();
+
+        WireMockServer mapsMock = TestUtils.getWireMockServer();
+        mapsMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(
+                "<?xml version='1.0' encoding='UTF-8'?>\n" +
+                "<imagery xmlns=\"http://josm.openstreetmap.de/maps-1.0\">\n" +
+                "<entry>\n" +
+                "<name>OSM Inspector: Geometry</name>\n" +
+                "<id>OSM_Inspector-Geometry</id>\n" +
+                "<type>wms_endpoint</type>\n" +
+                "<url><![CDATA[" + getCapabilitiesMock.url("/any") + "]]></url>\n" +
+                "<icon>data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAAB5UlEQVQ4y4WTwWsTURDGfy8W1yYmXZOqtGJJFyGw6KF7CEigwYuS0kthrYUi4i0iORS9BU9hQdA/ILcixVBrwENKLz1FUBB0wWOwYFAqxUNYTZq6BfM8yC5d05iBObz3vfnmm3kz4sqDh/zP7szdlG5I+Of1zQ1xFA8xxI4GH2cjg4Cl+UUJcC4SJq6c7FPkKRlIoPQk0+NnuDwxHrhvuYd83+8OVuBlHouE/eDXzW8+/qO9DyHB0vyiVHoy2INSNiPdeg23XuPs3icmIoofPKXGmFJjjEUjgf4EFNi2TT6fJ5FI0Gg0ePrkMRfnbvn41QsJgEAJAQUdbYZyuQxAcvoSpmnydesFAF+cn8f2KUCw/fGt6GgzWJbF706bVCoFwGxyktnk5N8kB79QepL1zQ3xbOulCJWyGbkQHZWlbEZ6JIZhBDI1nQ5Np8P2zi4t9zAwGyNe3QALti11XSedTvsPYrEY73f3Bk+irusAnI6qrNy7z43sNUbFCQC6LYdCoYBbr/k1/2sh690HUalUaH7eIRxXA+6RFItF3HqN6+dP9REIb5lK2Yy0bdsHDMMgl8vRbTkAhOMqlmVhmibLq2ui7xsf1d+IV+0D3zVNw7KsPiXVapXnd2/Lodu4vLomTNMcSvIHY6bDkqJtEqIAAAAASUVORK5CYII=</icon>\n" +
+                "<attribution-text mandatory=\"true\">© Geofabrik GmbH, OpenStreetMap contributors, CC-BY-SA</attribution-text>\n" +
+                "<attribution-url>http://tools.geofabrik.de/osmi/</attribution-url>\n" +
+                "<max-zoom>18</max-zoom>\n" +
+                "<valid-georeference>true</valid-georeference>\n" +
+                "<defaultLayers>" +
+                "<layer name=\"single_node_in_way\" style=\"default\" />" +
+                "</defaultLayers>" +
+                "</entry>\n" +
+                "</imagery>"
+                )));
+        mapsMock.start();
+        Config.getPref().put("josm.url", mapsMock.url("/"));
+        ImageryLayerInfo.instance.loadDefaults(true, null, false);
+        assertEquals(1, ImageryLayerInfo.instance.getDefaultLayers().size());
+        ImageryInfo wmsImageryInfo = ImageryLayerInfo.instance.getDefaultLayers().get(0);
+        assertEquals("single_node_in_way", wmsImageryInfo.getDefaultLayers().get(0).getLayerName());
+        WMSEndpointTileSource tileSource = new WMSEndpointTileSource(wmsImageryInfo, Main.getProjection());
+        tileSource.initProjection(Projections.getProjectionByCode("EPSG:3857"));
+        assertEquals("https://tools.geofabrik.de/osmi/views/geometry/wxs?FORMAT=image/png&TRANSPARENT=TRUE&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&"
+                + "LAYERS=single_node_in_way&STYLES=default&"
+                + "SRS=EPSG:3857&WIDTH=512&HEIGHT=512&"
+                + "BBOX=20037506.6204108,-60112521.5836107,60112521.5836107,-20037506.6204108", tileSource.getTileUrl(1, 1, 1));
+
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/data/imagery/WMTSTileSourceTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/data/imagery/WMTSTileSourceTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/data/imagery/WMTSTileSourceTest.java	(revision 13733)
@@ -8,9 +8,12 @@
 import java.io.IOException;
 import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.Collection;
-
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.ClassRule;
 import org.junit.Ignore;
-import org.junit.Rule;
 import org.junit.Test;
 import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
@@ -19,6 +22,12 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
 import org.openstreetmap.josm.data.projection.Projections;
+import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -28,4 +37,11 @@
  */
 public class WMTSTileSourceTest {
+
+    /**
+     * Setup test.
+     */
+    @ClassRule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public static JOSMTestRules test = new JOSMTestRules().preferences().platform();
 
     private ImageryInfo testImageryTMS = new ImageryInfo("test imagery", "http://localhost", "tms", null, null);
@@ -44,17 +60,13 @@
             "wmts/bug13975-multiple-tile-matrices-for-one-layer-projection.xml");
 
-    /**
-     * Setup test.
-     */
-    @Rule
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
 
     private static ImageryInfo getImagery(String path) {
         try {
-            return new ImageryInfo(
+            ImageryInfo ret = new ImageryInfo(
                     "test",
                     new File(path).toURI().toURL().toString()
                     );
+            ret.setImageryType(ImageryType.WMTS);
+            return ret;
         } catch (MalformedURLException e) {
             e.printStackTrace();
@@ -64,5 +76,5 @@
 
     @Test
-    public void testPseudoMercator() throws IOException {
+    public void testPseudoMercator() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
         WMTSTileSource testSource = new WMTSTileSource(testImageryPSEUDO_MERCATOR);
@@ -94,5 +106,5 @@
 
     @Test
-    public void testWALLONIE() throws IOException {
+    public void testWALLONIE() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:31370"));
         WMTSTileSource testSource = new WMTSTileSource(testImageryWALLONIE);
@@ -114,5 +126,5 @@
     @Test
     @Ignore("disable this test, needs further working") // XXX
-    public void testWALLONIENoMatrixDimension() throws IOException {
+    public void testWALLONIENoMatrixDimension() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:31370"));
         WMTSTileSource testSource = new WMTSTileSource(getImagery("test/data/wmts/WMTSCapabilities-Wallonie-nomatrixdimension.xml"));
@@ -138,5 +150,5 @@
 
     @Test
-    public void testWIEN() throws IOException {
+    public void testWIEN() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
         WMTSTileSource testSource = new WMTSTileSource(testImageryWIEN);
@@ -180,5 +192,5 @@
 
     @Test
-    public void testGeoportalTOPOPL() throws IOException {
+    public void testGeoportalTOPOPL() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:4326"));
         WMTSTileSource testSource = new WMTSTileSource(testImageryTOPO_PL);
@@ -202,5 +214,5 @@
 
     @Test
-    public void testGeoportalORTOPL4326() throws IOException {
+    public void testGeoportalORTOPL4326() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:4326"));
         WMTSTileSource testSource = new WMTSTileSource(testImageryORTO_PL);
@@ -211,5 +223,5 @@
 
     @Test
-    public void testGeoportalORTOPL2180() throws IOException {
+    public void testGeoportalORTOPL2180() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:2180"));
         WMTSTileSource testSource = new WMTSTileSource(testImageryORTO_PL);
@@ -221,5 +233,5 @@
 
     @Test
-    public void testTicket12168() throws IOException {
+    public void testTicket12168() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
         WMTSTileSource testSource = new WMTSTileSource(testImagery12168);
@@ -231,15 +243,36 @@
 
     @Test
-    @Ignore("disabled as this needs user action") // XXX
     public void testTwoTileSetsForOneProjection() throws Exception {
         Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
-        WMTSTileSource testSource = new WMTSTileSource(testImageryOntario);
-        testSource.initProjection(Main.getProjection());
-        verifyTile(new LatLon(45.4105023, -75.7153702), testSource, 303751, 375502, 12);
-        verifyTile(new LatLon(45.4601306, -75.7617187), testSource, 1186, 1466, 4);
-    }
-
-    @Test
-    @Ignore("disabled as this needs user action") // XXX
+        ImageryInfo ontario = getImagery(TestUtils.getTestDataRoot() + "wmts/WMTSCapabilities-Ontario.xml");
+        ontario.setDefaultLayers(Arrays.asList(new DefaultLayer[] {
+                new DefaultLayer(ImageryType.WMTS, "Basemap_Imagery_2014", null, "default028mm")
+        }));
+        WMTSTileSource testSource = new WMTSTileSource(ontario);
+        testSource.initProjection(Main.getProjection());
+        assertEquals(
+                "http://maps.ottawa.ca/arcgis/rest/services/Basemap_Imagery_2014/MapServer/WMTS/tile/1.0.0/Basemap_Imagery_2014/default/default028mm/4/2932/2371.jpg",
+                testSource.getTileUrl(4, 2371, 2932));
+        verifyTile(new LatLon(45.4601306, -75.7617187), testSource, 2372, 2932, 4);
+        verifyTile(new LatLon(45.4602510, -75.7617187), testSource, 607232, 750591, 12);
+    }
+
+    @Test
+    public void testTwoTileSetsForOneProjectionSecondLayer() throws Exception {
+        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
+        ImageryInfo ontario = getImagery(TestUtils.getTestDataRoot() + "wmts/WMTSCapabilities-Ontario.xml");
+        ontario.setDefaultLayers(Arrays.asList(new DefaultLayer[] {
+                new DefaultLayer(ImageryType.WMTS, "Basemap_Imagery_2014", null, "GoogleMapsCompatible")
+        }));
+        WMTSTileSource testSource = new WMTSTileSource(ontario);
+        testSource.initProjection(Main.getProjection());
+        assertEquals(
+                "http://maps.ottawa.ca/arcgis/rest/services/Basemap_Imagery_2014/MapServer/WMTS/tile/1.0.0/Basemap_Imagery_2014/default/GoogleMapsCompatible/4/2932/2371.jpg",
+                testSource.getTileUrl(4, 2371, 2932));
+        verifyMercatorTile(testSource, 74, 91, 8);
+        verifyMercatorTile(testSource, 37952, 46912, 17);
+    }
+
+    @Test
     public void testManyLayersScrollbars() throws Exception {
         Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
@@ -271,6 +304,6 @@
         Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
         ImageryInfo copy = new ImageryInfo(testMultipleTileMatrixForLayer);
-        Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
-        defaultLayers.add(new WMTSDefaultLayer("Mashhad_BaseMap_1", "default028mm"));
+        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
+        defaultLayers.add(new DefaultLayer(ImageryType.WMTS, "Mashhad_BaseMap_1", null, "default028mm"));
         copy.setDefaultLayers(defaultLayers);
         WMTSTileSource testSource = new WMTSTileSource(copy);
@@ -286,11 +319,12 @@
      * Test WMTS dimension.
      * @throws IOException if any I/O error occurs
+     * @throws WMTSGetCapabilitiesException
      */
     @Test
-    public void testDimension() throws IOException {
+    public void testDimension() throws IOException, WMTSGetCapabilitiesException {
         Main.setProjection(Projections.getProjectionByCode("EPSG:21781"));
         ImageryInfo info = new ImageryInfo(testImageryGeoAdminCh);
-        Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
-        defaultLayers.add(new WMTSDefaultLayer("ch.are.agglomerationen_isolierte_staedte", "21781_26"));
+        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
+        defaultLayers.add(new DefaultLayer(ImageryType.WMTS, "ch.are.agglomerationen_isolierte_staedte", null, "21781_26"));
         info.setDefaultLayers(defaultLayers);
         WMTSTileSource testSource = new WMTSTileSource(info);
@@ -300,4 +334,45 @@
                 testSource.getTileUrl(1, 2, 3)
                 );
+    }
+
+    @Test
+    public void testDefaultLayer() throws Exception {
+        // https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/1.0.0/WMTSCapabilities.xml
+        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
+        String getCapabilitiesBody = new String(Files.readAllBytes(Paths.get(TestUtils.getTestDataRoot() + "wmts/getCapabilities-lots-of-layers.xml")), "UTF-8");
+        // do not use withFileBody as it needs different directory layout :(
+        getCapabilitiesMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(getCapabilitiesBody)));
+        getCapabilitiesMock.start();
+
+        WireMockServer mapsMock = TestUtils.getWireMockServer();
+        mapsMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(
+                "<?xml version='1.0' encoding='UTF-8'?>\n" +
+                "<imagery xmlns=\"http://josm.openstreetmap.de/maps-1.0\">\n" +
+                "<entry>\n" +
+                "<name>Landsat</name>\n" +
+                "<id>landsat</id>\n" +
+                "<type>wmts</type>\n" +
+                "<url><![CDATA[" + getCapabilitiesMock.url("/getcapabilities.xml") + "]]></url>\n" +
+                "<defaultLayers>" +
+                "<layer name=\"GEOGRAPHICALGRIDSYSTEMS.MAPS\" />" +
+                "</defaultLayers>" +
+                "</entry>\n" +
+                "</imagery>"
+                )));
+        mapsMock.start();
+        Config.getPref().put("josm.url", mapsMock.url("/"));
+
+        ImageryLayerInfo.instance.loadDefaults(true, null, false);
+
+        assertEquals(1, ImageryLayerInfo.instance.getDefaultLayers().size());
+        ImageryInfo wmtsImageryInfo = ImageryLayerInfo.instance.getDefaultLayers().get(0);
+        assertEquals(1, wmtsImageryInfo.getDefaultLayers().size());
+        assertEquals("GEOGRAPHICALGRIDSYSTEMS.MAPS", wmtsImageryInfo.getDefaultLayers().get(0).getLayerName());
+        WMTSTileSource tileSource = new WMTSTileSource(wmtsImageryInfo);
+        tileSource.initProjection(Projections.getProjectionByCode("EPSG:3857"));
+        assertEquals("http://wxs.ign.fr/61fs25ymczag0c67naqvvmap/geoportail/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&"
+                + "LAYER=GEOGRAPHICALGRIDSYSTEMS.MAPS"
+                + "&STYLE=normal&FORMAT=image/jpeg&tileMatrixSet=PM&tileMatrix=1&tileRow=1&tileCol=1", tileSource.getTileUrl(1, 1, 1));
+
     }
 
Index: trunk/test/unit/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayerTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayerTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayerTest.java	(revision 13733)
@@ -120,5 +120,6 @@
             return new TileLoaderFactory() {
                 @Override
-                public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers) {
+                public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers,
+                        long minimumExpiryTime) {
                     return null;
                 }
Index: trunk/test/unit/org/openstreetmap/josm/gui/layer/WMTSLayerTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/layer/WMTSLayerTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/gui/layer/WMTSLayerTest.java	(revision 13733)
@@ -22,5 +22,5 @@
     @Rule
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().timeout(20000);
+    public JOSMTestRules test = new JOSMTestRules().preferences().timeout(20000);
 
     /**
Index: trunk/test/unit/org/openstreetmap/josm/io/imagery/WMSImageryTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/io/imagery/WMSImageryTest.java	(revision 13712)
+++ trunk/test/unit/org/openstreetmap/josm/io/imagery/WMSImageryTest.java	(revision 13733)
@@ -6,5 +6,8 @@
 
 import java.io.IOException;
-import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
 
 import org.junit.Rule;
@@ -13,4 +16,7 @@
 import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -26,5 +32,5 @@
     @Rule
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
+    public JOSMTestRules test = new JOSMTestRules().platform().projection();
 
     /**
@@ -49,10 +55,23 @@
     @Test
     public void testTicket15730() throws IOException, WMSGetCapabilitiesException {
-        try (InputStream is = TestUtils.getRegressionDataStream(15730, "capabilities.xml")) {
-            WMSImagery wms = new WMSImagery();
-            wms.parseCapabilities(null, is);
-            assertEquals(1, wms.getLayers().size());
-            assertTrue(wms.getLayers().get(0).abstr.startsWith("South Carolina  NAIP Imagery 2017    Resolution: 100CM "));
-        }
+       WireMockServer wm = TestUtils.getWireMockServer(15730);
+       wm.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBodyFile("capabilities.xml")));
+       wm.start();
+       WMSImagery wms = new WMSImagery(wm.url("capabilities.xml"));
+       assertEquals(1, wms.getLayers().size());
+       assertTrue(wms.getLayers().get(0).getAbstract().startsWith("South Carolina  NAIP Imagery 2017    Resolution: 100CM "));
+       wm.shutdown();
+    }
+
+    @Test
+    public void testNestedLayers() throws Exception {
+        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
+        String getCapabilitiesBody = new String(Files.readAllBytes(Paths.get(TestUtils.getTestDataRoot() + "wms/mapa-um-warszawa-pl.xml")), "UTF-8");
+        getCapabilitiesMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(getCapabilitiesBody)));
+        getCapabilitiesMock.start();
+        WMSImagery wmsi = new WMSImagery(getCapabilitiesMock.url("/serwis"));
+        assertEquals(1, wmsi.getLayers().size());
+        assertEquals("Server WMS m.st. Warszawy", wmsi.getLayers().get(0).toString());
+        assertEquals(202, wmsi.getLayers().get(0).getChildren().size());
     }
 
@@ -64,9 +83,17 @@
     @Test
     public void testTicket16248() throws IOException, WMSGetCapabilitiesException {
-        try (InputStream is = TestUtils.getRegressionDataStream(16248, "capabilities.xml")) {
-            WMSImagery wms = new WMSImagery();
-            wms.parseCapabilities(null, is);
-            assertEquals("http://wms.hgis.cartomatic.pl/topo/3857/m25k", wms.getServiceUrl().toExternalForm());
-        }
+        Path capabilitiesPath = Paths.get(TestUtils.getRegressionDataFile(16248, "capabilities.xml"));
+        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
+        getCapabilitiesMock.stubFor(
+                WireMock.get(WireMock.anyUrl())
+                .willReturn(WireMock.aResponse().withBody(Files.readAllBytes(capabilitiesPath))));
+        getCapabilitiesMock.start();
+        WMSImagery wms = new WMSImagery(getCapabilitiesMock.url("any"));
+        assertEquals("http://wms.hgis.cartomatic.pl/topo/3857/m25k", wms.buildRootUrl());
+        assertEquals("wms.hgis.cartomatic.pl", wms.getLayers().get(0).getName());
+        assertEquals("http://wms.hgis.cartomatic.pl/topo/3857/m25kFORMAT=image/png&TRANSPARENT=TRUE&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&"
+                + "LAYERS=wms.hgis.cartomatic.pl&STYLES=&SRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}",
+                wms.buildGetMapUrl(wms.getLayers(), (List<String>)null, true));
     }
 }
+
