Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java	(revision 35319)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java	(revision 35320)
@@ -22,4 +22,5 @@
 public final class FeatureAdapter {
 
+    private static ApiKeyAdapter apiKeyAdapter = new DefaultApiKeyAdapter();
     private static BrowserAdapter browserAdapter = new DefaultBrowserAdapter();
     private static ImageAdapter imageAdapter = new DefaultImageAdapter();
@@ -32,18 +33,84 @@
     }
 
+    /**
+     * Provider of confidential API keys.
+     */
+    @FunctionalInterface
+    public interface ApiKeyAdapter {
+        /**
+         * Retrieves the API key for the given imagery id.
+         * @param imageryId imagery id
+         * @return the API key for the given imagery id
+         */
+        String retrieveApiKey(String imageryId);
+    }
+
+    /**
+     * Link browser.
+     */
+    @FunctionalInterface
     public interface BrowserAdapter {
+        /**
+         * Browses to a given link.
+         * @param url link
+         */
         void openLink(String url);
     }
 
+    /**
+     * Translation support.
+     */
     public interface TranslationAdapter {
+        /**
+         * Translates some text for the current locale.
+         * <br>
+         * For example, <code>tr("JMapViewer''s default value is ''{0}''.", val)</code>.
+         * <br>
+         * @param text the text to translate.
+         * Must be a string literal. (No constants or local vars.)
+         * Can be broken over multiple lines.
+         * An apostrophe ' must be quoted by another apostrophe.
+         * @param objects the parameters for the string.
+         * Mark occurrences in {@code text} with <code>{0}</code>, <code>{1}</code>, ...
+         * @return the translated string.
+         */
         String tr(String text, Object... objects);
         // TODO: more i18n functions
     }
 
+    /**
+     * Logging support.
+     */
+    @FunctionalInterface
     public interface LoggingAdapter {
+        /**
+         * Retrieves a logger for the given name.
+         * @param name logger name
+         * @return logger for the given name
+         */
         Logger getLogger(String name);
     }
 
+    /**
+     * Image provider.
+     */
+    @FunctionalInterface
     public interface ImageAdapter {
+        /**
+         * Returns a <code>BufferedImage</code> as the result of decoding a supplied <code>URL</code>.
+         *
+         * @param input a <code>URL</code> to read from.
+         * @param readMetadata if {@code true}, makes sure to read image metadata to detect transparency color for non translucent images,
+         * if any.
+         * Always considered {@code true} if {@code enforceTransparency} is also {@code true}
+         * @param enforceTransparency if {@code true}, makes sure to read image metadata and, if the image does not
+         * provide an alpha channel but defines a {@code TransparentColor} metadata node, that the resulting image
+         * has a transparency set to {@code TRANSLUCENT} and uses the correct transparent color.
+         *
+         * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
+         *
+         * @throws IllegalArgumentException if <code>input</code> is <code>null</code>.
+         * @throws IOException if an error occurs during reading.
+         */
         BufferedImage read(URL input, boolean readMetadata, boolean enforceTransparency) throws IOException;
     }
@@ -71,16 +138,40 @@
     }
 
+    /**
+     * Registers API key adapter.
+     * @param apiKeyAdapter API key adapter
+     */
+    public static void registerApiKeyAdapter(ApiKeyAdapter apiKeyAdapter) {
+        FeatureAdapter.apiKeyAdapter = Objects.requireNonNull(apiKeyAdapter);
+    }
+
+    /**
+     * Registers browser adapter.
+     * @param browserAdapter browser adapter
+     */
     public static void registerBrowserAdapter(BrowserAdapter browserAdapter) {
         FeatureAdapter.browserAdapter = Objects.requireNonNull(browserAdapter);
     }
 
+    /**
+     * Registers image adapter.
+     * @param imageAdapter image adapter
+     */
     public static void registerImageAdapter(ImageAdapter imageAdapter) {
         FeatureAdapter.imageAdapter = Objects.requireNonNull(imageAdapter);
     }
 
+    /**
+     * Registers translation adapter.
+     * @param translationAdapter translation adapter
+     */
     public static void registerTranslationAdapter(TranslationAdapter translationAdapter) {
         FeatureAdapter.translationAdapter = Objects.requireNonNull(translationAdapter);
     }
 
+    /**
+     * Registers logging adapter.
+     * @param loggingAdapter logging adapter
+     */
     public static void registerLoggingAdapter(LoggingAdapter loggingAdapter) {
         FeatureAdapter.loggingAdapter = Objects.requireNonNull(loggingAdapter);
@@ -96,20 +187,59 @@
     }
 
+    /**
+     * Retrieves the API key for the given imagery id using the current {@link ApiKeyAdapter}.
+     * @param imageryId imagery id
+     * @return the API key for the given imagery id
+     */
+    public static String retrieveApiKey(String imageryId) {
+        return apiKeyAdapter.retrieveApiKey(imageryId);
+    }
+
+    /**
+     * Opens a link using the current {@link BrowserAdapter}.
+     * @param url link to open
+     */
     public static void openLink(String url) {
         browserAdapter.openLink(url);
     }
 
+    /**
+     * Reads an image using the current {@link ImageAdapter}.
+     * @param url image URL to read
+     * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
+     * @throws IOException if an error occurs during reading.
+     */
     public static BufferedImage readImage(URL url) throws IOException {
         return imageAdapter.read(url, false, false);
     }
 
+    /**
+     * Translates a text using the current {@link TranslationAdapter}.
+     * @param text the text to translate.
+     * Must be a string literal. (No constants or local vars.)
+     * Can be broken over multiple lines.
+     * An apostrophe ' must be quoted by another apostrophe.
+     * @param objects the parameters for the string.
+     * Mark occurrences in {@code text} with <code>{0}</code>, <code>{1}</code>, ...
+     * @return the translated string.
+     */
     public static String tr(String text, Object... objects) {
         return translationAdapter.tr(text, objects);
     }
 
+    /**
+     * Returns a logger for the given name using the current {@link LoggingAdapter}.
+     * @param name logger name
+     * @return logger for the given name
+     */
     public static Logger getLogger(String name) {
         return loggingAdapter.getLogger(name);
     }
 
+    /**
+     * Returns a logger for the given class using the current {@link LoggingAdapter}.
+     * @param klass logger class
+     * @return logger for the given class
+     */
     public static Logger getLogger(Class<?> klass) {
         return loggingAdapter.getLogger(klass.getSimpleName());
@@ -148,4 +278,17 @@
     }
 
+    /**
+     * Default API key support that relies on system property named {@code <imageryId>.api-key}.
+     */
+    public static class DefaultApiKeyAdapter implements ApiKeyAdapter {
+        @Override
+        public String retrieveApiKey(String imageryId) {
+            return System.getProperty(imageryId + ".api-key");
+        }
+    }
+
+    /**
+     * Default browser support that relies on Java Desktop API.
+     */
     public static class DefaultBrowserAdapter implements BrowserAdapter {
         @Override
@@ -165,4 +308,7 @@
     }
 
+    /**
+     * Default image support that relies on Java Image IO API.
+     */
     public static class DefaultImageAdapter implements ImageAdapter {
         @Override
@@ -172,4 +318,7 @@
     }
 
+    /**
+     * Default "translation" support that do not really translates strings, but only takes care of formatting arguments.
+     */
     public static class DefaultTranslationAdapter implements TranslationAdapter {
         @Override
@@ -179,4 +328,7 @@
     }
 
+    /**
+     * Default logging support that relies on Java Logging API.
+     */
     public static class DefaultLoggingAdapter implements LoggingAdapter {
         @Override
Index: applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
===================================================================
--- applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java	(revision 35319)
+++ applications/viewer/jmapviewer/src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java	(revision 35320)
@@ -5,7 +5,9 @@
 import java.util.Map;
 import java.util.Random;
+import java.util.function.BiConsumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
 import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
 
@@ -25,4 +27,5 @@
  * {!y} - substituted with Yahoo Y tile number
  * {-y} - substituted with reversed Y tile number
+ * {apiKey} - substituted with API key retrieved for the imagery id
  * {switch:VAL_A,VAL_B,VAL_C,...} - substituted with one of VAL_A, VAL_B, VAL_C. Usually
  *                                  used to specify many tile servers
@@ -46,9 +49,10 @@
     private static final Pattern PATTERN_SWITCH  = Pattern.compile("\\{switch:([^}]+)\\}");
     private static final Pattern PATTERN_HEADER  = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
+    private static final Pattern PATTERN_API_KEY = Pattern.compile("\\{apiKey\\}");
     private static final Pattern PATTERN_PARAM  = Pattern.compile("\\{((?:\\d+-)?z(?:oom)?(:?[+-]\\d+)?|x|y|!y|-y|switch:([^}]+))\\}");
     // CHECKSTYLE.ON: SingleSpaceSeparator
 
     private static final Pattern[] ALL_PATTERNS = {
-        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y, PATTERN_SWITCH
+        PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y, PATTERN_SWITCH, PATTERN_API_KEY
     };
 
@@ -63,8 +67,18 @@
             headers.put(COOKIE_HEADER, cookies);
         }
-        handleTemplate();
+        handleTemplate(info.getId());
     }
 
-    private void handleTemplate() {
+    private void replacePattern(Pattern p, BiConsumer<Matcher, StringBuffer> replaceAction) {
+        StringBuffer output = new StringBuffer();
+        Matcher m = p.matcher(baseUrl);
+        while (m.find()) {
+            replaceAction.accept(m, output);
+        }
+        m.appendTail(output);
+        baseUrl = output.toString();
+    }
+
+    private void handleTemplate(String imageryId) {
         // Capturing group pattern on switch values
         Matcher m = PATTERN_SWITCH.matcher(baseUrl);
@@ -73,13 +87,15 @@
             randomParts = m.group(1).split(",");
         }
-        StringBuffer output = new StringBuffer();
-        Matcher matcher = PATTERN_HEADER.matcher(baseUrl);
-        while (matcher.find()) {
+        // Capturing group pattern on header values
+        replacePattern(PATTERN_HEADER, (matcher, output) -> {
             headers.put(matcher.group(1), matcher.group(2));
             matcher.appendReplacement(output, "");
-        }
-        matcher.appendTail(output);
-        baseUrl = output.toString();
-        m = PATTERN_ZOOM.matcher(this.baseUrl);
+        });
+        // Capturing group pattern on API key values
+        replacePattern(PATTERN_API_KEY, (matcher, output) ->
+            matcher.appendReplacement(output, FeatureAdapter.retrieveApiKey(imageryId))
+        );
+        // Capturing group pattern on zoom values
+        m = PATTERN_ZOOM.matcher(baseUrl);
         if (m.find()) {
             if (m.group(1) != null) {
@@ -94,5 +110,4 @@
             }
         }
-
     }
 
Index: applications/viewer/jmapviewer/test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java
===================================================================
--- applications/viewer/jmapviewer/test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java	(revision 35319)
+++ applications/viewer/jmapviewer/test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java	(revision 35320)
@@ -1,3 +1,3 @@
-// License: GPL. For details, see LICENSE file.
+// License: GPL. For details, see Readme.txt file.
 package org.openstreetmap.gui.jmapviewer.tilesources;
 
@@ -13,12 +13,10 @@
 import org.junit.Test;
 
-
 /**
- *
  * Tests for TemplaedTMSTileSource
  */
 public class TemplatedTMSTileSourceTest {
 
-    private final static Collection<String> TMS_IMAGERIES = Arrays.asList(new String[]{
+    private static final Collection<String> TMS_IMAGERIES = Arrays.asList(new String[]{
             "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z={zoom}&x={x}&y={-y}",
             /*
@@ -36,13 +34,13 @@
      *  * expected tile url for zoom=3, x=2, y=1
      */
-    @SuppressWarnings("unchecked")
     private Collection<String[]> TEST_DATA = Arrays.asList(new String[][] {
         /*
          * generate with main method below once TMS_IMAGERIES is filled in
          */
-            new String[]{"http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z={zoom}&x={x}&y={-y}", 
-                    "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=1&x=2&y=-2", 
-                    "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=3&x=2&y=6"
-                    }
+        new String[] {
+                "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z={zoom}&x={x}&y={-y}", 
+                "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=1&x=2&y=-2", 
+                "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=3&x=2&y=6"
+                }
     });
 
@@ -59,5 +57,4 @@
     }
 
-
     /**
      * Check template with positive zoom index
@@ -106,4 +103,15 @@
                 "http://localhost/2/1/2"
                 );
+    }
+
+    /**
+     * Test template with switch
+     */
+    @Test
+    public void testGetTileUrl_apiKey() {
+        System.setProperty("id1.api-key", "wololo");
+        TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", "http://localhost/{zoom}/{x}/{y}?token={apiKey}&foo=bar", "id1");
+        TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
+        assertEquals("http://localhost/1/2/3?token=wololo&foo=bar", ts.getTileUrl(1, 2, 3));
     }
 
@@ -176,4 +184,5 @@
         assertEquals(expected312, ts.getTileUrl(3, 1, 2));
     }
+
     /**
      * Tests all entries in TEST_DATA. This test will fail if {switch:...} template is used
@@ -181,5 +190,5 @@
     @Test
     public void testAllUrls() {
-        for(String[] test: TEST_DATA) {
+        for (String[] test: TEST_DATA) {
             TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", test[0], "id1");
             TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
@@ -190,10 +199,10 @@
 
     public static void main(String[] args) {
-        for(String url: TMS_IMAGERIES) {
+        for (String url: TMS_IMAGERIES) {
             TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", url, "id1");
             TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
-            System.out.println(MessageFormat.format("new String[]{\"{0}\", \"{1}\", \"{2}\"},", url, ts.getTileUrl(1, 2, 3), ts.getTileUrl(3, 2, 1)));
+            System.out.println(MessageFormat.format("new String[]{\"{0}\", \"{1}\", \"{2}\"},",
+                    url, ts.getTileUrl(1, 2, 3), ts.getTileUrl(3, 2, 1)));
         }
     }
-
 }
