Index: trunk/src/org/openstreetmap/josm/data/cache/CacheEntryAttributes.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/cache/CacheEntryAttributes.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/data/cache/CacheEntryAttributes.java	(revision 8418)
@@ -2,8 +2,14 @@
 package org.openstreetmap.josm.data.cache;
 
-import java.util.HashMap;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.commons.jcs.engine.ElementAttributes;
+import org.openstreetmap.josm.Main;
 
 /**
@@ -15,5 +21,5 @@
 public class CacheEntryAttributes extends ElementAttributes {
     private static final long serialVersionUID = 1L; //version
-    private final Map<String, String> attrs = new HashMap<String, String>();
+    private final Map<String, String> attrs = new ConcurrentHashMap<String, String>(RESERVED_KEYS.size());
     private static final String NO_TILE_AT_ZOOM = "noTileAtZoom";
     private static final String ETAG = "Etag";
@@ -21,4 +27,12 @@
     private static final String EXPIRATION_TIME = "expirationTime";
     private static final String HTTP_RESPONSE_CODE = "httpResponceCode";
+    // this contains all of the above
+    private static final Set<String> RESERVED_KEYS = new HashSet<>(Arrays.asList(new String[]{
+        NO_TILE_AT_ZOOM,
+        ETAG,
+        LAST_MODIFICATION,
+        EXPIRATION_TIME,
+        HTTP_RESPONSE_CODE
+    }));
 
     /**
@@ -28,5 +42,4 @@
         super();
         attrs.put(NO_TILE_AT_ZOOM, "false");
-        attrs.put(ETAG, null);
         attrs.put(LAST_MODIFICATION, "0");
         attrs.put(EXPIRATION_TIME, "0");
@@ -44,5 +57,7 @@
     }
     public void setEtag(String etag) {
-        attrs.put(ETAG, etag);
+        if(etag != null) {
+            attrs.put(ETAG, etag);
+        }
     }
 
@@ -82,3 +97,26 @@
     }
 
+    /**
+     * Sets the metadata about cache entry. As it stores all data together, with other attributes
+     * in common map, some keys might not be stored.
+     *
+     * @param map metadata to save
+     */
+    public void setMetadata(Map<String, String> map) {
+        for (Entry<String, String> e: map.entrySet()) {
+            if (RESERVED_KEYS.contains(e.getKey())) {
+                Main.info("Metadata key configuration contains key {0} which is reserved for internal use");
+            } else {
+                attrs.put(e.getKey(), e.getValue());
+            }
+        }
+    }
+
+    /**
+     * Returns an unmodifiable Map containing all metadata. Unmodifiable prevents access to metadata within attributes.
+     * @return unmodifiable Map with cache element metadata
+     */
+    public Map<String, String> getMetadata() {
+        return Collections.unmodifiableMap(attrs);
+    }
 }
Index: trunk/src/org/openstreetmap/josm/data/cache/HostLimitQueue.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/cache/HostLimitQueue.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/data/cache/HostLimitQueue.java	(revision 8418)
@@ -30,7 +30,8 @@
 
 public class HostLimitQueue extends LinkedBlockingDeque<Runnable> {
+    private static final long serialVersionUID = 1L;
 
-    private Map<String, Semaphore> hostSemaphores = new ConcurrentHashMap<>();
-    private int hostLimit;
+    private final Map<String, Semaphore> hostSemaphores = new ConcurrentHashMap<>();
+    private final int hostLimit;
 
     /**
@@ -91,5 +92,5 @@
 
     private  Semaphore getSemaphore(JCSCachedTileLoaderJob<?, ?> job) {
-        String host = ((JCSCachedTileLoaderJob<?, ?>)job).getUrl().getHost();
+        String host = job.getUrl().getHost();
         Semaphore limit = hostSemaphores.get(host);
         if (limit == null) {
@@ -112,4 +113,5 @@
                 limit.acquire();
                 jcsJob.setFinishedTask(new Runnable() {
+                    @Override
                     public void run() {
                         releaseSemaphore(jcsJob);
@@ -128,4 +130,5 @@
             if (ret) {
                 job.setFinishedTask(new Runnable() {
+                    @Override
                     public void run() {
                         releaseSemaphore(job);
Index: trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java	(revision 8418)
@@ -198,6 +198,5 @@
     /** icon used in menu */
     private String icon;
-    // when adding a field, also adapt the ImageryInfo(ImageryInfo) constructor
-    private Map<String, String> noTileHeaders;
+    // when adding a field, also adapt the ImageryInfo(ImageryInfo) constructor, equals method, and ImageryPreferenceEntry
 
     /**
@@ -228,4 +227,5 @@
         @pref Map<String, String> noTileHeaders;
         @pref int tileSize = OsmMercator.DEFAUL_TILE_SIZE;
+        @pref Map<String, String> metadataHeaders;
 
         /**
@@ -285,4 +285,8 @@
             }
 
+            if (i.metadataHeaders != null && !i.metadataHeaders.isEmpty()) {
+                metadataHeaders = i.metadataHeaders;
+            }
+
             tileSize = i.getTileSize();
         }
@@ -408,4 +412,5 @@
         }
         setTileSize(e.tileSize);
+        metadataHeaders = e.metadataHeaders;
     }
 
@@ -436,4 +441,6 @@
         this.icon = i.icon;
         this.description = i.description;
+        this.noTileHeaders = i.noTileHeaders;
+        this.metadataHeaders = i.metadataHeaders;
     }
 
@@ -465,62 +472,27 @@
             return false;
         }
-        if (!Objects.equals(this.name, other.name)) {
-            return false;
-        }
-        if (!Objects.equals(this.id, other.id)) {
-            return false;
-        }
-        if (!Objects.equals(this.url, other.url)) {
-            return false;
-        }
-        if (!Objects.equals(this.cookies, other.cookies)) {
-            return false;
-        }
-        if (!Objects.equals(this.eulaAcceptanceRequired, other.eulaAcceptanceRequired)) {
-            return false;
-        }
-        if (this.imageryType != other.imageryType) {
-            return false;
-        }
-        if (this.defaultMaxZoom != other.defaultMaxZoom) {
-            return false;
-        }
-        if (this.defaultMinZoom != other.defaultMinZoom) {
-            return false;
-        }
-        if (!Objects.equals(this.bounds, other.bounds)) {
-            return false;
-        }
-        if (!Objects.equals(this.serverProjections, other.serverProjections)) {
-            return false;
-        }
-        if (!Objects.equals(this.attributionText, other.attributionText)) {
-            return false;
-        }
-        if (!Objects.equals(this.attributionLinkURL, other.attributionLinkURL)) {
-            return false;
-        }
-        if (!Objects.equals(this.attributionImage, other.attributionImage)) {
-            return false;
-        }
-        if (!Objects.equals(this.attributionImageURL, other.attributionImageURL)) {
-            return false;
-        }
-        if (!Objects.equals(this.termsOfUseText, other.termsOfUseText)) {
-            return false;
-        }
-        if (!Objects.equals(this.termsOfUseURL, other.termsOfUseURL)) {
-            return false;
-        }
-        if (!Objects.equals(this.countryCode, other.countryCode)) {
-            return false;
-        }
-        if (!Objects.equals(this.icon, other.icon)) {
-            return false;
-        }
-        if (!Objects.equals(this.description, other.description)) {
-            return false;
-        }
-        return true;
+
+        return
+                Objects.equals(this.name, other.name) &&
+                Objects.equals(this.id, other.id) &&
+                Objects.equals(this.url, other.url) &&
+                Objects.equals(this.cookies, other.cookies) &&
+                Objects.equals(this.eulaAcceptanceRequired, other.eulaAcceptanceRequired) &&
+                Objects.equals(this.imageryType, other.imageryType) &&
+                Objects.equals(this.defaultMaxZoom, other.defaultMaxZoom) &&
+                Objects.equals(this.defaultMinZoom, other.defaultMinZoom) &&
+                Objects.equals(this.bounds, other.bounds) &&
+                Objects.equals(this.serverProjections, other.serverProjections) &&
+                Objects.equals(this.attributionText, other.attributionText) &&
+                Objects.equals(this.attributionLinkURL, other.attributionLinkURL) &&
+                Objects.equals(this.attributionImageURL, other.attributionImageURL) &&
+                Objects.equals(this.attributionImage, other.attributionImage) &&
+                Objects.equals(this.termsOfUseText, other.termsOfUseText) &&
+                Objects.equals(this.termsOfUseURL, other.termsOfUseURL) &&
+                Objects.equals(this.countryCode, other.countryCode) &&
+                Objects.equals(this.icon, other.icon) &&
+                Objects.equals(this.description, other.description) &&
+                Objects.equals(this.noTileHeaders, other.noTileHeaders) &&
+                Objects.equals(this.metadataHeaders, other.metadataHeaders);
     }
 
@@ -1058,3 +1030,7 @@
         return noTileHeaders;
     }
+
+    public void setMetadataHeaders(Map<String, String> metadataHeaders) {
+        this.metadataHeaders = metadataHeaders;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/data/imagery/ImageryLayerInfo.java	(revision 8418)
@@ -150,5 +150,5 @@
         for (ImageryInfo def : defaultLayers) {
             // temporary migration code, so all user preferences will get updated with new settings from JOSM site (can be removed ~Dez. 2015)
-            if (def.getNoTileHeaders() != null || def.getTileSize() > 0) {
+            if (def.getNoTileHeaders() != null || def.getTileSize() > 0 || def.getMetadataHeaders() != null ) {
                 for (ImageryInfo i: layers) {
                     if (isSimilar(def,  i)) {
@@ -158,4 +158,7 @@
                         if (def.getTileSize() > 0) {
                             i.setTileSize(def.getTileSize());
+                        }
+                        if (def.getMetadataHeaders() != null && def.getMetadataHeaders().size() > 0) {
+                            i.setMetadataHeaders(def.getMetadataHeaders());
                         }
                         changed = true;
Index: trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java	(revision 8418)
@@ -10,4 +10,5 @@
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -133,4 +134,7 @@
     @Override
     protected boolean cacheAsEmpty(Map<String, List<String>> headers, int statusCode, byte[] content) {
+        // cacheAsEmpty is called for every successful download, so we can put
+        // metadata handling here
+        attributes.setMetadata(tile.getTileSource().getMetadata(headers));
         if (tile.getTileSource().isNoTileAtZoom(headers, statusCode, content)) {
             attributes.setNoTileAtZoom(true);
@@ -166,4 +170,11 @@
             if(!tile.isLoaded()) { //if someone else already loaded tile, skip all the handling
                 tile.finishLoading(); // whatever happened set that loading has finished
+                // set tile metadata
+                if (this.attributes != null) {
+                    for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
+                        tile.putValue(e.getKey(), e.getValue());
+                    }
+                }
+
                 switch(result){
                 case SUCCESS:
@@ -214,4 +225,11 @@
         if (isObjectLoadable()) {
             try {
+                // set tile metadata
+                if (this.attributes != null) {
+                    for (Entry<String, String> e: this.attributes.getMetadata().entrySet()) {
+                        tile.putValue(e.getKey(), e.getValue());
+                    }
+                }
+
                 if (data != null && data.getImage() != null) {
                     tile.setImage(data.getImage());
Index: trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java	(revision 8418)
@@ -8,4 +8,5 @@
 import java.awt.Graphics;
 import java.awt.Graphics2D;
+import java.awt.GridBagLayout;
 import java.awt.Image;
 import java.awt.Point;
@@ -20,11 +21,14 @@
 import java.io.StringReader;
 import java.net.URL;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Scanner;
 import java.util.concurrent.Callable;
@@ -34,8 +38,12 @@
 import javax.swing.AbstractAction;
 import javax.swing.Action;
+import javax.swing.BorderFactory;
 import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JLabel;
 import javax.swing.JMenuItem;
 import javax.swing.JOptionPane;
+import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
+import javax.swing.JTextField;
 
 import org.openstreetmap.gui.jmapviewer.AttributionSupport;
@@ -80,4 +88,5 @@
 import org.openstreetmap.josm.io.UTFInputStreamReader;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.Utils;
 import org.xml.sax.InputSource;
@@ -533,19 +542,53 @@
         tileOptionMenu.add(new JMenuItem(new AbstractAction(
                 tr("Show Tile Info")) {
+            private String getSizeString(int size) {
+                StringBuilder ret = new StringBuilder();
+                return ret.append(size).append("x").append(size).toString();
+            }
+
+            private JTextField createTextField(String text) {
+                JTextField ret = new JTextField(text);
+                ret.setEditable(false);
+                ret.setBorder(BorderFactory.createEmptyBorder());
+                return ret;
+            }
             @Override
             public void actionPerformed(ActionEvent ae) {
                 if (clickedTile != null) {
                     ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
+                    JPanel panel = new JPanel(new GridBagLayout());
+                    Rectangle displaySize = tileToRect(clickedTile);
+                    String url = "";
+                    try {
+                        url = clickedTile.getUrl();
+                    } catch (IOException e) {
+                        // silence exceptions
+                    }
+
+                    String[][] content = {
+                            {"Tile name", clickedTile.getKey()},
+                            {"Tile url", url},
+                            {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
+                            {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
+                    };
+
+                    for (String[] entry: content) {
+                        panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
+                        panel.add(GBC.glue(5,0), GBC.std());
+                        panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
+                    }
+
+                    for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
+                        panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
+                        panel.add(GBC.glue(5,0), GBC.std());
+                        String value = e.getValue();
+                        if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
+                            value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
+                        }
+                        panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
+
+                    }
                     ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
-                    StringBuilder content = new StringBuilder();
-                    content.append("Tile name: ").append(clickedTile.getKey()).append('\n');
-                    try {
-                        content.append("Tile url: ").append(clickedTile.getUrl()).append('\n');
-                    } catch (IOException e) {
-                    }
-                    content.append("Tile size: ").append(clickedTile.getTileSource().getTileSize()).append('x').append(clickedTile.getTileSource().getTileSize()).append('\n');
-                    Rectangle displaySize = tileToRect(clickedTile);
-                    content.append("Tile display size: ").append(displaySize.width).append('x').append(displaySize.height).append('\n');
-                    ed.setContent(content.toString());
+                    ed.setContent(panel);
                     ed.showDialog();
                 }
Index: trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 8416)
+++ trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java	(revision 8418)
@@ -42,4 +42,5 @@
         SHAPE,
         NO_TILE,
+        METADATA,
         UNKNOWN,            // element is not recognized in the current context
     }
@@ -88,4 +89,5 @@
         private List<String> projections;
         private Map<String, String> noTileHeaders;
+        private Map<String, String> metadataHeaders;
 
         @Override
@@ -117,4 +119,6 @@
                     skipEntry = false;
                     newState = State.ENTRY;
+                    noTileHeaders = new HashMap<>();
+                    metadataHeaders = new HashMap<>();
                 }
                 break;
@@ -157,7 +161,9 @@
                     newState = State.PROJECTIONS;
                 } else if ("no-tile-header".equals(qName)) {
-                    noTileHeaders = new HashMap<>();
                     noTileHeaders.put(atts.getValue("name"), atts.getValue("value"));
                     newState = State.NO_TILE;
+                } else if ("metadata-header".equals(qName)) {
+                    metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
+                    newState = State.METADATA;
                 }
                 break;
@@ -196,5 +202,4 @@
                 skipEntry = true;
             }
-            return;
         }
 
@@ -211,4 +216,9 @@
             case ENTRY:
                 if ("entry".equals(qName)) {
+                    entry.setNoTileHeaders(noTileHeaders);
+                    noTileHeaders = null;
+                    entry.setMetadataHeaders(metadataHeaders);
+                    metadataHeaders = null;
+
                     if (!skipEntry) {
                         entries.add(entry);
@@ -332,6 +342,4 @@
                 break;
             case NO_TILE:
-                entry.setNoTileHeaders(noTileHeaders);
-                noTileHeaders = null;
                 break;
 
