Changeset 13733 in josm


Ignore:
Timestamp:
2018-05-12T14:18:57+02:00 (13 months ago)
Author:
wiktorn
Message:

Imagery definition refactor

Extend imagery definitions by:

  • allowing setting default layers for WMS_ENDPOINT and WMTS
  • allowing setting minimum expires time for tile for this imagery
  • allowing setting custom headers that will be sent for all requests

(get map, get capabilities) for this imagery

Additional changes in code:

  • use TileJobOptions to pass miscellaneous options to loaders
  • refactor WMSImagery to use SAX parser

See: #15981, #7953, #16224, #15940, #16249

Location:
trunk
Files:
9 added
1 deleted
30 edited

Legend:

Unmodified
Added
Removed
  • trunk/data/maps.xsd

    r13536 r13733  
    108108            <!-- TODO: find an easy way to validate projections codes -->
    109109            <xs:element name="code" minOccurs="0" maxOccurs="unbounded" type="xs:string" />
     110        </xs:sequence>
     111    </xs:complexType>
     112
     113    <xs:complexType name="defaultLayers">
     114        <xs:sequence>
     115            <xs:element name="layer" maxOccurs="unbounded">
     116                <xs:complexType>
     117                    <xs:attribute name="name" type="xs:string" use="optional" />
     118                    <xs:attribute name="style" type="xs:string" use="optional" />
     119                    <xs:attribute name="tileMatrixSet" type="xs:string" use="optional" />
     120                </xs:complexType>
     121            </xs:element>
    110122        </xs:sequence>
    111123    </xs:complexType>
     
    713725                            <!-- old unused feature, ignored -->
    714726                            <xs:element name="epsg4326to3857Supported" minOccurs="0" maxOccurs="1" type="xs:boolean" />
     727                            <xs:element name="defaultLayers" type="tns:defaultLayers" minOccurs="0" maxOccurs="1" />
     728                            <xs:element name="custom-http-header" minOccurs="0" maxOccurs="unbounded">
     729                                <xs:complexType>
     730                                    <xs:attribute name="header-name" type="xs:string" />
     731                                    <xs:attribute name="header-value" type="xs:string" />
     732                                </xs:complexType>
     733                            </xs:element>
     734                            <xs:element name="transparent" type="xs:boolean" minOccurs="0" maxOccurs="1"/>
     735                            <xs:element name="format" type="xs:string" minOccurs="0" maxOccurs="1"/>
    715736                            <!-- does imagery server supports "/status" and tile re-rendering using "/dirty". Defaults to false. -->
    716737                            <xs:element name="mod-tile-features" minOccurs="0" maxOccurs="1" type="xs:boolean" />
     738                            <!--  minimum time in seconds for which tile will be considered valid -->
     739                            <xs:element name="minimum-tile-expire" minOccurs="0" maxOccurs="1" type="xs:positiveInteger" />
    717740                        </xs:choice>
    718741                    </xs:sequence>
  • trunk/src/org/openstreetmap/josm/actions/AddImageryLayerAction.java

    r13388 r13733  
    1313import java.util.ArrayList;
    1414import java.util.Collection;
    15 import java.util.HashSet;
    1615import java.util.List;
    17 import java.util.Set;
     16import java.util.stream.Collectors;
    1817
    1918import javax.swing.JComboBox;
     
    2726import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    2827import org.openstreetmap.josm.data.imagery.WMTSTileSource;
     28import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
    2929import org.openstreetmap.josm.gui.ExtendedDialog;
    3030import org.openstreetmap.josm.gui.layer.AlignImageryPanel;
     
    3434import org.openstreetmap.josm.gui.util.GuiHelper;
    3535import org.openstreetmap.josm.io.imagery.WMSImagery;
    36 import org.openstreetmap.josm.io.imagery.WMSImagery.LayerDetails;
    3736import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
    3837import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    9695            case WMS_ENDPOINT:
    9796                // convert to WMS type
    98                 return getWMSLayerInfo(info);
     97                if (info.getDefaultLayers() == null || info.getDefaultLayers().isEmpty()) {
     98                    return getWMSLayerInfo(info);
     99                } else {
     100                    return info;
     101                }
    99102            case WMTS:
    100103                // specify which layer to use
    101                 DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
    102                 if (layerId != null) {
    103                     ImageryInfo copy = new ImageryInfo(info);
    104                     Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
    105                     defaultLayers.add(layerId);
    106                     copy.setDefaultLayers(defaultLayers);
    107                     return copy;
    108                 }
    109                 // layer not selected - refuse to add
    110                 return null;
     104                if (info.getDefaultLayers() == null || info.getDefaultLayers().isEmpty()) {
     105                    DefaultLayer layerId = new WMTSTileSource(info).userSelectLayer();
     106                    if (layerId != null) {
     107                        ImageryInfo copy = new ImageryInfo(info);
     108                        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
     109                        defaultLayers.add(layerId);
     110                        copy.setDefaultLayers(defaultLayers);
     111                        return copy;
     112                    }
     113                    return null;
     114                } else {
     115                    return info;
     116                }
    111117            default:
    112118                return info;
     
    130136            }
    131137            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
     138        } catch (WMTSGetCapabilitiesException e) {
     139            if (!GraphicsEnvironment.isHeadless()) {
     140                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMTS layer list."),
     141                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     142            }
     143            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMTS layer list.", e);
    132144        }
    133145        return null;
     
    166178     */
    167179    protected static ImageryInfo getWMSLayerInfo(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
    168         CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
    169 
    170         final WMSImagery wms = new WMSImagery();
    171         wms.attemptGetCapabilities(info.getUrl());
    172 
    173         final WMSLayerTree tree = new WMSLayerTree();
    174         tree.updateTree(wms);
    175         List<String> wmsFormats = wms.getFormats();
    176         final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[0]));
    177         formats.setSelectedItem(wms.getPreferredFormats());
    178         formats.setToolTipText(tr("Select image format for WMS layer"));
    179 
    180         if (!GraphicsEnvironment.isHeadless() && 1 != new SelectWmsLayersDialog(tree, formats).showDialog().getValue()) {
    181             return null;
    182         }
    183 
    184         final String url = wms.buildGetMapUrl(
    185                 tree.getSelectedLayers(), (String) formats.getSelectedItem());
    186         Set<String> supportedCrs = new HashSet<>();
    187         boolean first = true;
    188         StringBuilder layersString = new StringBuilder();
    189         for (LayerDetails layer: tree.getSelectedLayers()) {
    190             if (first) {
    191                 supportedCrs.addAll(layer.getProjections());
    192                 first = false;
    193             }
    194             layersString.append(layer.name);
    195             layersString.append(", ");
    196             supportedCrs.retainAll(layer.getProjections());
    197         }
    198 
    199         // copy all information from WMS
    200         ImageryInfo ret = new ImageryInfo(info);
    201         // and update according to user choice
    202         ret.setUrl(url);
    203         ret.setImageryType(ImageryType.WMS);
    204         if (layersString.length() > 2) {
    205             ret.setName(ret.getName() + ' ' + layersString.substring(0, layersString.length() - 2));
    206         }
    207         ret.setServerProjections(supportedCrs);
    208         return ret;
     180        try {
     181            CheckParameterUtil.ensureThat(ImageryType.WMS_ENDPOINT.equals(info.getImageryType()), "wms_endpoint imagery type expected");
     182            final WMSImagery wms = new WMSImagery(info.getUrl());
     183
     184            final WMSLayerTree tree = new WMSLayerTree();
     185            tree.updateTree(wms);
     186
     187            Collection<String> wmsFormats = wms.getFormats();
     188            final JComboBox<String> formats = new JComboBox<>(wmsFormats.toArray(new String[wmsFormats.size()]));
     189            formats.setSelectedItem(wms.getPreferredFormat());
     190            formats.setToolTipText(tr("Select image format for WMS layer"));
     191
     192            if (!GraphicsEnvironment.isHeadless()) {
     193                if (1 != new ExtendedDialog(Main.parent, tr("Select WMS layers"), new String[]{tr("Add layers"), tr("Cancel")}) { {
     194                    final JScrollPane scrollPane = new JScrollPane(tree.getLayerTree());
     195                    scrollPane.setPreferredSize(new Dimension(400, 400));
     196                    final JPanel panel = new JPanel(new GridBagLayout());
     197                    panel.add(scrollPane, GBC.eol().fill());
     198                    panel.add(formats, GBC.eol().fill(GBC.HORIZONTAL));
     199                    setContent(panel);
     200                } }.showDialog().getValue()) {
     201                    return null;
     202                }
     203            }
     204
     205            final String url = wms.buildGetMapUrl(
     206                    tree.getSelectedLayers().stream().map(x -> x.getName()).collect(Collectors.toList()),
     207                    (List<String>) null,
     208                    (String) formats.getSelectedItem(),
     209                    true // TODO: ask the user if (s)he wants transparent layer
     210                    );
     211
     212            String selectedLayers = tree.getSelectedLayers().stream()
     213                    .map(x -> x.getName())
     214                    .collect(Collectors.joining(", "));
     215            ImageryInfo ret = new ImageryInfo(info.getName() + selectedLayers,
     216                    url,
     217                    "wms",
     218                    info.getEulaAcceptanceRequired(),
     219                    info.getCookies());
     220
     221            ret.setServerProjections(wms.getServerProjections(tree.getSelectedLayers()));
     222
     223            return ret;
     224        } catch (MalformedURLException ex) {
     225            if (!GraphicsEnvironment.isHeadless()) {
     226                JOptionPane.showMessageDialog(Main.parent, tr("Invalid service URL."),
     227                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     228            }
     229            Logging.log(Logging.LEVEL_ERROR, ex);
     230        } catch (IOException ex) {
     231            if (!GraphicsEnvironment.isHeadless()) {
     232                JOptionPane.showMessageDialog(Main.parent, tr("Could not retrieve WMS layer list."),
     233                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     234            }
     235            Logging.log(Logging.LEVEL_ERROR, ex);
     236        } catch (WMSGetCapabilitiesException ex) {
     237            if (!GraphicsEnvironment.isHeadless()) {
     238                JOptionPane.showMessageDialog(Main.parent, tr("Could not parse WMS layer list."),
     239                        tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
     240            }
     241            Logging.log(Logging.LEVEL_ERROR, "Could not parse WMS layer list. Incoming data:\n"+ex.getIncomingData(), ex);
     242        }
     243        return null;
    209244    }
    210245
  • trunk/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java

    r13358 r13733  
    2121import org.apache.commons.jcs.engine.behavior.ICacheElement;
    2222import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
     23import org.openstreetmap.josm.data.imagery.TileJobOptions;
    2324import org.openstreetmap.josm.data.preferences.IntegerProperty;
    2425import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    9394    private Runnable finishTask;
    9495    private boolean force;
     96    private long minimumExpiryTime;
    9597
    9698    /**
    9799     * @param cache cache instance that we will work on
    98      * @param headers HTTP headers to be sent together with request
    99      * @param readTimeout when connecting to remote resource
    100      * @param connectTimeout when connecting to remote resource
     100     * @param options options of the request
    101101     * @param downloadJobExecutor that will be executing the jobs
    102102     */
    103103    public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache,
    104             int connectTimeout, int readTimeout,
    105             Map<String, String> headers,
     104            TileJobOptions options,
    106105            ThreadPoolExecutor downloadJobExecutor) {
    107106        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
    108107        this.cache = cache;
    109108        this.now = System.currentTimeMillis();
    110         this.connectTimeout = connectTimeout;
    111         this.readTimeout = readTimeout;
    112         this.headers = headers;
     109        this.connectTimeout = options.getConnectionTimeout();
     110        this.readTimeout = options.getReadTimeout();
     111        this.headers = options.getHeaders();
    113112        this.downloadJobExecutor = downloadJobExecutor;
     113        this.minimumExpiryTime = TimeUnit.SECONDS.toMillis(options.getMinimumExpiryTime());
    114114    }
    115115
    116116    /**
    117117     * @param cache cache instance that we will work on
    118      * @param headers HTTP headers to be sent together with request
    119      * @param readTimeout when connecting to remote resource
    120      * @param connectTimeout when connecting to remote resource
     118     * @param options of the request
    121119     */
    122120    public JCSCachedTileLoaderJob(ICacheAccess<K, V> cache,
    123             int connectTimeout, int readTimeout,
    124             Map<String, String> headers) {
    125         this(cache, connectTimeout, readTimeout,
    126                 headers, DEFAULT_DOWNLOAD_JOB_DISPATCHER);
     121            TileJobOptions options) {
     122        this(cache, options, DEFAULT_DOWNLOAD_JOB_DISPATCHER);
    127123    }
    128124
     
    278274            // put a limit to the expire time (some servers send a value
    279275            // that is too large)
    280             expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT);
     276            expires = Math.min(expires, attributes.getCreateTime() + Math.max(EXPIRE_TIME_SERVER_LIMIT, minimumExpiryTime));
    281277            if (now > expires) {
    282278                Logging.debug("JCS - Object {0} has expired -> valid to {1}, now is: {2}",
     
    285281            }
    286282        } else if (attributes.getLastModification() > 0 &&
    287                 now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {
     283                now - attributes.getLastModification() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) {
    288284            // check by file modification date
    289285            Logging.debug("JCS - Object has expired, maximum file age reached {0}", getUrlNoException());
    290286            return false;
    291         } else if (now - attributes.getCreateTime() > DEFAULT_EXPIRE_TIME) {
     287        } else if (now - attributes.getCreateTime() > Math.max(DEFAULT_EXPIRE_TIME, minimumExpiryTime)) {
    292288            Logging.debug("JCS - Object has expired, maximum time since object creation reached {0}", getUrlNoException());
    293289            return false;
     
    330326                // and the server answers with a HTTP 304 = "Not Modified"
    331327                Logging.debug("JCS - If-Modified-Since/ETag test: local version is up to date: {0}", getUrl());
     328                // update cache attributes
     329                attributes = parseHeaders(urlConn);
     330                cache.put(getCacheKey(), cacheData, attributes);
    332331                return true;
    333332            } else if (isObjectLoadable() // we have an object in cache, but we haven't received 304 response code
     
    453452                Logging.trace(e);
    454453            }
    455         }
    456 
    457         ret.setExpirationTime(lng);
     454            if (lng.equals(0L)) {
     455                lng = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;
     456            }
     457        }
     458
     459        ret.setExpirationTime(Math.max(minimumExpiryTime + System.currentTimeMillis(), lng));
    458460        ret.setLastModification(now);
    459461        ret.setEtag(urlConn.getHeaderField("ETag"));
     
    480482        final HttpClient.Response urlConn = getRequest("HEAD", false).connect();
    481483        long lastModified = urlConn.getLastModified();
    482         return (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getHeaderField("ETag"))) ||
     484        boolean ret = (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getHeaderField("ETag"))) ||
    483485                (lastModified != 0 && lastModified <= attributes.getLastModification());
     486        if (ret) {
     487            // update attributes
     488            attributes = parseHeaders(urlConn);
     489            cache.put(getCacheKey(), cacheData, attributes);
     490        }
     491        return ret;
    484492    }
    485493
  • trunk/src/org/openstreetmap/josm/data/imagery/AbstractWMSTileSource.java

    r12669 r13733  
    33
    44import java.awt.Point;
     5import java.text.DecimalFormat;
     6import java.text.DecimalFormatSymbols;
     7import java.text.NumberFormat;
     8import java.util.Locale;
    59
    610import org.openstreetmap.gui.jmapviewer.Projected;
     
    2428public abstract class AbstractWMSTileSource extends TMSTileSource {
    2529
     30    static final NumberFormat LATLON_FORMAT = new DecimalFormat("###0.0000000", new DecimalFormatSymbols(Locale.US));
     31
    2632    private EastNorth anchorPosition;
    2733    private int[] tileXMin;
     
    209215        return this.tileProjection.toCode();
    210216    }
     217
     218    protected String getBbox(int zoom, int tilex, int tiley, boolean switchLatLon) {
     219        EastNorth nw = getTileEastNorth(tilex, tiley, zoom);
     220        EastNorth se = getTileEastNorth(tilex + 1, tiley + 1, zoom);
     221
     222        double w = nw.getX();
     223        double n = nw.getY();
     224
     225        double s = se.getY();
     226        double e = se.getX();
     227
     228        return (
     229                switchLatLon ?
     230                        String.format("%s,%s,%s,%s",
     231                                LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e))
     232                        :
     233                        String.format("%s,%s,%s,%s",
     234                                LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n))
     235
     236                );
     237    }
    211238}
  • trunk/src/org/openstreetmap/josm/data/imagery/CachedTileLoaderFactory.java

    r13647 r13733  
    4444                    TileLoaderListener.class,
    4545                    ICacheAccess.class,
    46                     int.class,
    47                     int.class,
    48                     Map.class);
     46                    TileJobOptions.class
     47                    );
    4948        } catch (NoSuchMethodException | SecurityException e) {
    5049            Logging.log(Logging.LEVEL_WARN, "Unable to initialize cache tile loader factory", e);
     
    6463
    6564    @Override
    66     public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
     65    public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders, long minimumExpiryTime) {
    6766        Map<String, String> headers = new ConcurrentHashMap<>();
    6867        headers.put("User-Agent", Version.getInstance().getFullAgentString());
     
    7271
    7372        return getLoader(listener, cache,
    74                 (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15)),
    75                 (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30)),
    76                 headers);
     73                new TileJobOptions(
     74                        (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15)),
     75                        (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30)),
     76                        headers,
     77                        minimumExpiryTime
     78                        )
     79                );
    7780    }
    7881
    7982    protected TileLoader getLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
    80             int connectTimeout, int readTimeout, Map<String, String> headers) {
     83            TileJobOptions options) {
    8184        try {
    8285            return tileLoaderConstructor.newInstance(
    8386                    listener,
    8487                    cache,
    85                     connectTimeout,
    86                     readTimeout,
    87                     headers);
     88                    options
     89                    );
    8890        } catch (IllegalArgumentException e) {
    8991            Logging.warn(e);
  • trunk/src/org/openstreetmap/josm/data/imagery/DefaultLayer.java

    r11257 r13733  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.imagery;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import javax.json.Json;
     7import javax.json.JsonObject;
     8import javax.json.JsonObjectBuilder;
     9
     10import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    311
    412/**
     
    1220 */
    1321public class DefaultLayer {
    14 
    15     protected String layerName;
     22    private final String layerName;
     23    private final String tileMatrixSet;
     24    private final String style;
    1625
    1726    /**
    1827     * Constructor
    19      * @param layerName that is the DefaultLayer
     28     * @param imageryType for which this layer is defined
     29     * @param layerName as returned by getIdentifier for WMTS and getName for WMS
     30     * @param style of the layer
     31     * @param tileMatrixSet only for WMTS - tileMatrixSet to use
    2032     */
    21     public DefaultLayer(String layerName) {
    22         this.layerName = layerName;
     33    public DefaultLayer(ImageryType imageryType, String layerName, String style, String tileMatrixSet) {
     34        this.layerName = layerName == null ? "" : layerName;
     35        this.style = style == null ? "" : style;
     36        if (!imageryType.equals(ImageryType.WMTS) && !(tileMatrixSet == null || "".equals(tileMatrixSet))) {
     37            throw new IllegalArgumentException(tr("{0} imagery has tileMatrixSet defined to: {1}", imageryType, tileMatrixSet));
     38        }
     39        this.tileMatrixSet = tileMatrixSet == null ? "" : tileMatrixSet;
    2340    }
    2441
     
    3047    }
    3148
     49    /**
     50     * @return default tileMatrixSet. Only usable for WMTS
     51     */
     52    public String getTileMatrixSet() {
     53        return tileMatrixSet;
     54    }
     55
     56    /**
     57     * @return style for this WMS / WMTS layer to use
     58     */
     59    public String getStyle() {
     60        return style;
     61    }
     62
     63    /**
     64     * @return JSON representation of the default layer object
     65     */
     66    public JsonObject toJson() {
     67        JsonObjectBuilder ret = Json.createObjectBuilder();
     68        ret.add("layerName", layerName);
     69        ret.add("style", style);
     70        ret.add("tileMatrixSet", tileMatrixSet);
     71        return ret.build();
     72    }
     73
     74    /**
     75     * Factory method creating DefaultLayer from JSON objects
     76     * @param o serialized DefaultLayer object
     77     * @param type of ImageryType serialized
     78     * @return DefaultLayer instance based on JSON object
     79     */
     80    public static DefaultLayer fromJson(JsonObject o, ImageryType type) {
     81        return new DefaultLayer(type, o.getString("layerName"), o.getString("style"), o.getString("tileMatrixSet"));
     82    }
    3283}
  • trunk/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java

    r13537 r13733  
    55
    66import java.awt.Image;
     7import java.io.StringReader;
    78import java.util.ArrayList;
    89import java.util.Arrays;
     
    1516import java.util.Set;
    1617import java.util.TreeSet;
     18import java.util.concurrent.ConcurrentHashMap;
     19import java.util.concurrent.TimeUnit;
    1720import java.util.regex.Matcher;
    1821import java.util.regex.Pattern;
    1922import java.util.stream.Collectors;
    2023
     24import javax.json.Json;
     25import javax.json.JsonObject;
     26import javax.json.stream.JsonCollectors;
    2127import javax.swing.ImageIcon;
    2228
     
    218224    private boolean isGeoreferenceValid;
    219225    /** which layers should be activated by default on layer addition. **/
    220     private Collection<DefaultLayer> defaultLayers = Collections.emptyList();
    221     // when adding a field, also adapt the ImageryInfo(ImageryInfo)
    222     // and ImageryInfo(ImageryPreferenceEntry) constructor, equals method, and ImageryPreferenceEntry
     226    private List<DefaultLayer> defaultLayers = new ArrayList<>();
     227    /** HTTP headers **/
     228    private Map<String, String> customHttpHeaders = new ConcurrentHashMap<>();
     229    /** Should this map be transparent **/
     230    private boolean transparent = true;
     231    private int minimumTileExpire = (int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get());
     232    /** when adding a field, also adapt the:
     233     * {@link #ImageryPreferenceEntry ImageryPreferenceEntry object}
     234     * {@link #ImageryPreferenceEntry#ImageryPreferenceEntry(ImageryInfo) ImageryPreferenceEntry constructor}
     235     * {@link #ImageryInfo(ImageryPreferenceEntry) ImageryInfo constructor}
     236     * {@link #ImageryInfo(ImageryInfo) ImageryInfo constructor}
     237     * {@link #equalsPref(ImageryPreferenceEntry) equalsPref method}
     238     **/
    223239
    224240    /**
     
    258274        @StructEntry boolean modTileFeatures;
    259275        @StructEntry boolean overlay;
    260         // TODO: disabled until change of layers is implemented
    261         // @StructEntry String default_layers;
     276        @StructEntry String default_layers;
     277        @StructEntry Map<String, String> customHttpHeaders;
     278        @StructEntry boolean transparent;
     279        @StructEntry int minimumTileExpire;
    262280
    263281        /**
     
    308326                }
    309327            }
    310             projections = i.serverProjections.stream().collect(Collectors.joining(","));
     328            if (!i.serverProjections.isEmpty()) {
     329                projections = i.serverProjections.stream().collect(Collectors.joining(","));
     330            }
    311331            if (i.noTileHeaders != null && !i.noTileHeaders.isEmpty()) {
    312332                noTileHeaders = new MultiMap<>(i.noTileHeaders);
     
    325345            valid_georeference = i.isGeoreferenceValid();
    326346            modTileFeatures = i.isModTileFeatures();
    327             // TODO disabled until change of layers is implemented
    328             // default_layers = i.defaultLayers.stream().collect(Collectors.joining(","));
     347            if (!i.defaultLayers.isEmpty()) {
     348                default_layers = i.defaultLayers.stream().map(x -> x.toJson()).collect(JsonCollectors.toJsonArray()).toString();
     349            }
     350            customHttpHeaders = i.customHttpHeaders;
     351            transparent = i.isTransparent();
     352            minimumTileExpire = i.minimumTileExpire;
    329353        }
    330354
     
    468492        isGeoreferenceValid = e.valid_georeference;
    469493        modTileFeatures = e.modTileFeatures;
    470         // TODO disabled until change of layers is implemented
    471         // defaultLayers = Arrays.asList(e.default_layers.split(","));
     494        if (e.default_layers != null) {
     495            defaultLayers = Json.createReader(new StringReader(e.default_layers)).
     496                    readArray().
     497                    stream().
     498                    map(x -> DefaultLayer.fromJson((JsonObject) x, imageryType)).
     499                    collect(Collectors.toList());
     500        }
     501        customHttpHeaders = e.customHttpHeaders;
     502        transparent = e.transparent;
     503        minimumTileExpire = e.minimumTileExpire;
    472504    }
    473505
     
    514546        this.isGeoreferenceValid = i.isGeoreferenceValid;
    515547        this.defaultLayers = i.defaultLayers;
     548        this.customHttpHeaders = i.customHttpHeaders;
     549        this.transparent = i.transparent;
     550        this.minimumTileExpire = i.minimumTileExpire;
    516551    }
    517552
     
    565600                Objects.equals(this.noTileChecksums, other.noTileChecksums) &&
    566601                Objects.equals(this.metadataHeaders, other.metadataHeaders) &&
    567                 Objects.equals(this.defaultLayers, other.defaultLayers);
     602                Objects.equals(this.defaultLayers, other.defaultLayers) &&
     603                Objects.equals(this.customHttpHeaders, other.customHttpHeaders) &&
     604                Objects.equals(this.transparent, other.transparent) &&
     605                Objects.equals(this.minimumTileExpire, other.minimumTileExpire);
    568606        // CHECKSTYLE.ON: BooleanExpressionComplexity
    569607    }
     
    13721410     * @return Collection of the layer names
    13731411     */
    1374     public Collection<DefaultLayer> getDefaultLayers() {
     1412    public List<DefaultLayer> getDefaultLayers() {
    13751413        return defaultLayers;
    13761414    }
     
    13801418     * @param layers set the list of default layers
    13811419     */
    1382     public void setDefaultLayers(Collection<DefaultLayer> layers) {
    1383         if (ImageryType.WMTS.equals(this.imageryType)) {
    1384             CheckParameterUtil.ensureThat(layers == null ||
    1385                     layers.isEmpty() ||
    1386                     layers.iterator().next() instanceof WMTSDefaultLayer, "Incorrect default layer");
    1387         }
     1420    public void setDefaultLayers(List<DefaultLayer> layers) {
    13881421        this.defaultLayers = layers;
    13891422    }
     1423
     1424    /**
     1425     * Returns custom HTTP headers that should be sent with request towards imagery provider
     1426     * @return headers
     1427     */
     1428    public Map<String, String> getCustomHttpHeaders() {
     1429        return customHttpHeaders;
     1430    }
     1431
     1432    /**
     1433     * Sets custom HTTP headers that should be sent with request towards imagery provider
     1434     * @param customHttpHeaders
     1435     */
     1436    public void setCustomHttpHeaders(Map<String, String> customHttpHeaders) {
     1437        this.customHttpHeaders = customHttpHeaders;
     1438    }
     1439
     1440    /**
     1441     * @return should this imagery be transparent
     1442     */
     1443    public boolean isTransparent() {
     1444        return transparent;
     1445    }
     1446
     1447    /**
     1448     *
     1449     * @param transparent set to true if imagery should be transparent
     1450     */
     1451    public void setTransparent(boolean transparent) {
     1452        this.transparent = transparent;
     1453    }
     1454
     1455    /**
     1456     * @return minimum tile expiration in seconds
     1457     */
     1458    public int getMinimumTileExpire() {
     1459        return minimumTileExpire;
     1460    }
     1461
     1462    /**
     1463     * Sets minimum tile expiration in seconds
     1464     * @param minimumTileExpire
     1465     */
     1466    public void setMinimumTileExpire(int minimumTileExpire) {
     1467        this.minimumTileExpire = minimumTileExpire;
     1468
     1469    }
    13901470}
  • trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java

    r13112 r13733  
    22package org.openstreetmap.josm.data.imagery;
    33
    4 import java.util.Map;
    54import java.util.concurrent.ThreadPoolExecutor;
    65import java.util.concurrent.TimeUnit;
     
    2726
    2827    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
    29     protected final int connectTimeout;
    30     protected final int readTimeout;
    31     protected final Map<String, String> headers;
    3228    protected final TileLoaderListener listener;
    3329
     
    5046
    5147    private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
     48    protected final TileJobOptions options;
    5249
    5350    /**
    5451     * Constructor
    5552     * @param listener          called when tile loading has finished
    56      * @param cache              of the cache
    57      * @param connectTimeout    to remote resource
    58      * @param readTimeout       to remote resource
    59      * @param headers           HTTP headers to be sent along with request
     53     * @param cache             of the cache
     54     * @param options           tile job options
    6055     */
    6156    public TMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
    62             int connectTimeout, int readTimeout, Map<String, String> headers) {
     57           TileJobOptions options) {
    6358        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
    6459        this.cache = cache;
    65         this.connectTimeout = connectTimeout;
    66         this.readTimeout = readTimeout;
    67         this.headers = headers;
     60        this.options = options;
    6861        this.listener = listener;
    6962    }
     
    9891    @Override
    9992    public TileJob createTileLoaderJob(Tile tile) {
    100         return new TMSCachedTileLoaderJob(listener, tile, cache,
    101                 connectTimeout, readTimeout, headers, getDownloadExecutor());
     93        return new TMSCachedTileLoaderJob(
     94                listener,
     95                tile,
     96                cache,
     97                options,
     98                getDownloadExecutor());
    10299    }
    103100
  • trunk/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java

    r13730 r13733  
    4444 */
    4545public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener {
    46     private static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
    47     private static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
     46    /** General maximum expires for tiles. Might be overridden by imagery settings */
     47    public static final LongProperty MAXIMUM_EXPIRES = new LongProperty("imagery.generic.maximum_expires", TimeUnit.DAYS.toMillis(30));
     48    /** General minimum expires for tiles. Might be overridden by imagery settings */
     49    public static final LongProperty MINIMUM_EXPIRES = new LongProperty("imagery.generic.minimum_expires", TimeUnit.HOURS.toMillis(1));
    4850    static final Pattern SERVICE_EXCEPTION_PATTERN = Pattern.compile("(?s).+<ServiceException[^>]*>(.+)</ServiceException>.+");
    4951    protected final Tile tile;
    5052    private volatile URL url;
     53    private final TileJobOptions options;
    5154
    5255    // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
     
    5962     * @param tile to be fetched from cache
    6063     * @param cache object
    61      * @param connectTimeout when connecting to remote resource
    62      * @param readTimeout when connecting to remote resource
    63      * @param headers HTTP headers to be sent together with request
     64     * @param options for job (such as http headers, timeouts etc.)
    6465     * @param downloadExecutor that will be executing the jobs
    6566     */
    6667    public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
    6768            ICacheAccess<String, BufferedImageCacheEntry> cache,
    68             int connectTimeout, int readTimeout, Map<String, String> headers,
     69            TileJobOptions options,
    6970            ThreadPoolExecutor downloadExecutor) {
    70         super(cache, connectTimeout, readTimeout, headers, downloadExecutor);
     71        super(cache, options, downloadExecutor);
    7172        this.tile = tile;
     73        this.options = options;
    7274        if (listener != null) {
    7375            String deduplicationKey = getCacheKey();
     
    245247        // keep the expiration time between MINIMUM_EXPIRES and MAXIMUM_EXPIRES, so we will cache the tiles
    246248        // at least for some short period of time, but not too long
    247         if (ret.getExpirationTime() < now + MINIMUM_EXPIRES.get()) {
     249        if (ret.getExpirationTime() < now + Math.max(MINIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
    248250            ret.setExpirationTime(now + MINIMUM_EXPIRES.get());
    249251        }
    250         if (ret.getExpirationTime() > now + MAXIMUM_EXPIRES.get()) {
     252        if (ret.getExpirationTime() > now + Math.max(MAXIMUM_EXPIRES.get(), options.getMinimumExpiryTime())) {
    251253            ret.setExpirationTime(now + MAXIMUM_EXPIRES.get());
    252254        }
  • trunk/src/org/openstreetmap/josm/data/imagery/TemplatedWMSTileSource.java

    r12537 r13733  
    5959        super(info, tileProjection);
    6060        this.serverProjections = new TreeSet<>(info.getServerProjections());
     61        this.headers.putAll(info.getCustomHttpHeaders());
    6162        handleTemplate();
    6263        initProjection();
     
    109110            switchLatLon = Main.getProjection().switchXY();
    110111        }
    111         String bbox;
    112         if (switchLatLon) {
    113             bbox = String.format("%s,%s,%s,%s",
    114                     LATLON_FORMAT.format(s), LATLON_FORMAT.format(w), LATLON_FORMAT.format(n), LATLON_FORMAT.format(e));
    115         } else {
    116             bbox = String.format("%s,%s,%s,%s",
    117                     LATLON_FORMAT.format(w), LATLON_FORMAT.format(s), LATLON_FORMAT.format(e), LATLON_FORMAT.format(n));
    118         }
     112        String bbox = getBbox(zoom, tilex, tiley, switchLatLon);
    119113
    120114        // Using StringBuffer and generic PATTERN_PARAM matcher gives 2x performance improvement over replaceAll
  • trunk/src/org/openstreetmap/josm/data/imagery/TileLoaderFactory.java

    r10651 r13733  
    1919     * @param listener that will be notified, when tile has finished loading
    2020     * @param headers that will be sent with requests to TileSource. <code>null</code> indicates none
     21     * @param minimumExpiryTime minimum expiry time
    2122     * @return TileLoader that uses both of above
    2223     */
    23     TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
     24    TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers, long minimumExpiryTime);
    2425}
  • trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoader.java

    r11453 r13733  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.imagery;
    3 
    4 import java.util.Map;
    53
    64import org.apache.commons.jcs.access.behavior.ICacheAccess;
     
    3533     */
    3634    public WMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
    37             int connectTimeout, int readTimeout, Map<String, String> headers) {
     35            TileJobOptions options) {
    3836
    39         super(listener, cache, connectTimeout, readTimeout, headers);
     37        super(listener, cache, options);
    4038        setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("WMS-downloader-%d", THREAD_LIMIT.get()));
    4139    }
     
    4341    @Override
    4442    public TileJob createTileLoaderJob(Tile tile) {
    45         return new WMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers, getDownloadExecutor());
     43        return new WMSCachedTileLoaderJob(listener, tile, cache, options, getDownloadExecutor());
    4644    }
    4745}
  • trunk/src/org/openstreetmap/josm/data/imagery/WMSCachedTileLoaderJob.java

    r11858 r13733  
    22package org.openstreetmap.josm.data.imagery;
    33
    4 import java.util.Map;
    54import java.util.concurrent.ThreadPoolExecutor;
    65
     
    2322     * @param tile to load
    2423     * @param cache to use (get/put)
    25      * @param connectTimeout to tile source
    26      * @param readTimeout to tile source
    27      * @param headers to be sent with request
     24     * @param options options for tile job
    2825     * @param downloadExecutor that will execute the download task (if needed)
    2926     */
    30     public WMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
    31             ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout,
    32             Map<String, String> headers, ThreadPoolExecutor downloadExecutor) {
    33         super(listener, tile, cache, connectTimeout, readTimeout, headers, downloadExecutor);
     27    public WMSCachedTileLoaderJob(TileLoaderListener listener,
     28            Tile tile,
     29            ICacheAccess<String, BufferedImageCacheEntry> cache,
     30            TileJobOptions options,
     31            ThreadPoolExecutor downloadExecutor) {
     32        super(listener, tile, cache, options, downloadExecutor);
    3433    }
    3534
  • trunk/src/org/openstreetmap/josm/data/imagery/WMTSTileSource.java

    r13542 r13733  
    5151import org.openstreetmap.josm.data.coor.EastNorth;
    5252import org.openstreetmap.josm.data.coor.LatLon;
     53import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper.TransferMode;
     54import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
    5355import org.openstreetmap.josm.data.projection.Projection;
    5456import org.openstreetmap.josm.data.projection.Projections;
     
    126128    }
    127129
    128     private static class TileMatrixSet {
     130    /**
     131     *
     132     * class representing WMTS TileMatrixSet
     133     * This connects projection and TileMatrix (how the map is divided in tiles)
     134     *
     135     */
     136    public static class TileMatrixSet {
    129137
    130138        private final List<TileMatrix> tileMatrix;
     
    154162            return "TileMatrixSet [crs=" + crs + ", identifier=" + identifier + ']';
    155163        }
     164
     165        /**
     166         *
     167         * @return identifier of this TileMatrixSet
     168         */
     169        public String getIdentifier() {
     170            return identifier;
     171        }
    156172    }
    157173
     
    162178    }
    163179
    164     private static class Layer {
     180    /**
     181     * Class representing WMTS Layer information
     182     *
     183     */
     184    public static class Layer {
    165185        private String format;
    166186        private String identifier;
     
    202222                    + tileMatrixSet + ", baseUrl=" + baseUrl + ", style=" + style + ']';
    203223        }
     224
     225        /**
     226         *
     227         * @return identifier of this layer
     228         */
     229        public String getIdentifier() {
     230            return identifier;
     231        }
     232
     233        /**
     234         *
     235         * @return style of this layer
     236         */
     237        public String getStyle() {
     238            return style;
     239        }
     240
     241        /**
     242         *
     243         * @return
     244         */
     245        public TileMatrixSet getTileMatrixSet() {
     246            return tileMatrixSet;
     247        }
     248    }
     249
     250    /**
     251     * Exception thrown when praser doesn't find expected information in GetCapabilities document
     252     *
     253     */
     254    public static class WMTSGetCapabilitiesException extends Exception {
     255
     256        /**
     257         * @param cause description of cause
     258         */
     259        public WMTSGetCapabilitiesException(String cause) {
     260            super(cause);
     261        }
    204262    }
    205263
     
    211269            super(Main.parent, tr("Select WMTS layer"), tr("Add layers"), tr("Cancel"));
    212270            this.layers = groupLayersByNameAndTileMatrixSet(layers);
    213             //getLayersTable(layers, Main.getProjection())
    214             this.list = new JTable(
    215                     new AbstractTableModel() {
    216                         @Override
    217                         public Object getValueAt(int rowIndex, int columnIndex) {
    218                             switch (columnIndex) {
    219                             case 0:
    220                                 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
    221                                         .stream()
    222                                         .map(Layer::getUserTitle)
    223                                         .collect(Collectors.joining(", ")); //this should be only one
    224                             case 1:
    225                                 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
    226                                         .stream()
    227                                         .map(x -> x.tileMatrixSet.crs)
    228                                         .collect(Collectors.joining(", "));
    229                             case 2:
    230                                 return SelectLayerDialog.this.layers.get(rowIndex).getValue()
    231                                         .stream()
    232                                         .map(x -> x.tileMatrixSet.identifier)
    233                                         .collect(Collectors.joining(", ")); //this should be only one
    234                             default:
    235                                 throw new IllegalArgumentException();
    236                             }
    237                         }
    238 
    239                         @Override
    240                         public int getRowCount() {
    241                             return SelectLayerDialog.this.layers.size();
    242                         }
    243 
    244                         @Override
    245                         public int getColumnCount() {
    246                             return 3;
    247                         }
    248 
    249                         @Override
    250                         public String getColumnName(int column) {
    251                             switch (column) {
    252                             case 0: return tr("Layer name");
    253                             case 1: return tr("Projection");
    254                             case 2: return tr("Matrix set identifier");
    255                             default:
    256                                 throw new IllegalArgumentException();
    257                             }
    258                         }
    259                     });
    260             this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    261             this.list.setAutoCreateRowSorter(true);
    262             this.list.setRowSelectionAllowed(true);
    263             this.list.setColumnSelectionAllowed(false);
     271            this.list = getLayerSelectionPanel(this.layers);
    264272            JPanel panel = new JPanel(new GridBagLayout());
    265273            panel.add(new JScrollPane(this.list), GBC.eol().fill());
     
    273281            }
    274282            Layer selectedLayer = layers.get(list.convertRowIndexToModel(index)).getValue().get(0);
    275             return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier);
    276         }
    277 
    278         private static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
    279             Map<String, List<Layer>> layerByName = layers.stream().collect(
    280                     Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
    281             return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
    282         }
     283            return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
     284        }
     285
    283286    }
    284287
     
    292295    private ScaleList nativeScaleList;
    293296
    294     private final WMTSDefaultLayer defaultLayer;
     297    private final DefaultLayer defaultLayer;
    295298
    296299    private Projection tileProjection;
     
    300303     * @param info imagery info
    301304     * @throws IOException if any I/O error occurs
     305     * @throws WMTSGetCapabilitiesException
    302306     * @throws IllegalArgumentException if any other error happens for the given imagery info
    303307     */
    304     public WMTSTileSource(ImageryInfo info) throws IOException {
     308    public WMTSTileSource(ImageryInfo info) throws IOException, WMTSGetCapabilitiesException {
    305309        super(info);
    306310        CheckParameterUtil.ensureThat(info.getDefaultLayers().size() < 2, "At most 1 default layer for WMTS is supported");
    307 
     311        this.headers.putAll(info.getCustomHttpHeaders());
    308312        this.baseUrl = GetCapabilitiesParseHelper.normalizeCapabilitiesUrl(handleTemplate(info.getUrl()));
    309         this.layers = getCapabilities();
     313        WMTSCapabilities capabilities = getCapabilities(baseUrl, headers);
     314        this.layers =  capabilities.getLayers();
     315        this.baseUrl = capabilities.getBaseUrl();
     316        this.transferMode = capabilities.getTransferMode();
    310317        if (info.getDefaultLayers().isEmpty()) {
    311318            Logging.warn(tr("No default layer selected, choosing first layer."));
    312319            if (!layers.isEmpty()) {
    313320                Layer first = layers.iterator().next();
    314                 this.defaultLayer = new WMTSDefaultLayer(first.identifier, first.tileMatrixSet.identifier);
     321                this.defaultLayer = new DefaultLayer(info.getImageryType(), first.identifier, first.style, first.tileMatrixSet.identifier);
    315322            } else {
    316323                this.defaultLayer = null;
    317324            }
    318325        } else {
    319             DefaultLayer defLayer = info.getDefaultLayers().iterator().next();
    320             if (defLayer instanceof WMTSDefaultLayer) {
    321                 this.defaultLayer = (WMTSDefaultLayer) defLayer;
    322             } else {
    323                 this.defaultLayer = null;
    324             }
     326            this.defaultLayer = info.getDefaultLayers().iterator().next();
    325327        }
    326328        if (this.layers.isEmpty())
     
    343345                // only one tile matrix set with matching projection - no point in asking
    344346                Layer selectedLayer = ls.get(0);
    345                 return new WMTSDefaultLayer(selectedLayer.identifier, selectedLayer.tileMatrixSet.identifier);
     347                return new DefaultLayer(ImageryType.WMTS, selectedLayer.identifier, selectedLayer.style, selectedLayer.tileMatrixSet.identifier);
    346348            }
    347349        }
     
    366368    }
    367369
    368     /**
     370
     371    /**
     372     * @param url of the getCapabilities document
     373     * @param headers HTTP headers to set when calling getCapabilities url
    369374     * @return capabilities
    370375     * @throws IOException in case of any I/O error
     376     * @throws WMTSGetCapabilitiesException
    371377     * @throws IllegalArgumentException in case of any other error
    372378     */
    373     private Collection<Layer> getCapabilities() throws IOException {
    374         try (CachedFile cf = new CachedFile(baseUrl); InputStream in = cf.setHttpHeaders(headers).
     379    public static WMTSCapabilities getCapabilities(String url, Map<String, String> headers) throws IOException, WMTSGetCapabilitiesException {
     380        try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
    375381                setMaxAge(Config.getPref().getLong("wmts.capabilities.cache.max_age", 7 * CachedFile.DAYS)).
    376382                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
     
    379385            if (data.length == 0) {
    380386                cf.clear();
    381                 throw new IllegalArgumentException("Could not read data from: " + baseUrl);
     387                throw new IllegalArgumentException("Could not read data from: " + url);
    382388            }
    383389
    384390            try {
    385391                XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(new ByteArrayInputStream(data));
    386                 Collection<Layer> ret = new ArrayList<>();
     392                WMTSCapabilities ret = null;
     393                Collection<Layer> layers = null;
    387394                for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
    388395                    if (event == XMLStreamReader.START_ELEMENT) {
    389396                        if (GetCapabilitiesParseHelper.QN_OWS_OPERATIONS_METADATA.equals(reader.getName())) {
    390                             parseOperationMetadata(reader);
     397                            ret = parseOperationMetadata(reader);
    391398                        }
    392399
    393400                        if (QN_CONTENTS.equals(reader.getName())) {
    394                             ret = parseContents(reader);
     401                            layers = parseContents(reader);
    395402                        }
    396403                    }
    397404                }
     405                if (ret == null) {
     406                    /*
     407                     *  see #12168 - create dummy operation metadata - not all WMTS services provide this information
     408                     *
     409                     *  WMTS Standard:
     410                     *  > Resource oriented architecture style HTTP encodings SHALL not be described in the OperationsMetadata section.
     411                     *
     412                     *  And OperationMetada is not mandatory element. So REST mode is justifiable
     413                     */
     414                    ret = new WMTSCapabilities(url, TransferMode.REST);
     415                }
     416                if (layers == null) {
     417                    throw new WMTSGetCapabilitiesException(tr("WMTS Capabilties document did not contain layers in url:  {0}", url));
     418                }
     419                ret.addLayers(layers);
    398420                return ret;
    399421            } catch (XMLStreamException e) {
     
    456478        supportedMimeTypes.add("image/jpgpng");         // used by ESRI
    457479        supportedMimeTypes.add("image/png8");           // used by geoserver
     480        if (supportedMimeTypes.contains("image/jpeg")) {
     481            supportedMimeTypes.add("image/jpg"); // sometimes mispelled by Arcgis
     482        }
    458483        Collection<String> unsupportedFormats = new ArrayList<>();
    459484
     
    639664    /**
    640665     * Parses OperationMetadata section. Returns when reader is on OperationsMetadata closing tag.
    641      * Sets this.baseUrl and this.transferMode
     666     * return WMTSCapabilities with baseUrl and transferMode
    642667     *
    643668     * @param reader StAX reader instance
     669     * @return WMTSCapabilities with baseUrl and transferMode set
    644670     * @throws XMLStreamException See {@link XMLStreamReader}
    645671     */
    646     private void parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
     672    private static WMTSCapabilities parseOperationMetadata(XMLStreamReader reader) throws XMLStreamException {
    647673        for (int event = reader.getEventType();
    648674                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT &&
     
    657683                            GetCapabilitiesParseHelper.QN_OWS_GET
    658684                    )) {
    659                 this.baseUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
    660                 this.transferMode = GetCapabilitiesParseHelper.getTransferMode(reader);
    661             }
    662         }
     685                return new WMTSCapabilities(
     686                        reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href"),
     687                        GetCapabilitiesParseHelper.getTransferMode(reader)
     688                        );
     689            }
     690        }
     691        return null;
    663692    }
    664693
     
    671700            return;
    672701        List<Layer> matchingLayers = layers.stream().filter(
    673                 l -> l.identifier.equals(defaultLayer.layerName) && l.tileMatrixSet.crs.equals(proj.toCode()))
     702                l -> l.identifier.equals(defaultLayer.getLayerName()) && l.tileMatrixSet.crs.equals(proj.toCode()))
    674703                .collect(Collectors.toList());
    675704        if (matchingLayers.size() > 1) {
     
    686715                this.tileProjection = null;
    687716                for (Layer layer : layers) {
    688                     if (!layer.identifier.equals(defaultLayer.layerName)) {
     717                    if (!layer.identifier.equals(defaultLayer.getLayerName())) {
    689718                        continue;
    690719                    }
     
    922951    }
    923952
     953    public static JTable getLayerSelectionPanel(List<Entry<String, List<Layer>>> layers) {
     954        JTable list = new JTable(
     955                new AbstractTableModel() {
     956                    @Override
     957                    public Object getValueAt(int rowIndex, int columnIndex) {
     958                        switch (columnIndex) {
     959                        case 0:
     960                            return layers.get(rowIndex).getValue()
     961                                    .stream()
     962                                    .map(Layer::getUserTitle)
     963                                    .collect(Collectors.joining(", ")); //this should be only one
     964                        case 1:
     965                            return layers.get(rowIndex).getValue()
     966                                    .stream()
     967                                    .map(x -> x.tileMatrixSet.crs)
     968                                    .collect(Collectors.joining(", "));
     969                        case 2:
     970                            return layers.get(rowIndex).getValue()
     971                                    .stream()
     972                                    .map(x -> x.tileMatrixSet.identifier)
     973                                    .collect(Collectors.joining(", ")); //this should be only one
     974                        default:
     975                            throw new IllegalArgumentException();
     976                        }
     977                    }
     978
     979                    @Override
     980                    public int getRowCount() {
     981                        return layers.size();
     982                    }
     983
     984                    @Override
     985                    public int getColumnCount() {
     986                        return 3;
     987                    }
     988
     989                    @Override
     990                    public String getColumnName(int column) {
     991                        switch (column) {
     992                        case 0: return tr("Layer name");
     993                        case 1: return tr("Projection");
     994                        case 2: return tr("Matrix set identifier");
     995                        default:
     996                            throw new IllegalArgumentException();
     997                        }
     998                    }
     999                });
     1000        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
     1001        list.setAutoCreateRowSorter(true);
     1002        list.setRowSelectionAllowed(true);
     1003        list.setColumnSelectionAllowed(false);
     1004        return list;
     1005    }
     1006
     1007    public static List<Entry<String, List<Layer>>> groupLayersByNameAndTileMatrixSet(Collection<Layer> layers) {
     1008        Map<String, List<Layer>> layerByName = layers.stream().collect(
     1009                Collectors.groupingBy(x -> x.identifier + '\u001c' + x.tileMatrixSet.identifier));
     1010        return layerByName.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
     1011    }
     1012
     1013
    9241014    /**
    9251015     * @return set of projection codes that this TileSource supports
  • trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java

    r13434 r13733  
    2121import java.util.Set;
    2222import java.util.concurrent.CopyOnWriteArrayList;
     23import java.util.concurrent.TimeUnit;
    2324
    2425import javax.swing.ButtonModel;
     
    158159        TileLoaderFactory cachedLoaderFactory = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class);
    159160        if (cachedLoaderFactory != null) {
    160             cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers);
     161            cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers, TimeUnit.HOURS.toSeconds(1));
    161162        } else {
    162163            cachedLoader = null;
  • trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java

    r13674 r13733  
    203203    // prepared to be moved to the painter
    204204    protected TileCoordinateConverter coordinateConverter;
     205    private final long minimumTileExpire;
    205206
    206207    /**
     
    214215        getFilterSettings().addFilterChangeListener(this);
    215216        getDisplaySettings().addSettingsChangeListener(this);
     217        this.minimumTileExpire = info.getMinimumTileExpire();
    216218    }
    217219
     
    274276        Map<String, String> headers = getHeaders(tileSource);
    275277
    276         tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
     278        tileLoader = getTileLoaderFactory().makeTileLoader(this, headers, minimumTileExpire);
    277279
    278280        try {
     
    17591761        public PrecacheTask(ProgressMonitor progressMonitor) {
    17601762            this.progressMonitor = progressMonitor;
    1761             this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
     1763            this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource), minimumTileExpire);
    17621764            if (this.tileLoader instanceof TMSCachedTileLoader) {
    17631765                ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
  • trunk/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java

    r13244 r13733  
    196196        switch(info.getImageryType()) {
    197197        case WMS:
     198        case WMS_ENDPOINT:
    198199            return new WMSLayer(info);
    199200        case WMTS:
  • trunk/src/org/openstreetmap/josm/gui/layer/WMSLayer.java

    r13674 r13733  
    2525import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
    2626import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
     27import org.openstreetmap.josm.data.imagery.WMSEndpointTileSource;
    2728import org.openstreetmap.josm.data.preferences.BooleanProperty;
    2829import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    5758    private static final String CACHE_REGION_NAME = "WMS";
    5859
    59     private final List<String> serverProjections;
     60    private List<String> serverProjections;
    6061
    6162    /**
     
    6566    public WMSLayer(ImageryInfo info) {
    6667        super(info);
    67         CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS, "ImageryType is WMS");
     68        CheckParameterUtil.ensureThat(info.getImageryType() == ImageryType.WMS || info.getImageryType() == ImageryType.WMS_ENDPOINT, "ImageryType is WMS");
    6869        CheckParameterUtil.ensureParameterNotNull(info.getUrl(), "info.url");
    69         TemplatedWMSTileSource.checkUrl(info.getUrl());
     70        if (info.getImageryType() == ImageryType.WMS) {
     71            TemplatedWMSTileSource.checkUrl(info.getUrl());
     72
     73        }
    7074        this.serverProjections = new ArrayList<>(info.getServerProjections());
    7175    }
     
    8993    @Override
    9094    protected AbstractWMSTileSource getTileSource() {
    91         AbstractWMSTileSource tileSource = new TemplatedWMSTileSource(
    92                 info, chooseProjection(Main.getProjection()));
     95        AbstractWMSTileSource tileSource;
     96        if (info.getImageryType() == ImageryType.WMS) {
     97            tileSource = new TemplatedWMSTileSource(info, chooseProjection(Main.getProjection()));
     98        } else {
     99            /*
     100             *  Chicken-and-egg problem. We want to create tile source, but supported projections we can get only
     101             *  from this tile source. So create tilesource first with dummy Main.getProjection(), and then update
     102             *  once we update server projections.
     103             *
     104             *  Thus:
     105             *  * it is not required to provide projections for wms_endpoint imagery types
     106             *  * we always use current definitions returned by server
     107             */
     108            WMSEndpointTileSource endpointTileSource = new WMSEndpointTileSource(info, Main.getProjection());
     109            this.serverProjections = endpointTileSource.getServerProjections();
     110            endpointTileSource.setTileProjection(chooseProjection(Main.getProjection()));
     111            tileSource = endpointTileSource;
     112        }
    93113        info.setAttribution(tileSource);
    94114        return tileSource;
  • trunk/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java

    r12630 r13733  
    1313import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader;
    1414import org.openstreetmap.josm.data.imagery.WMTSTileSource;
     15import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
    1516import org.openstreetmap.josm.data.projection.Projection;
    1617import org.openstreetmap.josm.gui.MainApplication;
     
    6465            }
    6566            return null;
    66         } catch (IOException e) {
     67        } catch (IOException | WMTSGetCapabilitiesException e) {
    6768            Logging.warn(e);
    6869            throw new IllegalArgumentException(e);
  • trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java

    r13536 r13733  
    77import java.util.ArrayList;
    88import java.util.Arrays;
    9 import java.util.HashMap;
    109import java.util.List;
    1110import java.util.Map;
    1211import java.util.Objects;
    1312import java.util.Stack;
     13import java.util.concurrent.ConcurrentHashMap;
    1414
    1515import javax.xml.parsers.ParserConfigurationException;
    1616
     17import org.openstreetmap.josm.data.imagery.DefaultLayer;
    1718import org.openstreetmap.josm.data.imagery.ImageryInfo;
    1819import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
     
    5758        NO_TILESUM,
    5859        METADATA,
    59         UNKNOWN,            // element is not recognized in the current context
     60        DEFAULT_LAYERS,
     61        CUSTOM_HTTP_HEADERS,
     62        NOOP,
     63        UNKNOWN,             // element is not recognized in the current context
    6064    }
    6165
     
    132136        private MultiMap<String, String> noTileChecksums;
    133137        private Map<String, String> metadataHeaders;
     138        private List<DefaultLayer> defaultLayers;
     139        private Map<String, String> customHttpHeaders;
    134140
    135141        @Override
     
    145151            noTileHeaders = null;
    146152            noTileChecksums = null;
     153            customHttpHeaders = null;
    147154        }
    148155
     
    164171                    noTileHeaders = new MultiMap<>();
    165172                    noTileChecksums = new MultiMap<>();
    166                     metadataHeaders = new HashMap<>();
     173                    metadataHeaders = new ConcurrentHashMap<>();
     174                    defaultLayers = new ArrayList<>();
     175                    customHttpHeaders = new ConcurrentHashMap<>();
    167176                    String best = atts.getValue("eli-best");
    168177                    if (TRUE.equals(best)) {
     
    215224                        TILE_SIZE,
    216225                        "valid-georeference",
    217                         "mod-tile-features"
     226                        "mod-tile-features",
     227                        "transparent",
     228                        "minimum-tile-expire"
    218229                ).contains(qName)) {
    219230                    newState = State.ENTRY_ATTRIBUTE;
     
    247258                    metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
    248259                    newState = State.METADATA;
     260                } else if ("defaultLayers".equals(qName)) {
     261                    newState = State.DEFAULT_LAYERS;
     262                } else if ("custom-http-header".equals(qName)) {
     263                   customHttpHeaders.put(atts.getValue("header-name"), atts.getValue("header-value"));
     264                   newState = State.CUSTOM_HTTP_HEADERS;
    249265                }
    250266                break;
     
    269285                if ("code".equals(qName)) {
    270286                    newState = State.CODE;
     287                }
     288                break;
     289            case DEFAULT_LAYERS:
     290                if ("layer".equals(qName)) {
     291                    newState = State.NOOP;
     292                    defaultLayers.add(new DefaultLayer(entry.getImageryType(), atts.getValue("name"),atts.getValue("style"), atts.getValue("tileMatrixSet")));
    271293                }
    272294                break;
     
    306328                    entry.setMetadataHeaders(metadataHeaders);
    307329                    metadataHeaders = null;
     330                    entry.setDefaultLayers(defaultLayers);
     331                    defaultLayers = null;
     332                    entry.setCustomHttpHeaders(customHttpHeaders);
     333                    customHttpHeaders = null;
    308334
    309335                    if (!skipEntry) {
     
    323349                    switch(qName) {
    324350                    case "type":
     351                        ImageryType.values();
    325352                        boolean found = false;
    326353                        for (ImageryType type : ImageryType.values()) {
     
    487514                case "mod-tile-features":
    488515                    entry.setModTileFeatures(Boolean.parseBoolean(accumulator.toString()));
     516                    break;
     517                case "transparent":
     518                    entry.setTransparent(Boolean.parseBoolean(accumulator.toString()));
     519                    break;
     520                case "minimum-tile-expire":
     521                    entry.setMinimumTileExpire(Integer.valueOf(accumulator.toString()));
    489522                    break;
    490523                default: // Do nothing
  • trunk/src/org/openstreetmap/josm/io/imagery/WMSImagery.java

    r13699 r13733  
    22package org.openstreetmap.josm.io.imagery;
    33
    4 import java.awt.HeadlessException;
     4import static java.nio.charset.StandardCharsets.UTF_8;
     5import static org.openstreetmap.josm.tools.I18n.tr;
     6
     7import java.io.File;
    58import java.io.IOException;
    69import java.io.InputStream;
    7 import java.io.StringReader;
    8 import java.io.StringWriter;
    910import java.net.MalformedURLException;
    1011import java.net.URL;
     
    1314import java.util.Collections;
    1415import java.util.HashSet;
    15 import java.util.Iterator;
    1616import java.util.List;
    17 import java.util.Locale;
    18 import java.util.NoSuchElementException;
     17import java.util.Map;
    1918import java.util.Set;
    20 import java.util.regex.Matcher;
     19import java.util.concurrent.ConcurrentHashMap;
     20import java.util.function.Function;
    2121import java.util.regex.Pattern;
    2222import java.util.stream.Collectors;
    23 import java.util.stream.Stream;
    24 import java.util.stream.StreamSupport;
    2523
    2624import javax.imageio.ImageIO;
    27 import javax.xml.parsers.DocumentBuilder;
    28 import javax.xml.parsers.ParserConfigurationException;
    29 import javax.xml.transform.TransformerException;
    30 import javax.xml.transform.TransformerFactory;
    31 import javax.xml.transform.TransformerFactoryConfigurationError;
    32 import javax.xml.transform.dom.DOMSource;
    33 import javax.xml.transform.stream.StreamResult;
     25import javax.xml.namespace.QName;
     26import javax.xml.stream.XMLStreamException;
     27import javax.xml.stream.XMLStreamReader;
    3428
    3529import org.openstreetmap.josm.data.Bounds;
     30import org.openstreetmap.josm.data.coor.EastNorth;
     31import org.openstreetmap.josm.data.imagery.DefaultLayer;
    3632import org.openstreetmap.josm.data.imagery.GetCapabilitiesParseHelper;
    3733import org.openstreetmap.josm.data.imagery.ImageryInfo;
     34import org.openstreetmap.josm.data.imagery.LayerDetails;
     35import org.openstreetmap.josm.data.projection.Projection;
    3836import org.openstreetmap.josm.data.projection.Projections;
    39 import org.openstreetmap.josm.tools.HttpClient;
    40 import org.openstreetmap.josm.tools.HttpClient.Response;
     37import org.openstreetmap.josm.io.CachedFile;
    4138import org.openstreetmap.josm.tools.Logging;
    4239import org.openstreetmap.josm.tools.Utils;
    43 import org.w3c.dom.Document;
    44 import org.w3c.dom.Element;
    45 import org.w3c.dom.Node;
    46 import org.w3c.dom.NodeList;
    47 import org.xml.sax.InputSource;
    48 import org.xml.sax.SAXException;
    4940
    5041/**
     
    5344public class WMSImagery {
    5445
    55     private static final class ChildIterator implements Iterator<Element> {
    56         private Element child;
    57 
    58         ChildIterator(Element parent) {
    59             child = advanceToElement(parent.getFirstChild());
    60         }
    61 
    62         private static Element advanceToElement(Node firstChild) {
    63             Node node = firstChild;
    64             while (node != null && !(node instanceof Element)) {
    65                 node = node.getNextSibling();
    66             }
    67             return (Element) node;
    68         }
    69 
    70         @Override
    71         public boolean hasNext() {
    72             return child != null;
    73         }
    74 
    75         @Override
    76         public Element next() {
    77             if (!hasNext()) {
    78                 throw new NoSuchElementException("No next sibling.");
    79             }
    80             Element next = child;
    81             child = advanceToElement(child.getNextSibling());
    82             return next;
    83         }
    84     }
     46
     47    private static final String CAPABILITIES_QUERY_STRING = "SERVICE=WMS&REQUEST=GetCapabilities";
     48
     49    /**
     50     * WMS namespace address
     51     */
     52    public static final String WMS_NS_URL = "http://www.opengis.net/wms";
     53
     54    // CHECKSTYLE.OFF: SingleSpaceSeparator
     55    // WMS 1.0 - 1.3.0
     56    private static final QName CAPABILITITES_ROOT_130 = new QName("WMS_Capabilities", WMS_NS_URL);
     57    private static final QName QN_ABSTRACT            = new QName(WMS_NS_URL, "Abstract");
     58    private static final QName QN_CAPABILITY          = new QName(WMS_NS_URL, "Capability");
     59    private static final QName QN_CRS                 = new QName(WMS_NS_URL, "CRS");
     60    private static final QName QN_DCPTYPE             = new QName(WMS_NS_URL, "DCPType");
     61    private static final QName QN_FORMAT              = new QName(WMS_NS_URL, "Format");
     62    private static final QName QN_GET                 = new QName(WMS_NS_URL, "Get");
     63    private static final QName QN_GETMAP              = new QName(WMS_NS_URL, "GetMap");
     64    private static final QName QN_HTTP                = new QName(WMS_NS_URL, "HTTP");
     65    private static final QName QN_LAYER               = new QName(WMS_NS_URL, "Layer");
     66    private static final QName QN_NAME                = new QName(WMS_NS_URL, "Name");
     67    private static final QName QN_REQUEST             = new QName(WMS_NS_URL, "Request");
     68    private static final QName QN_SERVICE             = new QName(WMS_NS_URL, "Service");
     69    private static final QName QN_STYLE               = new QName(WMS_NS_URL, "Style");
     70    private static final QName QN_TITLE               = new QName(WMS_NS_URL, "Title");
     71    private static final QName QN_BOUNDINGBOX         = new QName(WMS_NS_URL, "BoundingBox");
     72    private static final QName QN_EX_GEOGRAPHIC_BBOX  = new QName(WMS_NS_URL, "EX_GeographicBoundingBox");
     73    private static final QName QN_WESTBOUNDLONGITUDE  = new QName(WMS_NS_URL, "westBoundLongitude");
     74    private static final QName QN_EASTBOUNDLONGITUDE  = new QName(WMS_NS_URL, "eastBoundLongitude");
     75    private static final QName QN_SOUTHBOUNDLATITUDE  = new QName(WMS_NS_URL, "southBoundLatitude");
     76    private static final QName QN_NORTHBOUNDLATITUDE  = new QName(WMS_NS_URL, "northBoundLatitude");
     77    private static final QName QN_ONLINE_RESOURCE     = new QName(WMS_NS_URL, "OnlineResource");
     78
     79    // WMS 1.1 - 1.1.1
     80    private static final QName CAPABILITIES_ROOT_111 = new QName("WMT_MS_Capabilities");
     81    private static final QName QN_SRS                = new QName("SRS");
     82    private static final QName QN_LATLONBOUNDINGBOX  = new QName("LatLonBoundingBox");
     83
     84    // CHECKSTYLE.ON: SingleSpaceSeparator
    8585
    8686    /**
     
    120120    }
    121121
    122     private List<LayerDetails> layers;
    123     private URL serviceUrl;
    124     private List<String> formats;
    125     private String version = "1.1.1";
    126 
    127     /**
    128      * Returns the list of layers.
    129      * @return the list of layers
     122    private Map<String, String> headers = new ConcurrentHashMap<>();
     123    private String version = "1.1.1"; // default version
     124    private String getMapUrl;
     125    private URL capabilitiesUrl;
     126    private List<String> formats = new ArrayList<>();
     127    private List<LayerDetails> layers = new ArrayList<>();
     128
     129    private String title;
     130
     131    /**
     132     * Make getCapabilities request towards given URL
     133     * @param url service url
     134     * @throws IOException
     135     * @throws WMSGetCapabilitiesException
     136     */
     137    public WMSImagery(String url) throws IOException, WMSGetCapabilitiesException {
     138        this(url, null);
     139    }
     140
     141    /**
     142     * Make getCapabilities request towards given URL using headers
     143     * @param url service url
     144     * @param headers HTTP headers to be sent with request
     145     * @throws IOException
     146     * @throws WMSGetCapabilitiesException
     147     */
     148    public WMSImagery(String url, Map<String, String> headers) throws IOException, WMSGetCapabilitiesException {
     149        if (headers != null) {
     150            this.headers.putAll(headers);
     151        }
     152
     153        IOException savedExc = null;
     154        String workingAddress = null;
     155        url_search:
     156        for (String z: new String[]{
     157                normalizeUrl(url),
     158                url,
     159                url + CAPABILITIES_QUERY_STRING,
     160        }) {
     161            for (String ver: new String[]{"", "&VERSION=1.3.0", "&VERSION=1.1.1"}) {
     162                try {
     163                    attemptGetCapabilities(z + ver);
     164                    workingAddress = z;
     165                    calculateChildren();
     166                    // clear saved exception - we've got something working
     167                    savedExc = null;
     168                    break url_search;
     169                } catch (IOException e) {
     170                    savedExc = e;
     171                    Logging.warn(e);
     172                }
     173            }
     174        }
     175
     176        if (workingAddress != null) {
     177            try {
     178                capabilitiesUrl = new URL(workingAddress);
     179            } catch (MalformedURLException e) {
     180                if (savedExc != null) {
     181                    savedExc = e;
     182                }
     183                try {
     184                    capabilitiesUrl = new File(workingAddress).toURI().toURL();
     185                } catch (MalformedURLException e1) {
     186                    // do nothing, raise original exception
     187                }
     188            }
     189        }
     190
     191        if (savedExc != null) {
     192            throw savedExc;
     193        }
     194    }
     195
     196    private void calculateChildren() {
     197        Map<LayerDetails, List<LayerDetails>> layerChildren = layers.stream()
     198                .filter(x -> x.getParent() != null) // exclude top-level elements
     199                .collect(Collectors.groupingBy(LayerDetails::getParent));
     200        for (LayerDetails ld: layers) {
     201            if (layerChildren.containsKey(ld)) {
     202                ld.setChildren(layerChildren.get(ld));
     203            }
     204        }
     205        // leave only top-most elements in the list
     206        layers = layers.stream().filter(x -> x.getParent() == null).collect(Collectors.toCollection(ArrayList::new));
     207    }
     208
     209    /**
     210     * Returns the list of top-level layers.
     211     * @return the list of top-level layers
    130212     */
    131213    public List<LayerDetails> getLayers() {
     
    134216
    135217    /**
    136      * Returns the service URL.
    137      * @return the service URL
    138      */
    139     public URL getServiceUrl() {
    140         return serviceUrl;
    141     }
    142 
    143     /**
    144      * Returns the WMS version used.
    145      * @return the WMS version used (1.1.1 or 1.3.0)
    146      * @since 13358
    147      */
    148     public String getVersion() {
    149         return version;
    150     }
    151 
    152     /**
    153218     * Returns the list of supported formats.
    154219     * @return the list of supported formats
    155220     */
    156     public List<String> getFormats() {
     221    public Collection<String> getFormats() {
    157222        return Collections.unmodifiableList(formats);
    158223    }
    159224
    160225    /**
    161      * Gets the preffered format for this imagery layer.
    162      * @return The preffered format as mime type.
    163      */
    164     public String getPreferredFormats() {
    165         if (formats.contains("image/jpeg")) {
     226     * Gets the preferred format for this imagery layer.
     227     * @return The preferred format as mime type.
     228     */
     229    public String getPreferredFormat() {
     230        if (formats.contains("image/png")) {
     231            return "image/png";
     232        } else if (formats.contains("image/jpeg")) {
    166233            return "image/jpeg";
    167         } else if (formats.contains("image/png")) {
    168             return "image/png";
    169234        } else if (formats.isEmpty()) {
    170235            return null;
     
    174239    }
    175240
    176     String buildRootUrl() {
    177         if (serviceUrl == null) {
     241    /**
     242     * @return root URL of services in this GetCapabilities
     243     */
     244    public String buildRootUrl() {
     245        if (getMapUrl == null && capabilitiesUrl == null) {
    178246            return null;
    179247        }
     248        if (getMapUrl != null) {
     249            return getMapUrl;
     250        }
     251
     252        URL serviceUrl = capabilitiesUrl;
    180253        StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
    181254        a.append("://").append(serviceUrl.getHost());
     
    194267
    195268    /**
    196      * Returns the URL for the "GetMap" WMS request in JPEG format.
    197      * @param selectedLayers the list of selected layers, matching the "LAYERS" WMS request argument
    198      * @return the URL for the "GetMap" WMS request
    199      */
    200     public String buildGetMapUrl(Collection<LayerDetails> selectedLayers) {
    201         return buildGetMapUrl(selectedLayers, "image/jpeg");
    202     }
    203 
    204     /**
    205      * Returns the URL for the "GetMap" WMS request.
    206      * @param selectedLayers the list of selected layers, matching the "LAYERS" WMS request argument
    207      * @param format the requested image format, matching the "FORMAT" WMS request argument
    208      * @return the URL for the "GetMap" WMS request
    209      */
    210     public String buildGetMapUrl(Collection<LayerDetails> selectedLayers, String format) {
    211         return buildRootUrl() + "FORMAT=" + format + (imageFormatHasTransparency(format) ? "&TRANSPARENT=TRUE" : "")
    212                 + "&VERSION=" + version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS="
    213                 + selectedLayers.stream().map(x -> x.ident).collect(Collectors.joining(","))
    214                 + "&STYLES=&" + ("1.3.0".equals(version) ? "CRS" : "SRS") + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
    215     }
    216 
    217     /**
    218      * Attempts WMS "GetCapabilities" request and initializes internal variables if successful.
    219      * @param serviceUrlStr WMS service URL
    220      * @throws IOException if any I/O errors occurs
    221      * @throws WMSGetCapabilitiesException if the WMS server replies a ServiceException
    222      */
    223     public void attemptGetCapabilities(String serviceUrlStr) throws IOException, WMSGetCapabilitiesException {
     269     * Returns URL for accessing GetMap service. String will contain following parameters:
     270     * * {proj} - that needs to be replaced with projection (one of {@link #getServerProjections(List)})
     271     * * {width} - that needs to be replaced with width of the tile
     272     * * {height} - that needs to be replaces with height of the tile
     273     * * {bbox} - that needs to be replaced with area that should be fetched (in {proj} coordinates)
     274     *
     275     * Format of the response will be calculated using {@link #getPreferredFormat()}
     276     *
     277     * @param selectedLayers list of DefaultLayer selection of layers to be shown
     278     * @param transparent whether returned images should contain transparent pixels (if supported by format)
     279     * @return URL template for GetMap service containing
     280     */
     281    public String buildGetMapUrl(List<DefaultLayer> selectedLayers, boolean transparent) {
     282        return buildGetMapUrl(
     283                getLayers(selectedLayers),
     284                selectedLayers.stream().map(x -> x.getStyle()).collect(Collectors.toList()),
     285                transparent);
     286    }
     287
     288    /**
     289     * @see #buildGetMapUrl(List, boolean)
     290     *
     291     * @param selectedLayers selected layers as subset of the tree returned by {@link #getLayers()}
     292     * @param selectedStyles selected styles for all selectedLayers
     293     * @param transparent whether returned images should contain transparent pixels (if supported by format)
     294     * @return URL template for GetMap service
     295     */
     296    public String buildGetMapUrl(List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
     297        return buildGetMapUrl(
     298                selectedLayers.stream().map(x -> x.getName()).collect(Collectors.toList()),
     299                selectedStyles,
     300                getPreferredFormat(),
     301                transparent);
     302    }
     303
     304    /**
     305     * @see #buildGetMapUrl(List, boolean)
     306     *
     307     * @param selectedLayers selected layers as list of strings
     308     * @param selectedStyles selected styles of layers as list of strings
     309     * @param format format of the response - one of {@link #getFormats()}
     310     * @param transparent whether returned images should contain transparent pixels (if supported by format)
     311     * @return URL template for GetMap service
     312     */
     313    public String buildGetMapUrl(List<String> selectedLayers,
     314            Collection<String> selectedStyles,
     315            String format,
     316            boolean transparent) {
     317
     318        Utils.ensure(selectedStyles == null || selectedLayers.size() == selectedStyles.size(),
     319                tr("Styles size {0} doesn't match layers size {1}"),
     320                selectedStyles == null ? 0 : selectedStyles.size(),
     321                        selectedLayers.size());
     322
     323        return buildRootUrl() + "FORMAT=" + format + ((imageFormatHasTransparency(format) && transparent) ? "&TRANSPARENT=TRUE" : "")
     324                + "&VERSION=" + this.version + "&SERVICE=WMS&REQUEST=GetMap&LAYERS="
     325                + selectedLayers.stream().collect(Collectors.joining(","))
     326                + "&STYLES="
     327                + (selectedStyles != null ? Utils.join(",", selectedStyles) : "")
     328                + "&"
     329                + (belowWMS130() ? "SRS" : "CRS")
     330                + "={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}";
     331    }
     332
     333    private boolean tagEquals(QName a, QName b) {
     334        boolean ret = a.equals(b);
     335        if (ret) {
     336            return ret;
     337        }
     338
     339        if (belowWMS130()) {
     340            return a.getLocalPart().equals(b.getLocalPart());
     341        }
     342
     343        return false;
     344    }
     345
     346    private void attemptGetCapabilities(String url) throws IOException, WMSGetCapabilitiesException {
     347        Logging.debug("Trying WMS getcapabilities with url {0}", url);
     348        try (CachedFile cf = new CachedFile(url); InputStream in = cf.setHttpHeaders(headers).
     349                setMaxAge(7 * CachedFile.DAYS).
     350                setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince).
     351                getInputStream()) {
     352
     353            try {
     354                XMLStreamReader reader = GetCapabilitiesParseHelper.getReader(in);
     355                for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
     356                    if (event == XMLStreamReader.START_ELEMENT) {
     357                        if (tagEquals(CAPABILITIES_ROOT_111, reader.getName())) {
     358                            // version 1.1.1
     359                            this.version = reader.getAttributeValue(null, "version");
     360                            if (this.version == null) {
     361                                this.version = "1.1.1";
     362                            }
     363                        }
     364                        if (tagEquals(CAPABILITITES_ROOT_130, reader.getName())) {
     365                            this.version = reader.getAttributeValue(WMS_NS_URL, "version");
     366                        }
     367                        if (tagEquals(QN_SERVICE, reader.getName())) {
     368                            parseService(reader);
     369                        }
     370
     371                        if (tagEquals(QN_CAPABILITY, reader.getName())) {
     372                            parseCapability(reader);
     373                        }
     374                    }
     375                }
     376            } catch (XMLStreamException e) {
     377                String content = new String(cf.getByteContent(), UTF_8);
     378                cf.clear(); // if there is a problem with parsing of the file, remove it from the cache
     379                throw new WMSGetCapabilitiesException(e, content);
     380            }
     381        }
     382    }
     383
     384    private void parseService(XMLStreamReader reader) throws XMLStreamException {
     385        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_TITLE)) {
     386            this.title = reader.getElementText();
     387            for (int event = reader.getEventType();
     388                    reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_SERVICE, reader.getName()));
     389                    event = reader.next()) {
     390                // empty loop, just move reader to the end of Service tag, if moveReaderToTag return false, it's already done
     391            }
     392        }
     393    }
     394
     395    private void parseCapability(XMLStreamReader reader) throws XMLStreamException {
     396        for (int event = reader.getEventType();
     397                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_CAPABILITY, reader.getName()));
     398                event = reader.next()) {
     399
     400            if (event == XMLStreamReader.START_ELEMENT) {
     401                if (tagEquals(QN_REQUEST, reader.getName())) {
     402                    parseRequest(reader);
     403                }
     404                if (tagEquals(QN_LAYER, reader.getName())) {
     405                    parseLayer(reader, null);
     406                }
     407            }
     408        }
     409    }
     410
     411    private void parseRequest(XMLStreamReader reader) throws XMLStreamException {
     412        String mode = "";
     413        String getMapUrl = "";
     414        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_GETMAP)) {
     415            for (int event = reader.getEventType();
     416                    reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_GETMAP, reader.getName()));
     417                    event = reader.next()) {
     418
     419                if (event == XMLStreamReader.START_ELEMENT) {
     420                    if (tagEquals(QN_FORMAT, reader.getName())) {
     421                        String value = reader.getElementText();
     422                        if (isImageFormatSupportedWarn(value) && !this.formats.contains(value)) {
     423                            this.formats.add(value);
     424                        }
     425                    }
     426                    if (tagEquals(QN_DCPTYPE, reader.getName()) && GetCapabilitiesParseHelper.moveReaderToTag(reader,
     427                            this::tagEquals, QN_HTTP, QN_GET)) {
     428                        mode = reader.getName().getLocalPart();
     429                        if (GetCapabilitiesParseHelper.moveReaderToTag(reader, this::tagEquals, QN_ONLINE_RESOURCE)) {
     430                            getMapUrl = reader.getAttributeValue(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
     431                        }
     432                        // TODO should we handle also POST?
     433                        if ("GET".equalsIgnoreCase(mode) && getMapUrl != null && !"".equals(getMapUrl)) {
     434                            this.getMapUrl = getMapUrl;
     435                        }
     436                    }
     437                }
     438            }
     439        }
     440    }
     441
     442    private void parseLayer(XMLStreamReader reader, LayerDetails parentLayer) throws XMLStreamException {
     443        LayerDetails ret = new LayerDetails(parentLayer);
     444        for (int event = reader.next(); // start with advancing reader by one element to get the contents of the layer
     445                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_LAYER, reader.getName()));
     446                event = reader.next()) {
     447
     448            if (event == XMLStreamReader.START_ELEMENT) {
     449                if (tagEquals(QN_NAME, reader.getName())) {
     450                    ret.setName(reader.getElementText());
     451                }
     452                if (tagEquals(QN_ABSTRACT, reader.getName())) {
     453                    ret.setAbstract(GetCapabilitiesParseHelper.getElementTextWithSubtags(reader));
     454                }
     455                if (tagEquals(QN_TITLE, reader.getName())) {
     456                    ret.setTitle(reader.getElementText());
     457                }
     458                if (tagEquals(QN_CRS, reader.getName())) {
     459                    ret.addCrs(reader.getElementText());
     460                }
     461                if (tagEquals(QN_SRS, reader.getName()) && belowWMS130()) {
     462                    ret.addCrs(reader.getElementText());
     463                }
     464                if (tagEquals(QN_STYLE, reader.getName())) {
     465                    parseAndAddStyle(reader, ret);
     466                }
     467                if (tagEquals(QN_LAYER, reader.getName())) {
     468
     469                    parseLayer(reader, ret);
     470                }
     471                if (tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName())) {
     472                    if (ret.getBounds() == null) {
     473                        Bounds bbox = parseExGeographic(reader);
     474                        ret.setBounds(bbox);
     475                    }
     476
     477                }
     478                if (tagEquals(QN_BOUNDINGBOX, reader.getName())) {
     479                    Projection conv;
     480                    if (belowWMS130()) {
     481                        conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "SRS"));
     482                    } else {
     483                        conv = Projections.getProjectionByCode(reader.getAttributeValue(WMS_NS_URL, "CRS"));
     484                    }
     485                    if (ret.getBounds() == null && conv != null) {
     486                        Bounds bbox = parseBoundingBox(reader, conv);
     487                        ret.setBounds(bbox);
     488                    }
     489                }
     490                if (tagEquals(QN_LATLONBOUNDINGBOX, reader.getName()) && belowWMS130()) {
     491                    if (ret.getBounds() == null) {
     492                        Bounds bbox = parseBoundingBox(reader, null);
     493                        ret.setBounds(bbox);
     494                    }
     495                }
     496            }
     497        }
     498        this.layers.add(ret);
     499    }
     500
     501    /**
     502     * @return if this service operates at protocol level below 1.3.0
     503     */
     504    public boolean belowWMS130() {
     505        return this.version.equals("1.1.1") || this.version.equals("1.1") || this.version.equals("1.0");
     506    }
     507
     508    private void parseAndAddStyle(XMLStreamReader reader, LayerDetails ld) throws XMLStreamException {
     509        String name = null;
     510        String title = null;
     511        for (int event = reader.getEventType();
     512                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_STYLE, reader.getName()));
     513                event = reader.next()) {
     514            if (event == XMLStreamReader.START_ELEMENT) {
     515                if (tagEquals(QN_NAME, reader.getName())) {
     516                    name = reader.getElementText();
     517                }
     518                if (tagEquals(QN_TITLE, reader.getName())) {
     519                    title = reader.getElementText();
     520                }
     521            }
     522        }
     523        if (name == null) {
     524            name = "";
     525        }
     526        ld.addStyle(name, title);
     527    }
     528
     529    private Bounds parseExGeographic(XMLStreamReader reader) throws XMLStreamException {
     530        String minx = null, maxx = null, maxy = null, miny = null;
     531
     532        for (int event = reader.getEventType();
     533                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && tagEquals(QN_EX_GEOGRAPHIC_BBOX, reader.getName()));
     534                event = reader.next()) {
     535            if (event == XMLStreamReader.START_ELEMENT) {
     536                if (tagEquals(QN_WESTBOUNDLONGITUDE, reader.getName())) {
     537                    minx = reader.getElementText();
     538                }
     539
     540                if (tagEquals(QN_EASTBOUNDLONGITUDE, reader.getName())) {
     541                    maxx = reader.getElementText();
     542                }
     543
     544                if (tagEquals(QN_SOUTHBOUNDLATITUDE, reader.getName())) {
     545                    miny = reader.getElementText();
     546                }
     547
     548                if (tagEquals(QN_NORTHBOUNDLATITUDE, reader.getName())) {
     549                    maxy = reader.getElementText();
     550                }
     551            }
     552        }
     553        return parseBBox(null, miny, minx, maxy, maxx);
     554    }
     555
     556    private Bounds parseBoundingBox(XMLStreamReader reader, Projection conv) {
     557        Function<String, String> attrGetter = tag -> belowWMS130() ?
     558                reader.getAttributeValue(null, tag)
     559                : reader.getAttributeValue(WMS_NS_URL, tag);
     560
     561                return parseBBox(
     562                        conv,
     563                        attrGetter.apply("miny"),
     564                        attrGetter.apply("minx"),
     565                        attrGetter.apply("maxy"),
     566                        attrGetter.apply("maxx")
     567                        );
     568    }
     569
     570    private Bounds parseBBox(Projection conv, String miny, String minx, String maxy, String maxx) {
     571        if (miny == null || minx == null || maxy == null || maxx == null) {
     572            return null;
     573        }
     574        if (conv != null) {
     575            new Bounds(
     576                    conv.eastNorth2latlon(new EastNorth(getDecimalDegree(minx), getDecimalDegree(miny))),
     577                    conv.eastNorth2latlon(new EastNorth(getDecimalDegree(maxx), getDecimalDegree(maxy)))
     578                    );
     579        }
     580        return new Bounds(
     581                getDecimalDegree(miny),
     582                getDecimalDegree(minx),
     583                getDecimalDegree(maxy),
     584                getDecimalDegree(maxx)
     585                );
     586    }
     587
     588    private static double getDecimalDegree(String value) {
     589        // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
     590        return Double.parseDouble(value.replace(',', '.'));
     591    }
     592
     593
     594    private String normalizeUrl(String serviceUrlStr) throws MalformedURLException {
    224595        URL getCapabilitiesUrl = null;
    225         try {
    226             if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
    227                 // If the url doesn't already have GetCapabilities, add it in
    228                 getCapabilitiesUrl = new URL(serviceUrlStr);
    229                 final String getCapabilitiesQuery = "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities";
    230                 if (getCapabilitiesUrl.getQuery() == null) {
    231                     getCapabilitiesUrl = new URL(serviceUrlStr + '?' + getCapabilitiesQuery);
    232                 } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
    233                     getCapabilitiesUrl = new URL(serviceUrlStr + '&' + getCapabilitiesQuery);
    234                 } else {
    235                     getCapabilitiesUrl = new URL(serviceUrlStr + getCapabilitiesQuery);
    236                 }
     596        String ret = null;
     597
     598        if (!Pattern.compile(".*GetCapabilities.*", Pattern.CASE_INSENSITIVE).matcher(serviceUrlStr).matches()) {
     599            // If the url doesn't already have GetCapabilities, add it in
     600            getCapabilitiesUrl = new URL(serviceUrlStr);
     601            ret = serviceUrlStr;
     602            if (getCapabilitiesUrl.getQuery() == null) {
     603                ret = serviceUrlStr + '?' + CAPABILITIES_QUERY_STRING;
     604            } else if (!getCapabilitiesUrl.getQuery().isEmpty() && !getCapabilitiesUrl.getQuery().endsWith("&")) {
     605                ret = serviceUrlStr + '&' + CAPABILITIES_QUERY_STRING;
    237606            } else {
    238                 // Otherwise assume it's a good URL and let the subsequent error
    239                 // handling systems deal with problems
    240                 getCapabilitiesUrl = new URL(serviceUrlStr);
    241             }
    242             // Make sure we don't keep GetCapabilities request in service URL
    243             serviceUrl = new URL(serviceUrlStr.replace("REQUEST=GetCapabilities", "").replace("&&", "&"));
    244         } catch (HeadlessException e) {
    245             Logging.warn(e);
    246             return;
    247         }
    248 
    249         doAttemptGetCapabilities(serviceUrlStr, getCapabilitiesUrl);
    250     }
    251 
    252     /**
    253      * Attempts WMS GetCapabilities with version 1.1.1 first, then 1.3.0 in case of specific errors.
    254      * @param serviceUrlStr WMS service URL
    255      * @param getCapabilitiesUrl GetCapabilities URL
    256      * @throws IOException if any I/O error occurs
    257      * @throws WMSGetCapabilitiesException if any HTTP or parsing error occurs
    258      */
    259     private void doAttemptGetCapabilities(String serviceUrlStr, URL getCapabilitiesUrl)
    260             throws IOException, WMSGetCapabilitiesException {
    261         final String url = getCapabilitiesUrl.toExternalForm();
    262         final Response response = HttpClient.create(getCapabilitiesUrl).connect();
    263 
    264         // Is the HTTP connection successul ?
    265         if (response.getResponseCode() >= 400) {
    266             // HTTP error for servers handling only WMS 1.3.0 ?
    267             String errorMessage = response.getResponseMessage();
    268             String errorContent = response.fetchContent();
    269             Matcher tomcat = HttpClient.getTomcatErrorMatcher(errorContent);
    270             boolean messageAbout130 = errorMessage != null && errorMessage.contains("1.3.0");
    271             boolean contentAbout130 = errorContent != null && tomcat != null && tomcat.matches() && tomcat.group(1).contains("1.3.0");
    272             if (url.contains("VERSION=1.1.1") && (messageAbout130 || contentAbout130)) {
    273                 doAttemptGetCapabilities130(serviceUrlStr, url);
    274                 return;
    275             }
    276             throw new WMSGetCapabilitiesException(errorMessage, errorContent);
    277         }
    278 
    279         try {
    280             // Parse XML capabilities sent by the server
    281             parseCapabilities(serviceUrlStr, response.getContent());
    282         } catch (WMSGetCapabilitiesException e) {
    283             // ServiceException for servers handling only WMS 1.3.0 ?
    284             if (e.getCause() == null && url.contains("VERSION=1.1.1")) {
    285                 doAttemptGetCapabilities130(serviceUrlStr, url);
    286             } else {
    287                 throw e;
    288             }
    289         }
    290     }
    291 
    292     /**
    293      * Attempts WMS GetCapabilities with version 1.3.0.
    294      * @param serviceUrlStr WMS service URL
    295      * @param url GetCapabilities URL
    296      * @throws IOException if any I/O error occurs
    297      * @throws WMSGetCapabilitiesException if any HTTP or parsing error occurs
    298      * @throws MalformedURLException in case of invalid URL
    299      */
    300     private void doAttemptGetCapabilities130(String serviceUrlStr, final String url)
    301             throws IOException, WMSGetCapabilitiesException {
    302         doAttemptGetCapabilities(serviceUrlStr, new URL(url.replace("VERSION=1.1.1", "VERSION=1.3.0")));
    303         if (serviceUrl.toExternalForm().contains("VERSION=1.1.1")) {
    304             serviceUrl = new URL(serviceUrl.toExternalForm().replace("VERSION=1.1.1", "VERSION=1.3.0"));
    305         }
    306         version = "1.3.0";
    307     }
    308 
    309     void parseCapabilities(String serviceUrlStr, InputStream contentStream) throws IOException, WMSGetCapabilitiesException {
    310         String incomingData = null;
    311         try {
    312             DocumentBuilder builder = Utils.newSafeDOMBuilder();
    313             builder.setEntityResolver((publicId, systemId) -> {
    314                 Logging.info("Ignoring DTD " + publicId + ", " + systemId);
    315                 return new InputSource(new StringReader(""));
    316             });
    317             Document document = builder.parse(contentStream);
    318             Element root = document.getDocumentElement();
    319 
    320             try {
    321                 StringWriter writer = new StringWriter();
    322                 TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(writer));
    323                 incomingData = writer.getBuffer().toString();
    324                 Logging.debug("Server response to Capabilities request:");
    325                 Logging.debug(incomingData);
    326             } catch (TransformerFactoryConfigurationError | TransformerException e) {
    327                 Logging.warn(e);
    328             }
    329 
    330             // Check if the request resulted in ServiceException
    331             if ("ServiceException".equals(root.getTagName())) {
    332                 throw new WMSGetCapabilitiesException(root.getTextContent(), incomingData);
    333             }
    334 
    335             // Some WMS service URLs specify a different base URL for their GetMap service
    336             Element child = getChild(root, "Capability");
    337             child = getChild(child, "Request");
    338             child = getChild(child, "GetMap");
    339 
    340             formats = getChildrenStream(child, "Format")
    341                     .map(Node::getTextContent)
    342                     .filter(WMSImagery::isImageFormatSupportedWarn)
    343                     .collect(Collectors.toList());
    344 
    345             child = getChild(child, "DCPType");
    346             child = getChild(child, "HTTP");
    347             child = getChild(child, "Get");
    348             child = getChild(child, "OnlineResource");
    349             if (child != null) {
    350                 String baseURL = child.getAttributeNS(GetCapabilitiesParseHelper.XLINK_NS_URL, "href");
    351                 if (!baseURL.equals(serviceUrlStr)) {
    352                     URL newURL = new URL(baseURL);
    353                     if (newURL.getAuthority() != null) {
    354                         Logging.info("GetCapabilities specifies a different service URL: " + baseURL);
    355                         serviceUrl = newURL;
    356                     }
    357                 }
    358             }
    359 
    360             Element capabilityElem = getChild(root, "Capability");
    361             List<Element> children = getChildren(capabilityElem, "Layer");
    362             layers = parseLayers(children, new HashSet<String>());
    363         } catch (MalformedURLException | ParserConfigurationException | SAXException e) {
    364             throw new WMSGetCapabilitiesException(e, incomingData);
    365         }
     607                ret = serviceUrlStr + CAPABILITIES_QUERY_STRING;
     608            }
     609        } else {
     610            // Otherwise assume it's a good URL and let the subsequent error
     611            // handling systems deal with problems
     612            ret = serviceUrlStr;
     613        }
     614        return ret;
    366615    }
    367616
     
    392641    }
    393642
     643
    394644    static boolean imageFormatHasTransparency(final String format) {
    395645        return format != null && (format.startsWith("image/png") || format.startsWith("image/gif")
     
    398648
    399649    /**
    400      * Returns a new {@code ImageryInfo} describing the given service name and selected WMS layers.
    401      * @param name service name
    402      * @param selectedLayers selected WMS layers
    403      * @return a new {@code ImageryInfo} describing the given service name and selected WMS layers
    404      */
    405     public ImageryInfo toImageryInfo(String name, Collection<LayerDetails> selectedLayers) {
    406         ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers));
    407         if (selectedLayers != null) {
    408             Set<String> proj = new HashSet<>();
    409             for (WMSImagery.LayerDetails l : selectedLayers) {
    410                 proj.addAll(l.getProjections());
    411             }
    412             i.setServerProjections(proj);
     650     * Creates ImageryInfo object from this GetCapabilities document
     651     *
     652     * @param name name of imagery layer
     653     * @param selectedLayers layers which are to be used by this imagery layer
     654     * @param selectedStyles styles that should be used for selectedLayers
     655     * @param transparent if layer should be transparent
     656     * @return ImageryInfo object
     657     */
     658    public ImageryInfo toImageryInfo(String name, List<LayerDetails> selectedLayers, List<String> selectedStyles, boolean transparent) {
     659        ImageryInfo i = new ImageryInfo(name, buildGetMapUrl(selectedLayers, selectedStyles, transparent));
     660        if (selectedLayers != null && !selectedLayers.isEmpty()) {
     661            i.setServerProjections(getServerProjections(selectedLayers));
    413662        }
    414663        return i;
    415664    }
    416665
    417     private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) {
    418         List<LayerDetails> details = new ArrayList<>(children.size());
    419         for (Element element : children) {
    420             details.add(parseLayer(element, parentCrs));
    421         }
    422         return details;
    423     }
    424 
    425     private LayerDetails parseLayer(Element element, Set<String> parentCrs) {
    426         String name = getChildContent(element, "Title", null, null);
    427         String ident = getChildContent(element, "Name", null, null);
    428         String abstr = getChildContent(element, "Abstract", null, null);
    429 
    430         // The set of supported CRS/SRS for this layer
    431         Set<String> crsList = new HashSet<>();
    432         // ...including this layer's already-parsed parent projections
    433         crsList.addAll(parentCrs);
    434 
    435         // Parse the CRS/SRS pulled out of this layer's XML element
    436         // I think CRS and SRS are the same at this point
    437         getChildrenStream(element)
    438             .filter(child -> "CRS".equals(child.getNodeName()) || "SRS".equals(child.getNodeName()))
    439             .map(WMSImagery::getContent)
    440             .filter(crs -> !crs.isEmpty())
    441             .map(crs -> crs.trim().toUpperCase(Locale.ENGLISH))
    442             .forEach(crsList::add);
    443 
    444         // Check to see if any of the specified projections are supported by JOSM
    445         boolean josmSupportsThisLayer = false;
    446         for (String crs : crsList) {
    447             josmSupportsThisLayer |= isProjSupported(crs);
    448         }
    449 
    450         Bounds bounds = null;
    451         Element bboxElem = getChild(element, "EX_GeographicBoundingBox");
    452         if (bboxElem != null) {
    453             // Attempt to use EX_GeographicBoundingBox for bounding box
    454             double left = Double.parseDouble(getChildContent(bboxElem, "westBoundLongitude", null, null));
    455             double top = Double.parseDouble(getChildContent(bboxElem, "northBoundLatitude", null, null));
    456             double right = Double.parseDouble(getChildContent(bboxElem, "eastBoundLongitude", null, null));
    457             double bot = Double.parseDouble(getChildContent(bboxElem, "southBoundLatitude", null, null));
    458             bounds = new Bounds(bot, left, top, right);
    459         } else {
    460             // If that's not available, try LatLonBoundingBox
    461             bboxElem = getChild(element, "LatLonBoundingBox");
    462             if (bboxElem != null) {
    463                 double left = getDecimalDegree(bboxElem, "minx");
    464                 double top = getDecimalDegree(bboxElem, "maxy");
    465                 double right = getDecimalDegree(bboxElem, "maxx");
    466                 double bot = getDecimalDegree(bboxElem, "miny");
    467                 bounds = new Bounds(bot, left, top, right);
    468             }
    469         }
    470 
    471         List<Element> layerChildren = getChildren(element, "Layer");
    472         List<LayerDetails> childLayers = parseLayers(layerChildren, crsList);
    473 
    474         return new LayerDetails(name, ident, abstr, crsList, josmSupportsThisLayer, bounds, childLayers);
    475     }
    476 
    477     private static double getDecimalDegree(Element elem, String attr) {
    478         // Some real-world WMS servers use a comma instead of a dot as decimal separator (seen in Polish WMS server)
    479         return Double.parseDouble(elem.getAttribute(attr).replace(',', '.'));
    480     }
    481 
    482     private static boolean isProjSupported(String crs) {
    483         return Projections.getProjectionByCode(crs) != null;
    484     }
    485 
    486     private static String getChildContent(Element parent, String name, String missing, String empty) {
    487         Element child = getChild(parent, name);
    488         if (child == null)
    489             return missing;
    490         else {
    491             String content = getContent(child);
    492             return (!content.isEmpty()) ? content : empty;
    493         }
    494     }
    495 
    496     private static String getContent(Element element) {
    497         NodeList nl = element.getChildNodes();
    498         StringBuilder content = new StringBuilder();
    499         for (int i = 0; i < nl.getLength(); i++) {
    500             Node node = nl.item(i);
    501             switch (node.getNodeType()) {
    502                 case Node.ELEMENT_NODE:
    503                     content.append(getContent((Element) node));
    504                     break;
    505                 case Node.CDATA_SECTION_NODE:
    506                 case Node.TEXT_NODE:
    507                     content.append(node.getNodeValue());
    508                     break;
    509                 default: // Do nothing
    510             }
    511         }
    512         return content.toString().trim();
    513     }
    514 
    515     private static Stream<Element> getChildrenStream(Element parent) {
    516         if (parent == null) {
    517             // ignore missing elements
    518             return Stream.empty();
    519         } else {
    520             Iterable<Element> it = () -> new ChildIterator(parent);
    521             return StreamSupport.stream(it.spliterator(), false);
    522         }
    523     }
    524 
    525     private static Stream<Element> getChildrenStream(Element parent, String name) {
    526         return getChildrenStream(parent).filter(child -> name.equals(child.getNodeName()));
    527     }
    528 
    529     private static List<Element> getChildren(Element parent, String name) {
    530         return getChildrenStream(parent, name).collect(Collectors.toList());
    531     }
    532 
    533     private static Element getChild(Element parent, String name) {
    534         return getChildrenStream(parent, name).findFirst().orElse(null);
    535     }
    536 
    537     /**
    538      * The details of a layer of this WMS server.
    539      */
    540     public static class LayerDetails {
    541 
    542         /**
    543          * The layer name (WMS {@code Title})
    544          */
    545         public final String name;
    546         /**
    547          * The layer ident (WMS {@code Name})
    548          */
    549         public final String ident;
    550         /**
    551          * The layer abstract (WMS {@code Abstract})
    552          * @since 13199
    553          */
    554         public final String abstr;
    555         /**
    556          * The child layers of this layer
    557          */
    558         public final List<LayerDetails> children;
    559         /**
    560          * The bounds this layer can be used for
    561          */
    562         public final Bounds bounds;
    563         /**
    564          * the CRS/SRS pulled out of this layer's XML element
    565          */
    566         public final Set<String> crsList;
    567         /**
    568          * {@code true} if any of the specified projections are supported by JOSM
    569          */
    570         public final boolean supported;
    571 
    572         /**
    573          * Constructs a new {@code LayerDetails}.
    574          * @param name The layer name (WMS {@code Title})
    575          * @param ident The layer ident (WMS {@code Name})
    576          * @param abstr The layer abstract (WMS {@code Abstract})
    577          * @param crsList The CRS/SRS pulled out of this layer's XML element
    578          * @param supportedLayer {@code true} if any of the specified projections are supported by JOSM
    579          * @param bounds The bounds this layer can be used for
    580          * @param childLayers The child layers of this layer
    581          * @since 13199
    582          */
    583         public LayerDetails(String name, String ident, String abstr, Set<String> crsList, boolean supportedLayer, Bounds bounds,
    584                 List<LayerDetails> childLayers) {
    585             this.name = name;
    586             this.ident = ident;
    587             this.abstr = abstr;
    588             this.supported = supportedLayer;
    589             this.children = childLayers;
    590             this.bounds = bounds;
    591             this.crsList = crsList;
    592         }
    593 
    594         /**
    595          * Determines if any of the specified projections are supported by JOSM.
    596          * @return {@code true} if any of the specified projections are supported by JOSM
    597          */
    598         public boolean isSupported() {
    599             return this.supported;
    600         }
    601 
    602         /**
    603          * Returns the CRS/SRS pulled out of this layer's XML element.
    604          * @return the CRS/SRS pulled out of this layer's XML element
    605          */
    606         public Set<String> getProjections() {
    607             return crsList;
    608         }
    609 
    610         @Override
    611         public String toString() {
    612             String baseName = (name == null || name.isEmpty()) ? ident : name;
    613             return abstr == null || abstr.equalsIgnoreCase(baseName) ? baseName : baseName + " (" + abstr + ')';
    614         }
     666    /**
     667     * Returns projections that server supports for provided list of layers. This will be intersection of projections
     668     * defined for each layer
     669     *
     670     * @param selectedLayers list of layers
     671     * @return projection code
     672     */
     673    public Collection<String> getServerProjections(List<LayerDetails> selectedLayers) {
     674        if (selectedLayers.isEmpty()) {
     675            return Collections.emptyList();
     676        }
     677        Set<String> proj = new HashSet<>(selectedLayers.get(0).getCrs());
     678
     679        // set intersect with all layers
     680        for (LayerDetails ld: selectedLayers) {
     681            proj.retainAll(ld.getCrs());
     682        }
     683        return proj;
     684    }
     685
     686
     687    /**
     688     * @param defaultLayers
     689     * @return collection of LayerDetails specified by DefaultLayers
     690     */
     691    public List<LayerDetails> getLayers(List<DefaultLayer> defaultLayers) {
     692        Collection<String> layerNames = defaultLayers.stream().map(x -> x.getLayerName()).collect(Collectors.toList());
     693        return layers.stream()
     694                .flatMap(LayerDetails::flattened)
     695                .filter(x -> layerNames.contains(x.getName()))
     696                .collect(Collectors.toList());
     697    }
     698
     699    /**
     700     * @return title of this service
     701     */
     702    public String getTitle() {
     703        return title;
    615704    }
    616705}
  • trunk/test/unit/org/openstreetmap/josm/TestUtils.java

    r13489 r13733  
    1515import java.security.AccessController;
    1616import java.security.PrivilegedAction;
     17import java.time.Instant;
     18import java.time.ZoneOffset;
     19import java.time.format.DateTimeFormatter;
     20import java.time.temporal.Temporal;
    1721import java.util.Arrays;
    1822import java.util.Collection;
     
    3842import org.openstreetmap.josm.tools.JosmRuntimeException;
    3943import org.openstreetmap.josm.tools.Utils;
     44
     45import com.github.tomakehurst.wiremock.WireMockServer;
     46import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
    4047
    4148import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    380387        }
    381388    }
     389
     390    /**
     391     * Return WireMock server serving files under ticker directory
     392     * @param ticketId Ticket numeric identifier
     393     * @return WireMock HTTP server on dynamic port
     394     */
     395    public static WireMockServer getWireMockServer(int ticketId) {
     396            return new WireMockServer(
     397                    WireMockConfiguration.options()
     398                        .dynamicPort()
     399                        .usingFilesUnderDirectory(getRegressionDataDir(ticketId))
     400                    );
     401    }
     402
     403    /**
     404     * Return WireMock server serving files under ticker directory
     405     * @return WireMock HTTP server on dynamic port
     406     */
     407    public static WireMockServer getWireMockServer() {
     408            return new WireMockServer(
     409                    WireMockConfiguration.options()
     410                        .dynamicPort()
     411                    );
     412    }
     413    /**
     414     * Renders Temporal to RFC 1123 Date Time
     415     * @param time
     416     * @return string representation according to RFC1123 of time
     417     */
     418    public static String getHTTPDate(Temporal time) {
     419        return DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC).format(time);
     420    }
     421
     422    /**
     423     * Renders java time stamp to RFC 1123 Date Time
     424     * @param time
     425     * @return string representation according to RFC1123 of time
     426     */
     427    public static String getHTTPDate(long time) {
     428        return getHTTPDate(Instant.ofEpochMilli(time));
     429    }
     430
     431
    382432}
  • trunk/test/unit/org/openstreetmap/josm/actions/AddImageryLayerActionTest.java

    r12636 r13733  
    7070    @Test
    7171    public void testActionPerformedEnabledWms() {
    72         wireMockRule.stubFor(get(urlEqualTo("/wms?VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities"))
     72        wireMockRule.stubFor(get(urlEqualTo("/wms?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.1.1"))
    7373                .willReturn(aResponse()
    74                     .withStatus(200)
    75                     .withHeader("Content-Type", "text/xml")
    76                     .withBodyFile("imagery/wms-capabilities.xml")));
     74                        .withStatus(200)
     75                        .withHeader("Content-Type", "text/xml")
     76                        .withBodyFile("imagery/wms-capabilities.xml")));
     77        wireMockRule.stubFor(get(urlEqualTo("/wms?SERVICE=WMS&REQUEST=GetCapabilities"))
     78                .willReturn(aResponse()
     79                        .withStatus(404)));
     80        wireMockRule.stubFor(get(urlEqualTo("/wms?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0"))
     81                .willReturn(aResponse()
     82                        .withStatus(404)));
     83
    7784        new AddImageryLayerAction(new ImageryInfo("localhost", "http://localhost:" + wireMockRule.port() + "/wms?",
    7885                "wms_endpoint", null, null)).actionPerformed(null);
  • trunk/test/unit/org/openstreetmap/josm/data/cache/HostLimitQueueTest.java

    r12620 r13733  
    1414import org.junit.Rule;
    1515import org.junit.Test;
     16import org.openstreetmap.josm.data.imagery.TileJobOptions;
    1617import org.openstreetmap.josm.testutils.JOSMTestRules;
    1718import org.openstreetmap.josm.tools.Logging;
     
    5455
    5556        Task(ICacheAccess<String, CacheEntry> cache, URL url, AtomicInteger counter) {
    56             super(cache, 1, 1, null);
     57            super(cache, new TileJobOptions(1, 1, null, 10));
    5758            this.url = url;
    5859            this.counter = counter;
  • trunk/test/unit/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJobTest.java

    r13358 r13733  
    22package org.openstreetmap.josm.data.cache;
    33
     4import static org.junit.Assert.assertArrayEquals;
    45import static org.junit.Assert.assertEquals;
    56import static org.junit.Assert.assertFalse;
     7import static org.junit.Assert.assertTrue;
    68
    79import java.io.IOException;
     
    911import java.net.URL;
    1012import java.nio.charset.StandardCharsets;
     13import java.util.concurrent.TimeUnit;
    1114
    1215import org.apache.commons.jcs.access.behavior.ICacheAccess;
     16import org.apache.commons.jcs.engine.behavior.ICacheElement;
    1317import org.junit.Before;
    1418import org.junit.Rule;
    1519import org.junit.Test;
     20import org.openstreetmap.josm.TestUtils;
    1621import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
     22import org.openstreetmap.josm.data.imagery.TileJobOptions;
    1723import org.openstreetmap.josm.testutils.JOSMTestRules;
    1824import org.openstreetmap.josm.tools.Logging;
     25
     26import com.github.tomakehurst.wiremock.client.WireMock;
     27import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
     28import com.github.tomakehurst.wiremock.junit.WireMockRule;
     29import com.github.tomakehurst.wiremock.matching.UrlPattern;
    1930
    2031import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    2536public class JCSCachedTileLoaderJobTest {
    2637
     38    /**
     39     * mocked tile server
     40     */
     41    @Rule
     42    public WireMockRule tileServer = new WireMockRule(WireMockConfiguration.options()
     43            .dynamicPort());
     44
    2745    private static class TestCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, CacheEntry> {
    2846        private String url;
    2947        private String key;
    3048
    31         TestCachedTileLoaderJob(String url, String key) throws IOException {
    32             super(getCache(), 30000, 30000, null);
     49        TestCachedTileLoaderJob(String url, String key)  {
     50            this(url, key, (int) TimeUnit.DAYS.toSeconds(1));
     51        }
     52
     53        TestCachedTileLoaderJob(String url, String key, int minimumExpiry)  {
     54            super(getCache(), new TileJobOptions(30000, 30000, null, minimumExpiry));
    3355
    3456            this.url = url;
    3557            this.key = key;
    3658        }
     59
    3760
    3861        @Override
     
    5275        @Override
    5376        protected CacheEntry createCacheEntry(byte[] content) {
    54             return new CacheEntry("dummy".getBytes(StandardCharsets.UTF_8));
     77            return new CacheEntry(content);
    5578        }
    5679    }
     
    6083        private boolean ready;
    6184        private LoadResult result;
     85        private byte[] data;
    6286
    6387        @Override
     
    6690            this.ready = true;
    6791            this.result = result;
     92            if (data != null) {
     93                this.data = data.content;
     94            }
    6895            this.notifyAll();
    6996        }
     
    113140        String key = "key_unknown_host";
    114141        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob("http://unkownhost.unkownhost/unkown", key);
    115         Listener listener = new Listener();
    116         job.submit(listener, true);
    117         synchronized (listener) {
    118             while (!listener.ready) {
    119                 try {
    120                     listener.wait();
    121                 } catch (InterruptedException e1) {
    122                     // do nothing, still wait
    123                     Logging.trace(e1);
    124                 }
    125             }
    126         }
     142        Listener listener = submitJob(job);
    127143        assertEquals(LoadResult.FAILURE, listener.result); // because response will be cached, and that is checked below
    128144        assertEquals("java.net.UnknownHostException: unkownhost.unkownhost", listener.attributes.getErrorMessage());
     
    135151
    136152        job = new TestCachedTileLoaderJob("http://unkownhost.unkownhost/unkown", key);
    137         listener = new Listener();
    138         job.submit(listener, true);
     153        listener = submitJob(job);
     154        assertEquals(LoadResult.SUCCESS, listener.result);
     155        assertFalse(job.isCacheElementValid());
     156    }
     157
     158    private void doTestStatusCode(int responseCode) throws IOException {
     159        TestCachedTileLoaderJob job = getStatusLoaderJob(responseCode);
     160        Listener listener = submitJob(job);
     161        assertEquals(responseCode, listener.attributes.getResponseCode());
     162    }
     163
     164    private Listener submitJob(TestCachedTileLoaderJob job) throws IOException {
     165        return submitJob(job, true);
     166    }
     167
     168    private Listener submitJob(TestCachedTileLoaderJob job, boolean force) throws IOException {
     169        Listener listener = new Listener();
     170        job.submit(listener, force);
    139171        synchronized (listener) {
    140172            while (!listener.ready) {
    141173                try {
    142174                    listener.wait();
    143                 } catch (InterruptedException e1) {
     175                } catch (InterruptedException e) {
    144176                    // do nothing, wait
    145                     Logging.trace(e1);
     177                    Logging.trace(e);
    146178                }
    147179            }
    148180        }
    149         assertEquals(LoadResult.SUCCESS, listener.result);
    150         assertFalse(job.isCacheElementValid());
    151     }
    152 
    153     @SuppressFBWarnings(value = "WA_NOT_IN_LOOP")
    154     private void doTestStatusCode(int responseCode) throws IOException, InterruptedException {
    155         TestCachedTileLoaderJob job = getStatusLoaderJob(responseCode);
    156         Listener listener = new Listener();
    157         job.submit(listener, true);
    158         synchronized (listener) {
    159             if (!listener.ready) {
    160                 listener.wait();
    161             }
    162         }
    163         assertEquals(responseCode, listener.attributes.getResponseCode());
    164     }
    165 
    166     private static TestCachedTileLoaderJob getStatusLoaderJob(int responseCode) throws IOException {
     181        return listener;
     182    }
     183
     184    /**
     185     * That no requst is made when entry is in cache and force == false
     186     * @throws IOException
     187     */
     188    @Test
     189    public void testNoRequestMadeWhenEntryInCache() throws IOException {
     190        ICacheAccess<String, CacheEntry> cache = getCache();
     191        long expires = TimeUnit.DAYS.toMillis(1);
     192        long testStart = System.currentTimeMillis();
     193        cache.put("test",
     194                new CacheEntry("cached entry".getBytes(StandardCharsets.UTF_8)),
     195                createEntryAttributes(expires, 200, testStart, "eTag")
     196                );
     197        createHeadGetStub(WireMock.urlEqualTo("/test"), expires, testStart, "eTag", "mock entry");
     198
     199        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
     200        Listener listener = submitJob(job, false);
     201        tileServer.verify(0, WireMock.getRequestedFor(WireMock.anyUrl()));
     202        assertArrayEquals("cached entry".getBytes(StandardCharsets.UTF_8), listener.data);
     203    }
     204
     205    /**
     206     * that request is made, when object is in cache, but force mode is used
     207     * @throws IOException
     208     */
     209    @Test
     210    public void testRequestMadeWhenEntryInCacheAndForce() throws IOException {
     211        ICacheAccess<String, CacheEntry> cache = getCache();
     212        long expires =  TimeUnit.DAYS.toMillis(1);
     213        long testStart = System.currentTimeMillis();
     214        cache.put("test",
     215                new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
     216                createEntryAttributes(expires, 200, testStart + expires, "eTag")
     217                );
     218        createHeadGetStub(WireMock.urlEqualTo("/test"), expires, testStart, "eTag", "mock entry");
     219
     220        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
     221        Listener listener = submitJob(job, true);
     222        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     223        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     224    }
     225
     226    /**
     227     * Mock returns no cache-control / expires headers
     228     * Expire time should be set to DEFAULT_EXPIRE_TIME
     229     * @throws IOException
     230     */
     231    @Test
     232    public void testSettingMinimumExpiryWhenNoExpires() throws IOException {
     233        long testStart = System.currentTimeMillis();
     234        tileServer.stubFor(
     235                WireMock.get(WireMock.urlEqualTo("/test"))
     236                .willReturn(WireMock.aResponse()
     237                        .withBody("mock entry")
     238                        )
     239                );
     240
     241        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
     242        Listener listener = submitJob(job, false);
     243        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     244
     245        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
     246                JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME + " (DEFAULT_EXPIRE_TIME)",
     247                listener.attributes.getExpirationTime() >= testStart + JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME);
     248
     249        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
     250                JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME + " (DEFAULT_EXPIRE_TIME)",
     251                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME);
     252
     253        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     254    }
     255
     256    /**
     257     * Mock returns expires headers, but Cache-Control
     258     * Expire time should be set to max-age
     259     * @throws IOException
     260     */
     261    @Test
     262    public void testSettingExpireByMaxAge() throws IOException {
     263        long testStart = System.currentTimeMillis();
     264        long expires =  TimeUnit.DAYS.toSeconds(1);
     265        tileServer.stubFor(
     266                WireMock.get(WireMock.urlEqualTo("/test"))
     267                .willReturn(WireMock.aResponse()
     268                        .withHeader("Cache-control", "max-age=" + expires)
     269                        .withBody("mock entry")
     270                        )
     271                );
     272
     273        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
     274        Listener listener = submitJob(job, false);
     275        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     276
     277        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
     278                TimeUnit.SECONDS.toMillis(expires) + " (max-age)",
     279                listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(expires));
     280
     281        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
     282                TimeUnit.SECONDS.toMillis(expires) + " (max-age)",
     283                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expires));
     284
     285        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     286    }
     287
     288    /**
     289     * mock returns expiration: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10
     290     * minimum expire time: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 2
     291     * @throws IOException
     292     */
     293    @Test
     294    public void testSettingMinimumExpiryByMinimumExpiryTimeLessThanDefault() throws IOException {
     295        long testStart = System.currentTimeMillis();
     296        int minimumExpiryTimeSeconds = (int)(JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 2);
     297
     298        createHeadGetStub(WireMock.urlEqualTo("/test"), (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10), testStart, "eTag", "mock entry");
     299
     300        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test", minimumExpiryTimeSeconds);
     301        Listener listener = submitJob(job, false);
     302        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     303        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     304
     305
     306        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
     307                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
     308                listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) );
     309
     310        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
     311                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
     312                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds));
     313    }
     314
     315    /**
     316     * mock returns expiration: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10
     317     * minimum expire time: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME * 2
     318     * @throws IOException
     319     */
     320
     321    @Test
     322    public void testSettingMinimumExpiryByMinimumExpiryTimeGreaterThanDefault() throws IOException {
     323        long testStart = System.currentTimeMillis();
     324        int minimumExpiryTimeSeconds = (int)(JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME * 2);
     325
     326        createHeadGetStub(WireMock.urlEqualTo("/test"), (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10), testStart, "eTag", "mock entry");
     327
     328        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test", minimumExpiryTimeSeconds);
     329        Listener listener = submitJob(job, false);
     330        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     331        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     332
     333
     334        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
     335                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
     336                listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) );
     337
     338        assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - System.currentTimeMillis()) + " which is not less than " +
     339                TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
     340                listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds));
     341    }
     342
     343    /**
     344     * Check if verifying cache entries using HEAD requests work properly
     345     * @throws IOException
     346     */
     347    @Test
     348    public void testCheckUsingHead() throws IOException {
     349        ICacheAccess<String, CacheEntry> cache = getCache();
     350        long expires = TimeUnit.DAYS.toMillis(1);
     351        long testStart = System.currentTimeMillis();
     352        cache.put("test",
     353                new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
     354                createEntryAttributes(-1 * expires, 200, testStart, "eTag--gzip") // Jetty adds --gzip to etags when compressing output
     355                );
     356
     357        tileServer.stubFor(
     358                WireMock.get(WireMock.urlEqualTo("/test"))
     359                .willReturn(WireMock.aResponse()
     360                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
     361                        .withHeader("Last-Modified", Long.toString(testStart))
     362                        .withHeader("ETag", "eTag") // Jetty adds "--gzip" suffix for compressed content
     363                        .withBody("mock entry")
     364                        )
     365                );
     366        tileServer.stubFor(
     367                WireMock.head(WireMock.urlEqualTo("/test"))
     368                .willReturn(WireMock.aResponse()
     369                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
     370                        .withHeader("Last-Modified", Long.toString(testStart))
     371                        .withHeader("ETag", "eTag--gzip") // but doesn't add to uncompressed
     372                        )
     373                );
     374
     375        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
     376        Listener listener = submitJob(job, false); // cache entry is expired, no need to force refetch
     377        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     378        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     379
     380        // cache entry should be retrieved from cache
     381        listener = submitJob(job, false);
     382        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     383        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     384
     385        // invalidate entry in cache
     386        ICacheElement<String, CacheEntry> cacheEntry = cache.getCacheElement("test");
     387        CacheEntryAttributes attributes = (CacheEntryAttributes)cacheEntry.getElementAttributes();
     388        attributes.setExpirationTime(testStart - TimeUnit.DAYS.toMillis(1));
     389        cache.put("test", cacheEntry.getVal(), attributes);
     390
     391        // because cache entry is invalid - HEAD request shall be made
     392        tileServer.verify(0, WireMock.headRequestedFor(WireMock.urlEqualTo("/test"))); // no head requests were made until now
     393        listener = submitJob(job, false);
     394        tileServer.verify(1, WireMock.headRequestedFor(WireMock.urlEqualTo("/test"))); // verify head requests were made
     395        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test"))); // verify no more get requests were made
     396        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     397        assertTrue(listener.attributes.getExpirationTime() >= testStart + expires);
     398
     399        // cache entry should be retrieved from cache
     400        listener = submitJob(job, false); // cache entry is expired, no need to force refetch
     401        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     402        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     403        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
     404    }
     405
     406    /**
     407     * Check if server returns 304 - it will update cache attributes and not ask again for it
     408     * @throws IOException
     409     */
     410    @Test
     411    public void testCheckUsing304() throws IOException {
     412        ICacheAccess<String, CacheEntry> cache = getCache();
     413        long expires = TimeUnit.DAYS.toMillis(1);
     414        long testStart = System.currentTimeMillis();
     415        cache.put("test",
     416                new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
     417                createEntryAttributes(-1 * expires, 200, testStart, "eTag")
     418                );
     419
     420        tileServer.stubFor(
     421                WireMock.get(WireMock.urlEqualTo("/test"))
     422                .willReturn(WireMock.status(304)
     423                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
     424                        .withHeader("Last-Modified", Long.toString(testStart))
     425                        .withHeader("ETag", "eTag")
     426                        )
     427                );
     428
     429        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
     430        Listener listener = submitJob(job, false);
     431        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     432        assertArrayEquals("cached dummy".getBytes(StandardCharsets.UTF_8), listener.data);
     433        assertTrue(testStart + expires <= listener.attributes.getExpirationTime());
     434        listener = submitJob(job, false);
     435        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test"))); // no more requests were made
     436    }
     437
     438    private void createHeadGetStub(UrlPattern url, long expires, long lastModified, String eTag, String body) {
     439        tileServer.stubFor(
     440                WireMock.get(url)
     441                .willReturn(WireMock.aResponse()
     442                        .withHeader("Expires", TestUtils.getHTTPDate(lastModified + expires))
     443                        .withHeader("Last-Modified", Long.toString(lastModified))
     444                        .withHeader("ETag", eTag)
     445                        .withBody(body)
     446                        )
     447                );
     448        tileServer.stubFor(
     449                WireMock.head(url)
     450                .willReturn(WireMock.aResponse()
     451                        .withHeader("Expires", TestUtils.getHTTPDate(lastModified + expires))
     452                        .withHeader("Last-Modified", Long.toString(lastModified))
     453                        .withHeader("ETag", eTag)
     454                        )
     455                );
     456    }
     457
     458    private CacheEntryAttributes createEntryAttributes(long maxAge, int responseCode, String eTag) {
     459        long validTo = maxAge + System.currentTimeMillis();
     460        return createEntryAttributes(maxAge, responseCode, validTo, eTag);
     461    }
     462
     463    private CacheEntryAttributes createEntryAttributes(long expirationTime, int responseCode, long lastModification, String eTag) {
     464        CacheEntryAttributes entryAttributes = new CacheEntryAttributes();
     465        entryAttributes.setExpirationTime(lastModification + expirationTime);
     466        entryAttributes.setResponseCode(responseCode);
     467        entryAttributes.setLastModification(lastModification);
     468        entryAttributes.setEtag(eTag);
     469        return entryAttributes;
     470    }
     471
     472    private static TestCachedTileLoaderJob getStatusLoaderJob(int responseCode)  {
    167473        return new TestCachedTileLoaderJob("http://httpstat.us/" + responseCode, "key_" + responseCode);
    168474    }
    169475
    170     private static ICacheAccess<String, CacheEntry> getCache() throws IOException {
     476    private static ICacheAccess<String, CacheEntry> getCache() {
    171477        return JCSCacheManager.getCache("test");
    172478    }
  • trunk/test/unit/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJobTest.java

    r13631 r13733  
    22package org.openstreetmap.josm.data.imagery;
    33
     4import static org.junit.Assert.assertArrayEquals;
    45import static org.junit.Assert.assertEquals;
    56import static org.junit.Assert.assertTrue;
    67
     8import java.io.IOException;
     9import java.net.MalformedURLException;
     10import java.net.URL;
     11import java.nio.charset.StandardCharsets;
     12import java.util.concurrent.Executors;
     13import java.util.concurrent.ThreadPoolExecutor;
     14import java.util.concurrent.TimeUnit;
    715import java.util.regex.Matcher;
    816
     17import org.apache.commons.jcs.access.behavior.ICacheAccess;
     18import org.junit.Before;
    919import org.junit.Rule;
    1020import org.junit.Test;
     21import org.openstreetmap.gui.jmapviewer.Tile;
     22import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     23import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
     24import org.openstreetmap.josm.TestUtils;
     25import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     26import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
     27import org.openstreetmap.josm.data.cache.JCSCacheManager;
    1128import org.openstreetmap.josm.testutils.JOSMTestRules;
     29import org.openstreetmap.josm.tools.Logging;
    1230import org.openstreetmap.josm.tools.Utils;
     31
     32import com.github.tomakehurst.wiremock.client.WireMock;
     33import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
     34import com.github.tomakehurst.wiremock.junit.WireMockRule;
    1335
    1436import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    2446    @Rule
    2547    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    26     public JOSMTestRules test = new JOSMTestRules();
     48    public JOSMTestRules test = new JOSMTestRules().preferences();
     49
     50    /**
     51     * mocked tile server
     52     */
     53    @Rule
     54    public WireMockRule tileServer = new WireMockRule(WireMockConfiguration.options()
     55            .dynamicPort());
     56
     57    @Before
     58    public void clearCache() throws Exception {
     59        getCache().clear();
     60    }
     61
     62    private static ICacheAccess<String, BufferedImageCacheEntry> getCache() {
     63        return JCSCacheManager.getCache("test");
     64    }
     65
     66    private static class TestCachedTileLoaderJob extends TMSCachedTileLoaderJob {
     67        private String url;
     68        private String key;
     69
     70        TestCachedTileLoaderJob(TileLoaderListener listener, Tile tile, String key) throws IOException  {
     71            this(listener, tile, key,  (int) TimeUnit.DAYS.toSeconds(1));
     72        }
     73
     74        TestCachedTileLoaderJob(TileLoaderListener listener, Tile tile, String key, int minimumExpiry) throws IOException  {
     75            super(listener, tile, getCache(), new TileJobOptions(30000, 30000, null, minimumExpiry),
     76                    (ThreadPoolExecutor) Executors.newFixedThreadPool(1));
     77
     78            this.url = tile.getUrl();
     79            this.key = key;
     80        }
     81
     82        @Override
     83        public URL getUrl() {
     84            try {
     85                return new URL(url);
     86            } catch (MalformedURLException e) {
     87                throw new RuntimeException(e);
     88            }
     89        }
     90
     91        @Override
     92        protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
     93            return new BufferedImageCacheEntry(content);
     94        }
     95
     96        public CacheEntryAttributes getAttributes() {
     97            return attributes;
     98        }
     99
     100        @Override
     101        public boolean isObjectLoadable() {
     102            // use implementation from grand parent, to avoid calling getImage on dummy data
     103            if (cacheData == null) {
     104                return false;
     105            }
     106            return cacheData.getContent().length > 0;        }
     107    }
     108
     109    private static class Listener implements TileLoaderListener {
     110        private CacheEntryAttributes attributes;
     111        private boolean ready;
     112        private byte[] data;
     113
     114
     115        @Override
     116        public synchronized void tileLoadingFinished(Tile tile, boolean success) {
     117            ready = true;
     118            this.notifyAll();
     119        }
     120    }
     121
     122    private static class MockTile extends Tile {
     123        MockTile(String url) {
     124            super(new MockTileSource(url), 0, 0, 0);
     125        }
     126    }
     127
     128    private static class MockTileSource extends TMSTileSource {
     129        private final String url;
     130
     131        public MockTileSource(String url) {
     132            super(new ImageryInfo("mock"));
     133            this.url = url;
     134        }
     135
     136        @Override
     137        public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
     138            return url;
     139        }
     140    }
    27141
    28142    /**
     
    53167        assertEquals(expected, Utils.strip(m.group(1)));
    54168    }
     169
     170    private TestCachedTileLoaderJob submitJob(MockTile tile, String key, boolean force) throws IOException {
     171        return submitJob(tile, key, 0, force);
     172    }
     173
     174    private TestCachedTileLoaderJob submitJob(MockTile tile, String key, int minimumExpiry, boolean force) throws IOException {
     175        Listener listener = new Listener();
     176        TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(listener, tile, key, minimumExpiry);
     177        job.submit(force);
     178        synchronized (listener) {
     179            while (!listener.ready) {
     180                try {
     181                    listener.wait();
     182                } catch (InterruptedException e) {
     183                    // do nothing, wait
     184                    Logging.trace(e);
     185                }
     186            }
     187        }
     188        return job;
     189    }
     190
     191    /**
     192     * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least MINIMUM_EXPIRES
     193     * @throws IOException
     194     */
     195    @Test
     196    public void testNoCacheHeaders() throws IOException {
     197        long testStart = System.currentTimeMillis();
     198        tileServer.stubFor(
     199                WireMock.get(WireMock.urlEqualTo("/test"))
     200                .willReturn(WireMock.aResponse()
     201                        .withBody("mock entry")
     202                        )
     203                );
     204
     205        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
     206        assertExpirationAtLeast(testStart + TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get(), job);
     207        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     208        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
     209        // only one request to tile server should be made, second should come from cache
     210        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     211        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     212    }
     213
     214    /**
     215     * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least minimumExpires parameter
     216     * @throws IOException
     217     */
     218    @Test
     219    public void testNoCacheHeadersMinimumExpires() throws IOException {
     220        noCacheHeadersMinimumExpires((int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get() * 2));
     221    }
     222
     223    /**
     224     * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least minimumExpires parameter,
     225     * which is larger than MAXIMUM_EXPIRES
     226     * @throws IOException
     227     */
     228
     229    @Test
     230    public void testNoCacheHeadersMinimumExpiresLargerThanMaximum() throws IOException {
     231        noCacheHeadersMinimumExpires((int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() * 2));
     232    }
     233
     234    private void noCacheHeadersMinimumExpires(int minimumExpires) throws IOException {
     235        long testStart = System.currentTimeMillis();
     236        tileServer.stubFor(
     237                WireMock.get(WireMock.urlEqualTo("/test"))
     238                .willReturn(WireMock.aResponse()
     239                        .withBody("mock entry")
     240                        )
     241                );
     242        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", minimumExpires, false);
     243        assertExpirationAtLeast(testStart + minimumExpires, job);
     244        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     245        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
     246        // only one request to tile server should be made, second should come from cache
     247        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     248        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     249    }
     250
     251    /**
     252     * When tile server returns Expires header shorter than MINIMUM_EXPIRES, we should cache if for at least MINIMUM_EXPIRES
     253     * @throws IOException
     254     */
     255    @Test
     256    public void testShortExpire() throws IOException {
     257        long testStart = System.currentTimeMillis();
     258        long expires = TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get() / 2;
     259        tileServer.stubFor(
     260                WireMock.get(WireMock.urlEqualTo("/test"))
     261                .willReturn(WireMock.aResponse()
     262                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
     263                        .withBody("mock entry")
     264                        )
     265                );
     266        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
     267        assertExpirationAtLeast(testStart + TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get(), job);
     268        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     269        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
     270        // only one request to tile server should be made, second should come from cache
     271        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     272        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     273    }
     274
     275    private void assertExpirationAtLeast(long duration, TestCachedTileLoaderJob job) {
     276        assertTrue(
     277                "Expiration time shorter by " +
     278                        -1 * (job.getAttributes().getExpirationTime() - duration) +
     279                        " than expected",
     280                job.getAttributes().getExpirationTime() >= duration);
     281    }
     282
     283    private void assertExpirationAtMost(long duration, TestCachedTileLoaderJob job) {
     284        assertTrue(
     285                "Expiration time longer by " +
     286                        (job.getAttributes().getExpirationTime() - duration) +
     287                        " than expected",
     288                job.getAttributes().getExpirationTime() <= duration);
     289    }
     290
     291
     292    @Test
     293    public void testLongExpire() throws IOException {
     294        long testStart = System.currentTimeMillis();
     295        long expires = TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() * 2;
     296        tileServer.stubFor(
     297                WireMock.get(WireMock.urlEqualTo("/test"))
     298                .willReturn(WireMock.aResponse()
     299                        .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
     300                        .withBody("mock entry")
     301                        )
     302                );
     303        TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
     304        // give 1 second margin
     305        assertExpirationAtMost(testStart + TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() + TimeUnit.SECONDS.toMillis(1), job);
     306
     307        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     308        job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
     309        // only one request to tile server should be made, second should come from cache
     310        tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
     311        assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
     312    }
     313
    55314}
  • trunk/test/unit/org/openstreetmap/josm/data/imagery/WMTSTileSourceTest.java

    r12669 r13733  
    88import java.io.IOException;
    99import java.net.MalformedURLException;
     10import java.nio.file.Files;
     11import java.nio.file.Paths;
    1012import java.util.ArrayList;
    11 import java.util.Collection;
    12 
     13import java.util.Arrays;
     14import java.util.List;
     15
     16import org.junit.ClassRule;
    1317import org.junit.Ignore;
    14 import org.junit.Rule;
    1518import org.junit.Test;
    1619import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
     
    1922import org.openstreetmap.josm.data.Bounds;
    2023import org.openstreetmap.josm.data.coor.LatLon;
     24import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
     25import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
    2126import org.openstreetmap.josm.data.projection.Projections;
     27import org.openstreetmap.josm.spi.preferences.Config;
    2228import org.openstreetmap.josm.testutils.JOSMTestRules;
     29
     30import com.github.tomakehurst.wiremock.WireMockServer;
     31import com.github.tomakehurst.wiremock.client.WireMock;
    2332
    2433import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    2837 */
    2938public class WMTSTileSourceTest {
     39
     40    /**
     41     * Setup test.
     42     */
     43    @ClassRule
     44    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     45    public static JOSMTestRules test = new JOSMTestRules().preferences().platform();
    3046
    3147    private ImageryInfo testImageryTMS = new ImageryInfo("test imagery", "http://localhost", "tms", null, null);
     
    4460            "wmts/bug13975-multiple-tile-matrices-for-one-layer-projection.xml");
    4561
    46     /**
    47      * Setup test.
    48      */
    49     @Rule
    50     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    51     public JOSMTestRules test = new JOSMTestRules();
    5262
    5363    private static ImageryInfo getImagery(String path) {
    5464        try {
    55             return new ImageryInfo(
     65            ImageryInfo ret = new ImageryInfo(
    5666                    "test",
    5767                    new File(path).toURI().toURL().toString()
    5868                    );
     69            ret.setImageryType(ImageryType.WMTS);
     70            return ret;
    5971        } catch (MalformedURLException e) {
    6072            e.printStackTrace();
     
    6476
    6577    @Test
    66     public void testPseudoMercator() throws IOException {
     78    public void testPseudoMercator() throws IOException, WMTSGetCapabilitiesException {
    6779        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
    6880        WMTSTileSource testSource = new WMTSTileSource(testImageryPSEUDO_MERCATOR);
     
    94106
    95107    @Test
    96     public void testWALLONIE() throws IOException {
     108    public void testWALLONIE() throws IOException, WMTSGetCapabilitiesException {
    97109        Main.setProjection(Projections.getProjectionByCode("EPSG:31370"));
    98110        WMTSTileSource testSource = new WMTSTileSource(testImageryWALLONIE);
     
    114126    @Test
    115127    @Ignore("disable this test, needs further working") // XXX
    116     public void testWALLONIENoMatrixDimension() throws IOException {
     128    public void testWALLONIENoMatrixDimension() throws IOException, WMTSGetCapabilitiesException {
    117129        Main.setProjection(Projections.getProjectionByCode("EPSG:31370"));
    118130        WMTSTileSource testSource = new WMTSTileSource(getImagery("test/data/wmts/WMTSCapabilities-Wallonie-nomatrixdimension.xml"));
     
    138150
    139151    @Test
    140     public void testWIEN() throws IOException {
     152    public void testWIEN() throws IOException, WMTSGetCapabilitiesException {
    141153        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
    142154        WMTSTileSource testSource = new WMTSTileSource(testImageryWIEN);
     
    180192
    181193    @Test
    182     public void testGeoportalTOPOPL() throws IOException {
     194    public void testGeoportalTOPOPL() throws IOException, WMTSGetCapabilitiesException {
    183195        Main.setProjection(Projections.getProjectionByCode("EPSG:4326"));
    184196        WMTSTileSource testSource = new WMTSTileSource(testImageryTOPO_PL);
     
    202214
    203215    @Test
    204     public void testGeoportalORTOPL4326() throws IOException {
     216    public void testGeoportalORTOPL4326() throws IOException, WMTSGetCapabilitiesException {
    205217        Main.setProjection(Projections.getProjectionByCode("EPSG:4326"));
    206218        WMTSTileSource testSource = new WMTSTileSource(testImageryORTO_PL);
     
    211223
    212224    @Test
    213     public void testGeoportalORTOPL2180() throws IOException {
     225    public void testGeoportalORTOPL2180() throws IOException, WMTSGetCapabilitiesException {
    214226        Main.setProjection(Projections.getProjectionByCode("EPSG:2180"));
    215227        WMTSTileSource testSource = new WMTSTileSource(testImageryORTO_PL);
     
    221233
    222234    @Test
    223     public void testTicket12168() throws IOException {
     235    public void testTicket12168() throws IOException, WMTSGetCapabilitiesException {
    224236        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
    225237        WMTSTileSource testSource = new WMTSTileSource(testImagery12168);
     
    231243
    232244    @Test
    233     @Ignore("disabled as this needs user action") // XXX
    234245    public void testTwoTileSetsForOneProjection() throws Exception {
    235246        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
    236         WMTSTileSource testSource = new WMTSTileSource(testImageryOntario);
    237         testSource.initProjection(Main.getProjection());
    238         verifyTile(new LatLon(45.4105023, -75.7153702), testSource, 303751, 375502, 12);
    239         verifyTile(new LatLon(45.4601306, -75.7617187), testSource, 1186, 1466, 4);
    240     }
    241 
    242     @Test
    243     @Ignore("disabled as this needs user action") // XXX
     247        ImageryInfo ontario = getImagery(TestUtils.getTestDataRoot() + "wmts/WMTSCapabilities-Ontario.xml");
     248        ontario.setDefaultLayers(Arrays.asList(new DefaultLayer[] {
     249                new DefaultLayer(ImageryType.WMTS, "Basemap_Imagery_2014", null, "default028mm")
     250        }));
     251        WMTSTileSource testSource = new WMTSTileSource(ontario);
     252        testSource.initProjection(Main.getProjection());
     253        assertEquals(
     254                "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",
     255                testSource.getTileUrl(4, 2371, 2932));
     256        verifyTile(new LatLon(45.4601306, -75.7617187), testSource, 2372, 2932, 4);
     257        verifyTile(new LatLon(45.4602510, -75.7617187), testSource, 607232, 750591, 12);
     258    }
     259
     260    @Test
     261    public void testTwoTileSetsForOneProjectionSecondLayer() throws Exception {
     262        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
     263        ImageryInfo ontario = getImagery(TestUtils.getTestDataRoot() + "wmts/WMTSCapabilities-Ontario.xml");
     264        ontario.setDefaultLayers(Arrays.asList(new DefaultLayer[] {
     265                new DefaultLayer(ImageryType.WMTS, "Basemap_Imagery_2014", null, "GoogleMapsCompatible")
     266        }));
     267        WMTSTileSource testSource = new WMTSTileSource(ontario);
     268        testSource.initProjection(Main.getProjection());
     269        assertEquals(
     270                "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",
     271                testSource.getTileUrl(4, 2371, 2932));
     272        verifyMercatorTile(testSource, 74, 91, 8);
     273        verifyMercatorTile(testSource, 37952, 46912, 17);
     274    }
     275
     276    @Test
    244277    public void testManyLayersScrollbars() throws Exception {
    245278        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
     
    271304        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"));
    272305        ImageryInfo copy = new ImageryInfo(testMultipleTileMatrixForLayer);
    273         Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
    274         defaultLayers.add(new WMTSDefaultLayer("Mashhad_BaseMap_1", "default028mm"));
     306        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
     307        defaultLayers.add(new DefaultLayer(ImageryType.WMTS, "Mashhad_BaseMap_1", null, "default028mm"));
    275308        copy.setDefaultLayers(defaultLayers);
    276309        WMTSTileSource testSource = new WMTSTileSource(copy);
     
    286319     * Test WMTS dimension.
    287320     * @throws IOException if any I/O error occurs
     321     * @throws WMTSGetCapabilitiesException
    288322     */
    289323    @Test
    290     public void testDimension() throws IOException {
     324    public void testDimension() throws IOException, WMTSGetCapabilitiesException {
    291325        Main.setProjection(Projections.getProjectionByCode("EPSG:21781"));
    292326        ImageryInfo info = new ImageryInfo(testImageryGeoAdminCh);
    293         Collection<DefaultLayer> defaultLayers = new ArrayList<>(1);
    294         defaultLayers.add(new WMTSDefaultLayer("ch.are.agglomerationen_isolierte_staedte", "21781_26"));
     327        List<DefaultLayer> defaultLayers = new ArrayList<>(1);
     328        defaultLayers.add(new DefaultLayer(ImageryType.WMTS, "ch.are.agglomerationen_isolierte_staedte", null, "21781_26"));
    295329        info.setDefaultLayers(defaultLayers);
    296330        WMTSTileSource testSource = new WMTSTileSource(info);
     
    300334                testSource.getTileUrl(1, 2, 3)
    301335                );
     336    }
     337
     338    @Test
     339    public void testDefaultLayer() throws Exception {
     340        // https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/1.0.0/WMTSCapabilities.xml
     341        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
     342        String getCapabilitiesBody = new String(Files.readAllBytes(Paths.get(TestUtils.getTestDataRoot() + "wmts/getCapabilities-lots-of-layers.xml")), "UTF-8");
     343        // do not use withFileBody as it needs different directory layout :(
     344        getCapabilitiesMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(getCapabilitiesBody)));
     345        getCapabilitiesMock.start();
     346
     347        WireMockServer mapsMock = TestUtils.getWireMockServer();
     348        mapsMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(
     349                "<?xml version='1.0' encoding='UTF-8'?>\n" +
     350                "<imagery xmlns=\"http://josm.openstreetmap.de/maps-1.0\">\n" +
     351                "<entry>\n" +
     352                "<name>Landsat</name>\n" +
     353                "<id>landsat</id>\n" +
     354                "<type>wmts</type>\n" +
     355                "<url><![CDATA[" + getCapabilitiesMock.url("/getcapabilities.xml") + "]]></url>\n" +
     356                "<defaultLayers>" +
     357                "<layer name=\"GEOGRAPHICALGRIDSYSTEMS.MAPS\" />" +
     358                "</defaultLayers>" +
     359                "</entry>\n" +
     360                "</imagery>"
     361                )));
     362        mapsMock.start();
     363        Config.getPref().put("josm.url", mapsMock.url("/"));
     364
     365        ImageryLayerInfo.instance.loadDefaults(true, null, false);
     366
     367        assertEquals(1, ImageryLayerInfo.instance.getDefaultLayers().size());
     368        ImageryInfo wmtsImageryInfo = ImageryLayerInfo.instance.getDefaultLayers().get(0);
     369        assertEquals(1, wmtsImageryInfo.getDefaultLayers().size());
     370        assertEquals("GEOGRAPHICALGRIDSYSTEMS.MAPS", wmtsImageryInfo.getDefaultLayers().get(0).getLayerName());
     371        WMTSTileSource tileSource = new WMTSTileSource(wmtsImageryInfo);
     372        tileSource.initProjection(Projections.getProjectionByCode("EPSG:3857"));
     373        assertEquals("http://wxs.ign.fr/61fs25ymczag0c67naqvvmap/geoportail/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&"
     374                + "LAYER=GEOGRAPHICALGRIDSYSTEMS.MAPS"
     375                + "&STYLE=normal&FORMAT=image/jpeg&tileMatrixSet=PM&tileMatrix=1&tileRow=1&tileCol=1", tileSource.getTileUrl(1, 1, 1));
     376
    302377    }
    303378
  • trunk/test/unit/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayerTest.java

    r12636 r13733  
    120120            return new TileLoaderFactory() {
    121121                @Override
    122                 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers) {
     122                public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers,
     123                        long minimumExpiryTime) {
    123124                    return null;
    124125                }
  • trunk/test/unit/org/openstreetmap/josm/gui/layer/WMTSLayerTest.java

    r13710 r13733  
    2222    @Rule
    2323    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    24     public JOSMTestRules test = new JOSMTestRules().timeout(20000);
     24    public JOSMTestRules test = new JOSMTestRules().preferences().timeout(20000);
    2525
    2626    /**
  • trunk/test/unit/org/openstreetmap/josm/io/imagery/WMSImageryTest.java

    r13699 r13733  
    66
    77import java.io.IOException;
    8 import java.io.InputStream;
     8import java.nio.file.Files;
     9import java.nio.file.Path;
     10import java.nio.file.Paths;
     11import java.util.List;
    912
    1013import org.junit.Rule;
     
    1316import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
    1417import org.openstreetmap.josm.testutils.JOSMTestRules;
     18
     19import com.github.tomakehurst.wiremock.WireMockServer;
     20import com.github.tomakehurst.wiremock.client.WireMock;
    1521
    1622import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     
    2632    @Rule
    2733    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    28     public JOSMTestRules test = new JOSMTestRules();
     34    public JOSMTestRules test = new JOSMTestRules().platform().projection();
    2935
    3036    /**
     
    4955    @Test
    5056    public void testTicket15730() throws IOException, WMSGetCapabilitiesException {
    51         try (InputStream is = TestUtils.getRegressionDataStream(15730, "capabilities.xml")) {
    52             WMSImagery wms = new WMSImagery();
    53             wms.parseCapabilities(null, is);
    54             assertEquals(1, wms.getLayers().size());
    55             assertTrue(wms.getLayers().get(0).abstr.startsWith("South Carolina  NAIP Imagery 2017    Resolution: 100CM "));
    56         }
     57       WireMockServer wm = TestUtils.getWireMockServer(15730);
     58       wm.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBodyFile("capabilities.xml")));
     59       wm.start();
     60       WMSImagery wms = new WMSImagery(wm.url("capabilities.xml"));
     61       assertEquals(1, wms.getLayers().size());
     62       assertTrue(wms.getLayers().get(0).getAbstract().startsWith("South Carolina  NAIP Imagery 2017    Resolution: 100CM "));
     63       wm.shutdown();
     64    }
     65
     66    @Test
     67    public void testNestedLayers() throws Exception {
     68        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
     69        String getCapabilitiesBody = new String(Files.readAllBytes(Paths.get(TestUtils.getTestDataRoot() + "wms/mapa-um-warszawa-pl.xml")), "UTF-8");
     70        getCapabilitiesMock.stubFor(WireMock.get(WireMock.anyUrl()).willReturn(WireMock.aResponse().withBody(getCapabilitiesBody)));
     71        getCapabilitiesMock.start();
     72        WMSImagery wmsi = new WMSImagery(getCapabilitiesMock.url("/serwis"));
     73        assertEquals(1, wmsi.getLayers().size());
     74        assertEquals("Server WMS m.st. Warszawy", wmsi.getLayers().get(0).toString());
     75        assertEquals(202, wmsi.getLayers().get(0).getChildren().size());
    5776    }
    5877
     
    6483    @Test
    6584    public void testTicket16248() throws IOException, WMSGetCapabilitiesException {
    66         try (InputStream is = TestUtils.getRegressionDataStream(16248, "capabilities.xml")) {
    67             WMSImagery wms = new WMSImagery();
    68             wms.parseCapabilities(null, is);
    69             assertEquals("http://wms.hgis.cartomatic.pl/topo/3857/m25k", wms.getServiceUrl().toExternalForm());
    70         }
     85        Path capabilitiesPath = Paths.get(TestUtils.getRegressionDataFile(16248, "capabilities.xml"));
     86        WireMockServer getCapabilitiesMock = TestUtils.getWireMockServer();
     87        getCapabilitiesMock.stubFor(
     88                WireMock.get(WireMock.anyUrl())
     89                .willReturn(WireMock.aResponse().withBody(Files.readAllBytes(capabilitiesPath))));
     90        getCapabilitiesMock.start();
     91        WMSImagery wms = new WMSImagery(getCapabilitiesMock.url("any"));
     92        assertEquals("http://wms.hgis.cartomatic.pl/topo/3857/m25k", wms.buildRootUrl());
     93        assertEquals("wms.hgis.cartomatic.pl", wms.getLayers().get(0).getName());
     94        assertEquals("http://wms.hgis.cartomatic.pl/topo/3857/m25kFORMAT=image/png&TRANSPARENT=TRUE&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&"
     95                + "LAYERS=wms.hgis.cartomatic.pl&STYLES=&SRS={proj}&WIDTH={width}&HEIGHT={height}&BBOX={bbox}",
     96                wms.buildGetMapUrl(wms.getLayers(), (List<String>)null, true));
    7197    }
    7298}
     99
Note: See TracChangeset for help on using the changeset viewer.