Index: trunk/src/org/openstreetmap/josm/data/imagery/ImageryPatterns.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/ImageryPatterns.java	(revision 17578)
+++ trunk/src/org/openstreetmap/josm/data/imagery/ImageryPatterns.java	(revision 17578)
@@ -0,0 +1,77 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Patterns that can be replaced in imagery URLs.
+ * @since 17578
+ */
+public final class ImageryPatterns {
+
+    // CHECKSTYLE.OFF: SingleSpaceSeparator
+    static final Pattern PATTERN_HEADER = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
+    static final Pattern PATTERN_PROJ   = Pattern.compile("\\{proj\\}");
+    static final Pattern PATTERN_WKID   = Pattern.compile("\\{wkid\\}");
+    static final Pattern PATTERN_BBOX   = Pattern.compile("\\{bbox\\}");
+    static final Pattern PATTERN_W      = Pattern.compile("\\{w\\}");
+    static final Pattern PATTERN_S      = Pattern.compile("\\{s\\}");
+    static final Pattern PATTERN_E      = Pattern.compile("\\{e\\}");
+    static final Pattern PATTERN_N      = Pattern.compile("\\{n\\}");
+    static final Pattern PATTERN_WIDTH  = Pattern.compile("\\{width\\}");
+    static final Pattern PATTERN_HEIGHT = Pattern.compile("\\{height\\}");
+    static final Pattern PATTERN_TIME   = Pattern.compile("\\{time\\}"); // Sentinel-2
+    static final Pattern PATTERN_PARAM  = Pattern.compile("\\{([^}]+)\\}");
+    // CHECKSTYLE.ON: SingleSpaceSeparator
+
+    private static final Pattern[] ALL_WMS_PATTERNS = {
+            PATTERN_HEADER, PATTERN_PROJ, PATTERN_WKID, PATTERN_BBOX,
+            PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N,
+            PATTERN_WIDTH, PATTERN_HEIGHT, PATTERN_TIME
+    };
+
+    private static final Pattern[] ALL_WMTS_PATTERNS = {
+            PATTERN_HEADER
+    };
+
+    private ImageryPatterns() {
+        // Hide public constructor
+    }
+
+    private static void checkUrlPatterns(String url, Pattern[] allPatterns, String errMessage) {
+        Matcher m = PATTERN_PARAM.matcher(Objects.requireNonNull(url, "url"));
+        while (m.find()) {
+            if (Arrays.stream(allPatterns).noneMatch(pattern -> pattern.matcher(m.group()).matches())) {
+                throw new IllegalArgumentException(tr(errMessage, m.group(), url));
+            }
+        }
+    }
+
+    static void checkWmsUrlPatterns(String url) {
+        checkUrlPatterns(url, ALL_WMS_PATTERNS,
+                marktr("{0} is not a valid WMS argument. Please check this server URL:\n{1}"));
+    }
+
+    static void checkWmtsUrlPatterns(String url) {
+        checkUrlPatterns(url, ALL_WMTS_PATTERNS,
+                marktr("{0} is not a valid WMTS argument. Please check this server URL:\n{1}"));
+    }
+
+    static String handleHeaderTemplate(String url, Map<String, String> headers) {
+        StringBuffer output = new StringBuffer();
+        Matcher matcher = PATTERN_HEADER.matcher(url);
+        while (matcher.find()) {
+            headers.put(matcher.group(1), matcher.group(2));
+            matcher.appendReplacement(output, "");
+        }
+        matcher.appendTail(output);
+        return output.toString();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 17577)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java	(revision 17578)
@@ -2,10 +2,9 @@
 package org.openstreetmap.josm.data.imagery;
 
-import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.data.imagery.ImageryPatterns.PATTERN_PARAM;
 
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
-import java.util.Arrays;
 import java.util.Locale;
 import java.util.Map;
@@ -14,5 +13,4 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
@@ -20,5 +18,4 @@
 import org.openstreetmap.josm.data.projection.Projection;
 import org.openstreetmap.josm.gui.layer.WMSLayer;
-import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -30,26 +27,6 @@
  */
 public class TemplatedWMSTileSource extends AbstractWMSTileSource implements TemplatedTileSource {
-    // CHECKSTYLE.OFF: SingleSpaceSeparator
-    private static final Pattern PATTERN_HEADER = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)\\}");
-    private static final Pattern PATTERN_PROJ   = Pattern.compile("\\{proj\\}");
-    private static final Pattern PATTERN_WKID   = Pattern.compile("\\{wkid\\}");
-    private static final Pattern PATTERN_BBOX   = Pattern.compile("\\{bbox\\}");
-    private static final Pattern PATTERN_W      = Pattern.compile("\\{w\\}");
-    private static final Pattern PATTERN_S      = Pattern.compile("\\{s\\}");
-    private static final Pattern PATTERN_E      = Pattern.compile("\\{e\\}");
-    private static final Pattern PATTERN_N      = Pattern.compile("\\{n\\}");
-    private static final Pattern PATTERN_WIDTH  = Pattern.compile("\\{width\\}");
-    private static final Pattern PATTERN_HEIGHT = Pattern.compile("\\{height\\}");
-    private static final Pattern PATTERN_TIME   = Pattern.compile("\\{time\\}"); // Sentinel-2
-    private static final Pattern PATTERN_PARAM  = Pattern.compile("\\{([^}]+)\\}");
-    // CHECKSTYLE.ON: SingleSpaceSeparator
 
     private static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
-
-    private static final Pattern[] ALL_PATTERNS = {
-            PATTERN_HEADER, PATTERN_PROJ, PATTERN_WKID, PATTERN_BBOX,
-            PATTERN_W, PATTERN_S, PATTERN_E, PATTERN_N,
-            PATTERN_WIDTH, PATTERN_HEIGHT, PATTERN_TIME,
-    };
 
     private final Set<String> serverProjections;
@@ -68,5 +45,5 @@
         this.headers.putAll(info.getCustomHttpHeaders());
         this.date = info.getDate();
-        handleTemplate();
+        this.baseUrl = ImageryPatterns.handleHeaderTemplate(baseUrl, headers);
         initProjection();
         // Bounding box coordinates have to be switched for WMS 1.3.0 EPSG:4326.
@@ -170,26 +147,5 @@
      */
     public static void checkUrl(String url) {
-        CheckParameterUtil.ensureParameterNotNull(url, "url");
-        Matcher m = PATTERN_PARAM.matcher(url);
-        while (m.find()) {
-            boolean isSupportedPattern = Arrays.stream(ALL_PATTERNS)
-                    .anyMatch(pattern -> pattern.matcher(m.group()).matches());
-            if (!isSupportedPattern) {
-                throw new IllegalArgumentException(
-                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
-            }
-        }
-    }
-
-    private void handleTemplate() {
-        // Capturing group pattern on switch values
-        StringBuffer output = new StringBuffer();
-        Matcher matcher = PATTERN_HEADER.matcher(this.baseUrl);
-        while (matcher.find()) {
-            headers.put(matcher.group(1), matcher.group(2));
-            matcher.appendReplacement(output, "");
-        }
-        matcher.appendTail(output);
-        this.baseUrl = output.toString();
+        ImageryPatterns.checkWmsUrlPatterns(url);
     }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 17577)
+++ trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java	(revision 17578)
@@ -2,4 +2,6 @@
 package org.openstreetmap.josm.data.imagery;
 
+import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
+import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
 import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_DCP;
 import static org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.QN_OWS_GET;
@@ -37,6 +39,4 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiFunction;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -107,12 +107,6 @@
     // CHECKSTYLE.ON: SingleSpaceSeparator
 
-    private static final String PATTERN_HEADER = "\\{header\\(([^,]+),([^}]+)\\)\\}";
-
     private static final String URL_GET_ENCODING_PARAMS = "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER={layer}&STYLE={style}&"
             + "FORMAT={format}&tileMatrixSet={TileMatrixSet}&tileMatrix={TileMatrix}&tileRow={TileRow}&tileCol={TileCol}";
-
-    private static final String[] ALL_PATTERNS = {
-        PATTERN_HEADER,
-    };
 
     private int cachedTileSize = -1;
@@ -140,8 +134,7 @@
 
     /**
-     *
      * class representing WMTS TileMatrixSet
      * This connects projection and TileMatrix (how the map is divided in tiles)
-     *
+     * @since 13733
      */
     public static class TileMatrixSet {
@@ -208,5 +201,5 @@
     /**
      * Class representing WMTS Layer information
-     *
+     * @since 13733
      */
     public static class Layer {
@@ -298,5 +291,5 @@
     /**
      * Exception thrown when parser doesn't find expected information in GetCapabilities document
-     *
+     * @since 13733
      */
     public static class WMTSGetCapabilitiesException extends Exception {
@@ -368,5 +361,5 @@
         CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
         this.headers.putAll(info.getCustomHttpHeaders());
-        this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
+        this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(ImageryPatterns.handleHeaderTemplate(info.getUrl(), headers));
         WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
         this.layers = capabilities.getLayers();
@@ -444,16 +437,4 @@
     }
 
-    private String handleTemplate(String url) {
-        Pattern pattern = Pattern.compile(PATTERN_HEADER);
-        StringBuffer output = new StringBuffer();
-        Matcher matcher = pattern.matcher(url);
-        while (matcher.find()) {
-            this.headers.put(matcher.group(1), matcher.group(2));
-            matcher.appendReplacement(output, "");
-        }
-        matcher.appendTail(output);
-        return output.toString();
-    }
-
     /**
      * Call remote server and parse response to WMTSCapabilities object
@@ -482,5 +463,5 @@
                 Collection<Layer> layers = null;
                 for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
-                    if (event == XMLStreamReader.START_ELEMENT) {
+                    if (event == START_ELEMENT) {
                         QName qName = reader.getName();
                         if (QN_OWS_OPERATIONS_METADATA.equals(qName)) {
@@ -528,7 +509,7 @@
         Collection<Layer> layers = new ArrayList<>();
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_CONTENTS.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT) {
+            if (event == START_ELEMENT) {
                 QName qName = reader.getName();
                 if (QN_LAYER.equals(qName)) {
@@ -574,7 +555,7 @@
 
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_LAYER.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_LAYER.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT) {
+            if (event == START_ELEMENT) {
                 QName qName = reader.getName();
                 tagStack.push(qName);
@@ -612,5 +593,5 @@
             }
             // need to get event type from reader, as parsing might have change position of reader
-            if (reader.getEventType() == XMLStreamReader.END_ELEMENT) {
+            if (reader.getEventType() == END_ELEMENT) {
                 QName start = tagStack.pop();
                 if (!start.equals(reader.getName())) {
@@ -643,8 +624,7 @@
         Dimension ret = new Dimension();
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
-                        QN_DIMENSION.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_DIMENSION.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT) {
+            if (event == START_ELEMENT) {
                 QName qName = reader.getName();
                 if (QN_OWS_IDENTIFIER.equals(qName)) {
@@ -670,8 +650,7 @@
         String ret = null;
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
-                        QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIX_SET_LINK.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
+            if (event == START_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName())) {
                 ret = reader.getElementText();
             }
@@ -689,7 +668,7 @@
         TileMatrixSetBuilder matrixSet = new TileMatrixSetBuilder();
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIXSET.equals(reader.getName()));
                 event = reader.next()) {
-                    if (event == XMLStreamReader.START_ELEMENT) {
+                    if (event == START_ELEMENT) {
                         QName qName = reader.getName();
                         if (QN_OWS_IDENTIFIER.equals(qName)) {
@@ -717,7 +696,7 @@
         TileMatrix ret = new TileMatrix();
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_TILEMATRIX.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT) {
+            if (event == START_ELEMENT) {
                 QName qName = reader.getName();
                 if (QN_OWS_IDENTIFIER.equals(qName)) {
@@ -772,8 +751,7 @@
         LatLon upperCorner = null;
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
-                        QN_OWS_WGS84_BOUNDING_BOX.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_OWS_WGS84_BOUNDING_BOX.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT) {
+            if (event == START_ELEMENT) {
                 QName qName = reader.getName();
                 if (QN_OWS_LOWER_CORNER.equals(qName)) {
@@ -800,8 +778,7 @@
     private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
         for (int event = reader.getEventType();
-                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
-                        QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
+                reader.hasNext() && !(event == END_ELEMENT && QN_OWS_OPERATIONS_METADATA.equals(reader.getName()));
                 event = reader.next()) {
-            if (event == XMLStreamReader.START_ELEMENT &&
+            if (event == START_ELEMENT &&
                     QN_OWS_OPERATION.equals(reader.getName()) &&
                     "GetTile".equals(reader.getAttributeValue("", "name")) &&
@@ -909,10 +886,10 @@
         }
 
-        url = url.replaceAll("\\{layer\\}", this.currentLayer.identifier)
-                .replaceAll("\\{format\\}", this.currentLayer.format)
-                .replaceAll("\\{TileMatrixSet\\}", this.currentTileMatrixSet.identifier)
-                .replaceAll("\\{TileMatrix\\}", tileMatrix.identifier)
-                .replaceAll("\\{TileRow\\}", Integer.toString(tiley))
-                .replaceAll("\\{TileCol\\}", Integer.toString(tilex))
+        url = url.replace("{layer}", this.currentLayer.identifier)
+                .replace("{format}", this.currentLayer.format)
+                .replace("{TileMatrixSet}", this.currentTileMatrixSet.identifier)
+                .replace("{TileMatrix}", tileMatrix.identifier)
+                .replace("{TileRow}", Integer.toString(tiley))
+                .replace("{TileCol}", Integer.toString(tilex))
                 .replaceAll("(?i)\\{style\\}", this.currentLayer.style);
 
@@ -925,5 +902,5 @@
 
     /**
-     *
+     * Returns TileMatrix that's working on given zoom level
      * @param zoom zoom level
      * @return TileMatrix that's working on this zoom level
@@ -1057,13 +1034,5 @@
      */
     public static void checkUrl(String url) {
-        CheckParameterUtil.ensureParameterNotNull(url, "url");
-        Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
-        while (m.find()) {
-            boolean isSupportedPattern = Arrays.stream(ALL_PATTERNS).anyMatch(pattern -> m.group().matches(pattern));
-            if (!isSupportedPattern) {
-                throw new IllegalArgumentException(
-                        tr("{0} is not a valid WMS argument. Please check this server URL:\n{1}", m.group(), url));
-            }
-        }
+        ImageryPatterns.checkWmtsUrlPatterns(url);
     }
 
