Ticket #11216: jcs-tms-v11.patch

File jcs-tms-v11.patch, 100.5 KB (added by wiktorn, 4 years ago)

Corrected NullPointerException on "no tiles at this zoom level"

  • src/org/apache/commons

  • build.xml

    Property changes on: src/org/apache/commons
    ___________________________________________________________________
    Added: svn:externals
    ## -0,0 +1,2 ##
    +http://svn.apache.org/repos/asf/commons/proper/jcs/trunk/commons-jcs-core/src/main/java/org/apache/commons/jcs jcs
    +http://svn.apache.org/repos/asf/commons/proper/logging/trunk/src/main/java/org/apache/commons/logging logging
     
    220220            destdir="build" target="1.7" source="1.7" debug="on" includeantruntime="false" createMissingPackageInfoClass="false" encoding="iso-8859-1">
    221221            <!-- get rid of "internal proprietary API" warning -->
    222222            <compilerarg value="-XDignore.symbol.file"/>
     223                <exclude name="org/apache/commons/jcs/admin/**"/>
     224                <exclude name="org/apache/commons/jcs/auxiliary/disk/jdbc/**"/>
     225                <exclude name="org/apache/commons/jcs/auxiliary/remote/**"/>
     226                <exclude name="org/apache/commons/jcs/utils/servlet/**"/>
     227                <exclude name="org/apache/commons/logging/impl/AvalonLogger.java"/>
     228                <exclude name="org/apache/commons/logging/impl/Log4JLogger.java"/>
     229                <exclude name="org/apache/commons/logging/impl/LogKitLogger.java"/>
     230                <exclude name="org/apache/commons/logging/impl/ServletContextCleaner.java"/>
    223231        </javac>
    224232        <!-- JMapViewer/JOSM -->
    225233        <javac srcdir="${src.dir}" excludes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**,org/openstreetmap/gui/jmapviewer/Demo.java"
  • src/org/openstreetmap/gui/jmapviewer/TileController.java

     
    4545            tile.loadPlaceholderFromCache(tileCache);
    4646        }
    4747        if (!tile.isLoaded()) {
    48             jobDispatcher.addJob(tileLoader.createTileLoaderJob(tile));
     48            tileLoader.createTileLoaderJob(tile).submit();
    4949        }
    5050        return tile;
    5151    }
  • src/org/openstreetmap/gui/jmapviewer/Demo.java

     
    1010import java.awt.event.ItemListener;
    1111import java.awt.event.MouseAdapter;
    1212import java.awt.event.MouseEvent;
    13 import java.io.IOException;
    1413
    1514import javax.swing.JButton;
    1615import javax.swing.JCheckBox;
     
    101100            }
    102101        });
    103102        JComboBox<TileLoader> tileLoaderSelector;
    104         try {
    105             tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmFileCacheTileLoader(map()), new OsmTileLoader(map()) });
    106         } catch (IOException e) {
    107             tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmTileLoader(map()) });
    108         }
     103        tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmTileLoader(map()) });
    109104        tileLoaderSelector.addItemListener(new ItemListener() {
    110105            public void itemStateChanged(ItemEvent e) {
    111106                map().setTileLoader((TileLoader) e.getItem());
  • src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java

     
    8585            public Tile getTile() {
    8686                return tile;
    8787            }
     88
     89            @Override
     90            public void submit() {
     91                run();
     92
     93            }
    8894        };
    8995    }
    9096
  • src/org/openstreetmap/gui/jmapviewer/TMSFileCacheTileLoader.java

     
    1 // License: GPL. For details, see Readme.txt file.
    2 package org.openstreetmap.gui.jmapviewer;
    3 
    4 import java.io.File;
    5 import java.io.IOException;
    6 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
    7 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    8 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    9 
    10 /**
    11  * Reworked version of the OsmFileCacheTileLoader.
    12  *
    13  * When class OsmFileCacheTileLoader is no longer needed, it can be integrated
    14  * here and removed.
    15  */
    16 public class TMSFileCacheTileLoader extends OsmFileCacheTileLoader {
    17 
    18     public TMSFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws IOException {
    19         super(map, cacheDir);
    20     }
    21 
    22     @Override
    23     public TileJob createTileLoaderJob(final Tile tile) {
    24         return new TMSFileLoadJob(tile);
    25     }
    26 
    27     protected class TMSFileLoadJob extends FileLoadJob {
    28 
    29         public TMSFileLoadJob(Tile tile) {
    30             super(tile);
    31         }
    32 
    33         @Override
    34         protected File getTileFile() {
    35             return getDataFile(tile.getSource().getTileType());
    36         }
    37 
    38         @Override
    39         protected File getTagsFile() {
    40             return getDataFile(TAGS_FILE_EXT);
    41         }
    42 
    43         protected File getDataFile(String ext) {
    44             int nDigits = (int) Math.ceil(Math.log10(1 << tile.getZoom()));
    45             String x = String.format("%0" + nDigits + "d", tile.getXtile());
    46             String y = String.format("%0" + nDigits + "d", tile.getYtile());
    47             File path = new File(tileCacheDir, "z" + tile.getZoom());
    48             for (int i=0; i<nDigits; i++) {
    49                 String component = "x" + x.substring(i, i+1) + "y" + y.substring(i, i+1);
    50                 if (i == nDigits -1 ) {
    51                     component += "." + ext;
    52                 }
    53                 path = new File(path, component);
    54             }
    55             return path;
    56         }
    57     }
    58 
    59     @Override
    60     protected File getSourceCacheDir(TileSource source) {
    61         File dir = sourceCacheDirMap.get(source);
    62         if (dir == null) {
    63             String id = source.getId();
    64             if (id != null) {
    65                 dir = new File(cacheDirBase, id);
    66             } else {
    67                 dir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));
    68             }
    69             if (!dir.exists()) {
    70                 dir.mkdirs();
    71             }
    72         }
    73         return dir;
    74     }
    75 
    76 }
  • src/org/openstreetmap/gui/jmapviewer/JMapViewer.java

     
    985985    }
    986986
    987987    public void tileLoadingFinished(Tile tile, boolean success) {
     988        tile.setLoaded(success);
    988989        repaint();
    989990    }
    990991
  • src/org/openstreetmap/gui/jmapviewer/interfaces/CachedTileLoader.java

     
    55 * Interface that allow cleaning the tile cache without specifying exact type of loader
    66 */
    77public interface CachedTileLoader {
    8     public void clearCache(TileSource source);
    9     public void clearCache(TileSource source, TileClearController controller);
     8    public void clearCache();
    109}
  • src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java

     
    1717     * @return {@link Tile} to be handled
    1818     */
    1919    public Tile getTile();
     20
     21    /**
     22     * submits download job to backend.
     23     */
     24    void submit();
    2025}
  • src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java

     
    1 // License: GPL. For details, see Readme.txt file.
    2 package org.openstreetmap.gui.jmapviewer;
    3 
    4 import java.io.BufferedReader;
    5 import java.io.ByteArrayInputStream;
    6 import java.io.ByteArrayOutputStream;
    7 import java.io.File;
    8 import java.io.FileInputStream;
    9 import java.io.FileNotFoundException;
    10 import java.io.FileOutputStream;
    11 import java.io.IOException;
    12 import java.io.InputStream;
    13 import java.io.InputStreamReader;
    14 import java.io.OutputStreamWriter;
    15 import java.io.PrintWriter;
    16 import java.net.HttpURLConnection;
    17 import java.net.URL;
    18 import java.net.URLConnection;
    19 import java.nio.charset.Charset;
    20 import java.util.HashMap;
    21 import java.util.Map;
    22 import java.util.Map.Entry;
    23 import java.util.Random;
    24 import java.util.logging.Level;
    25 import java.util.logging.Logger;
    26 
    27 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
    28 import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;
    29 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
    30 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    31 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    32 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    33 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource.TileUpdate;
    34 
    35 /**
    36  * A {@link TileLoader} implementation that loads tiles from OSM via HTTP and
    37  * saves all loaded files in a directory located in the temporary directory.
    38  * If a tile is present in this file cache it will not be loaded from OSM again.
    39  *
    40  * @author Jan Peter Stotz
    41  * @author Stefan Zeller
    42  */
    43 public class OsmFileCacheTileLoader extends OsmTileLoader implements CachedTileLoader {
    44 
    45     private static final Logger log = FeatureAdapter.getLogger(OsmFileCacheTileLoader.class.getName());
    46 
    47     protected static final String TAGS_FILE_EXT = "tags";
    48 
    49     private static final Charset TAGS_CHARSET = Charset.forName("UTF-8");
    50 
    51     // Default expire time (i.e. maximum age of cached tile before refresh).
    52     // Used when the server does not send an expires or max-age value in the http header.
    53     protected static final long DEFAULT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 7; // 7 days
    54     // Limit for the max-age value send by the server.
    55     protected static final long EXPIRE_TIME_SERVER_LIMIT = 1000L * 60 * 60 * 24 * 28; // 4 weeks
    56     // Absolute expire time limit. Cached tiles that are older will not be used,
    57     // even if the refresh from the server fails.
    58     protected static final long ABSOLUTE_EXPIRE_TIME_LIMIT = Long.MAX_VALUE; // unlimited
    59 
    60     protected String cacheDirBase;
    61 
    62     protected final Map<TileSource, File> sourceCacheDirMap;
    63 
    64 
    65     public static File getDefaultCacheDir() throws SecurityException {
    66         String tempDir = null;
    67         String userName = System.getProperty("user.name");
    68         try {
    69             tempDir = System.getProperty("java.io.tmpdir");
    70         } catch (SecurityException e) {
    71             log.log(Level.WARNING,
    72                     "Failed to access system property ''java.io.tmpdir'' for security reasons. Exception was: "
    73                         + e.toString());
    74             throw e; // rethrow
    75         }
    76         try {
    77             if (tempDir == null)
    78                 throw new IOException("No temp directory set");
    79             String subDirName = "JMapViewerTiles";
    80             // On Linux/Unix systems we do not have a per user tmp directory.
    81             // Therefore we add the user name for getting a unique dir name.
    82             if (userName != null && userName.length() > 0) {
    83                 subDirName += "_" + userName;
    84             }
    85             File cacheDir = new File(tempDir, subDirName);
    86             return cacheDir;
    87         } catch (Exception e) {
    88         }
    89         return null;
    90     }
    91 
    92     /**
    93      * Create a OSMFileCacheTileLoader with given cache directory.
    94      * If cacheDir is not set or invalid, IOException will be thrown.
    95      * @param map the listener checking for tile load events (usually the map for display)
    96      * @param cacheDir directory to store cached tiles
    97      */
    98     public OsmFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws IOException  {
    99         super(map);
    100         if (cacheDir == null || (!cacheDir.exists() && !cacheDir.mkdirs()))
    101             throw new IOException("Cannot access cache directory");
    102 
    103         log.finest("Tile cache directory: " + cacheDir);
    104         cacheDirBase = cacheDir.getAbsolutePath();
    105         sourceCacheDirMap = new HashMap<>();
    106     }
    107 
    108     /**
    109      * Create a OSMFileCacheTileLoader with system property temp dir.
    110      * If not set an IOException will be thrown.
    111      * @param map the listener checking for tile load events (usually the map for display)
    112      */
    113     public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException, IOException {
    114         this(map, getDefaultCacheDir());
    115     }
    116 
    117     @Override
    118     public TileJob createTileLoaderJob(final Tile tile) {
    119         return new FileLoadJob(tile);
    120     }
    121 
    122     protected File getSourceCacheDir(TileSource source) {
    123         File dir = sourceCacheDirMap.get(source);
    124         if (dir == null) {
    125             dir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));
    126             if (!dir.exists()) {
    127                 dir.mkdirs();
    128             }
    129         }
    130         return dir;
    131     }
    132 
    133     protected class FileLoadJob implements TileJob {
    134         InputStream input = null;
    135 
    136         Tile tile;
    137         File tileCacheDir;
    138         File tileFile = null;
    139         File tagsFile = null;
    140         Long fileMtime = null;
    141         Long now = null; // current time in milliseconds (keep consistent value for the whole run)
    142 
    143         public FileLoadJob(Tile tile) {
    144             this.tile = tile;
    145         }
    146 
    147         @Override
    148         public Tile getTile() {
    149             return tile;
    150         }
    151 
    152         @Override
    153         public void run() {
    154             synchronized (tile) {
    155                 if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
    156                     return;
    157                 tile.loaded = false;
    158                 tile.error = false;
    159                 tile.loading = true;
    160             }
    161             now = System.currentTimeMillis();
    162             tileCacheDir = getSourceCacheDir(tile.getSource());
    163             tileFile = getTileFile();
    164             tagsFile = getTagsFile();
    165 
    166             loadTagsFromFile();
    167 
    168             if (isCacheValid() && (isNoTileAtZoom() || loadTileFromFile())) {
    169                 log.log(Level.FINE, "TMS - found in tile cache: {0}", tile);
    170                 tile.setLoaded(true);
    171                 listener.tileLoadingFinished(tile, true);
    172                 return;
    173             }
    174 
    175             TileJob job = new TileJob() {
    176 
    177                 @Override
    178                 public void run() {
    179                     if (loadOrUpdateTile()) {
    180                         tile.setLoaded(true);
    181                         listener.tileLoadingFinished(tile, true);
    182                     } else {
    183                         // failed to download - use old cache file if available
    184                         if (isNoTileAtZoom() || loadTileFromFile()) {
    185                             tile.setLoaded(true);
    186                             tile.error = false;
    187                             listener.tileLoadingFinished(tile, true);
    188                             log.log(Level.FINE, "TMS - found stale tile in cache: {0}", tile);
    189                         } else {
    190                             // failed completely
    191                             tile.setLoaded(true);
    192                             listener.tileLoadingFinished(tile, false);
    193                         }
    194                     }
    195                 }
    196                 @Override
    197                 public Tile getTile() {
    198                     return tile;
    199                 }
    200             };
    201             JobDispatcher.getInstance().addJob(job);
    202         }
    203 
    204         protected boolean loadOrUpdateTile() {
    205             try {
    206                 URLConnection urlConn = loadTileFromOsm(tile);
    207                 if (fileMtime != null && now - fileMtime <= ABSOLUTE_EXPIRE_TIME_LIMIT) {
    208                     switch (tile.getSource().getTileUpdate()) {
    209                     case IfModifiedSince:
    210                         urlConn.setIfModifiedSince(fileMtime);
    211                         break;
    212                     case LastModified:
    213                         if (!isOsmTileNewer(fileMtime)) {
    214                             log.log(Level.FINE, "TMS - LastModified test: local version is up to date: {0}", tile);
    215                             tileFile.setLastModified(now);
    216                             return true;
    217                         }
    218                         break;
    219                     default:
    220                         break;
    221                     }
    222                 }
    223                 if (tile.getSource().getTileUpdate() == TileUpdate.ETag || tile.getSource().getTileUpdate() == TileUpdate.IfNoneMatch) {
    224                     String fileETag = tile.getValue("etag");
    225                     if (fileETag != null) {
    226                         switch (tile.getSource().getTileUpdate()) {
    227                         case IfNoneMatch:
    228                             urlConn.addRequestProperty("If-None-Match", fileETag);
    229                             break;
    230                         case ETag:
    231                             if (hasOsmTileETag(fileETag)) {
    232                                 log.log(Level.FINE, "TMS - ETag test: local version is up to date: {0}", tile);
    233                                 tileFile.setLastModified(now);
    234                                 return true;
    235                             }
    236                         default:
    237                             break;
    238                         }
    239                     }
    240                     tile.putValue("etag", urlConn.getHeaderField("ETag"));
    241                 }
    242                 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) {
    243                     // If isModifiedSince or If-None-Match has been set
    244                     // and the server answers with a HTTP 304 = "Not Modified"
    245                     switch (tile.getSource().getTileUpdate()) {
    246                     case IfModifiedSince:
    247                         log.log(Level.FINE, "TMS - IfModifiedSince test: local version is up to date: {0}", tile);
    248                         break;
    249                     case IfNoneMatch:
    250                         log.log(Level.FINE, "TMS - IfNoneMatch test: local version is up to date: {0}", tile);
    251                         break;
    252                     default:
    253                         break;
    254                     }
    255                     loadTileFromFile();
    256                     tileFile.setLastModified(now);
    257                     return true;
    258                 }
    259 
    260                 loadTileMetadata(tile, urlConn);
    261                 saveTagsToFile();
    262 
    263                 if ("no-tile".equals(tile.getValue("tile-info")))
    264                 {
    265                     log.log(Level.FINE, "TMS - No tile: tile-info=no-tile: {0}", tile);
    266                     tile.setError("No tile at this zoom level");
    267                     return true;
    268                 } else {
    269                     for (int i = 0; i < 5; ++i) {
    270                         if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {
    271                             Thread.sleep(5000+(new Random()).nextInt(5000));
    272                             continue;
    273                         }
    274                         byte[] buffer = loadTileInBuffer(urlConn);
    275                         if (buffer != null) {
    276                             tile.loadImage(new ByteArrayInputStream(buffer));
    277                             saveTileToFile(buffer);
    278                             log.log(Level.FINE, "TMS - downloaded tile from server: {0}", tile.getUrl());
    279                             return true;
    280                         }
    281                     }
    282                 }
    283             } catch (Exception e) {
    284                 tile.setError(e.getMessage());
    285                 if (input == null) {
    286                     try {
    287                         log.log(Level.WARNING, "TMS - Failed downloading {0}: {1}", new Object[]{tile.getUrl(), e.getMessage()});
    288                         return false;
    289                     } catch(IOException i) {
    290                     }
    291                 }
    292             }
    293             log.log(Level.WARNING, "TMS - Failed downloading tile: {0}", tile);
    294             return false;
    295         }
    296 
    297         protected boolean isCacheValid() {
    298             Long expires = null;
    299             if (tileFile.exists()) {
    300                 fileMtime = tileFile.lastModified();
    301             } else if (tagsFile.exists()) {
    302                 fileMtime = tagsFile.lastModified();
    303             } else
    304                 return false;
    305 
    306             try {
    307                 expires = Long.parseLong(tile.getValue("expires"));
    308             } catch (NumberFormatException e) {}
    309 
    310             // check by expire date set by server
    311             if (expires != null && !expires.equals(0L)) {
    312                 // put a limit to the expire time (some servers send a value
    313                 // that is too large)
    314                 expires = Math.min(expires, fileMtime + EXPIRE_TIME_SERVER_LIMIT);
    315                 if (now > expires) {
    316                     log.log(Level.FINE, "TMS - Tile has expired -> not valid {0}", tile);
    317                     return false;
    318                 }
    319             } else {
    320                 // check by file modification date
    321                 if (now - fileMtime > DEFAULT_EXPIRE_TIME) {
    322                     log.log(Level.FINE, "TMS - Tile has expired, maximum file age reached {0}", tile);
    323                     return false;
    324                 }
    325             }
    326             return true;
    327         }
    328 
    329         protected boolean isNoTileAtZoom() {
    330             if ("no-tile".equals(tile.getValue("tile-info"))) {
    331                 // do not remove file - keep the information, that there is no tile, for further requests
    332                 // the code above will check, if this information is still valid
    333                 log.log(Level.FINE, "TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
    334                 tile.setError("No tile at this zoom level");
    335                 return true;
    336             }
    337             return false;
    338         }
    339 
    340         protected boolean loadTileFromFile() {
    341             if (!tileFile.exists())
    342                 return false;
    343 
    344             try (FileInputStream fin = new FileInputStream(tileFile)) {
    345                 if (fin.available() == 0)
    346                     throw new IOException("File empty");
    347                 tile.loadImage(fin);
    348                 return true;
    349             } catch (Exception e) {
    350                 log.log(Level.WARNING, "TMS - Error while loading image from tile cache: {0}; {1}", new Object[]{e.getMessage(), tile});
    351                 tileFile.delete();
    352                 if (tagsFile.exists()) {
    353                     tagsFile.delete();
    354                 }
    355                 tileFile = null;
    356                 fileMtime = null;
    357             }
    358             return false;
    359         }
    360 
    361         protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException {
    362             input = urlConn.getInputStream();
    363             try {
    364                 ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available());
    365                 byte[] buffer = new byte[2048];
    366                 boolean finished = false;
    367                 do {
    368                     int read = input.read(buffer);
    369                     if (read >= 0) {
    370                         bout.write(buffer, 0, read);
    371                     } else {
    372                         finished = true;
    373                     }
    374                 } while (!finished);
    375                 if (bout.size() == 0)
    376                     return null;
    377                 return bout.toByteArray();
    378             } finally {
    379                 input.close();
    380                 input = null;
    381             }
    382         }
    383 
    384         /**
    385          * Performs a <code>HEAD</code> request for retrieving the
    386          * <code>LastModified</code> header value.
    387          *
    388          * Note: This does only work with servers providing the
    389          * <code>LastModified</code> header:
    390          * <ul>
    391          * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.CycleMap} - supported</li>
    392          * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik} - not supported</li>
    393          * </ul>
    394          *
    395          * @param fileAge time of the
    396          * @return <code>true</code> if the tile on the server is newer than the
    397          *         file
    398          * @throws IOException
    399          */
    400         protected boolean isOsmTileNewer(long fileAge) throws IOException {
    401             URL url;
    402             url = new URL(tile.getUrl());
    403             HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
    404             prepareHttpUrlConnection(urlConn);
    405             urlConn.setRequestMethod("HEAD");
    406             urlConn.setReadTimeout(30000); // 30 seconds read timeout
    407             // System.out.println("Tile age: " + new
    408             // Date(urlConn.getLastModified()) + " / "
    409             // + new Date(fileMtime));
    410             long lastModified = urlConn.getLastModified();
    411             if (lastModified == 0)
    412                 return true; // no LastModified time returned
    413             return (lastModified > fileAge);
    414         }
    415 
    416         protected boolean hasOsmTileETag(String eTag) throws IOException {
    417             URL url;
    418             url = new URL(tile.getUrl());
    419             HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
    420             prepareHttpUrlConnection(urlConn);
    421             urlConn.setRequestMethod("HEAD");
    422             urlConn.setReadTimeout(30000); // 30 seconds read timeout
    423             // System.out.println("Tile age: " + new
    424             // Date(urlConn.getLastModified()) + " / "
    425             // + new Date(fileMtime));
    426             String osmETag = urlConn.getHeaderField("ETag");
    427             if (osmETag == null)
    428                 return true;
    429             return (osmETag.equals(eTag));
    430         }
    431 
    432         protected File getTileFile() {
    433             return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."
    434                     + tile.getSource().getTileType());
    435         }
    436 
    437         protected File getTagsFile() {
    438             return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."
    439                     + TAGS_FILE_EXT);
    440         }
    441 
    442         protected void saveTileToFile(byte[] rawData) {
    443             File file = getTileFile();
    444             file.getParentFile().mkdirs();
    445             try (FileOutputStream f = new FileOutputStream(file)) {
    446                 f.write(rawData);
    447             } catch (Exception e) {
    448                 log.log(Level.SEVERE, "Failed to save tile content: {0}", e.getLocalizedMessage());
    449             }
    450         }
    451 
    452         protected void saveTagsToFile() {
    453             File tagsFile = getTagsFile();
    454             tagsFile.getParentFile().mkdirs();
    455             if (tile.getMetadata() == null) {
    456                 tagsFile.delete();
    457                 return;
    458             }
    459             try (PrintWriter f = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tagsFile), TAGS_CHARSET))) {
    460                 for (Entry<String, String> entry : tile.getMetadata().entrySet()) {
    461                     f.println(entry.getKey() + "=" + entry.getValue());
    462                 }
    463             } catch (Exception e) {
    464                 System.err.println("Failed to save tile tags: " + e.getLocalizedMessage());
    465             }
    466         }
    467 
    468         protected boolean loadTagsFromFile() {
    469             File tagsFile = getTagsFile();
    470             try (BufferedReader f = new BufferedReader(new InputStreamReader(new FileInputStream(tagsFile), TAGS_CHARSET))) {
    471                 for (String line = f.readLine(); line != null; line = f.readLine()) {
    472                     final int i = line.indexOf('=');
    473                     if (i == -1 || i == 0) {
    474                         System.err.println("Malformed tile tag in file '" + tagsFile.getName() + "':" + line);
    475                         continue;
    476                     }
    477                     tile.putValue(line.substring(0,i),line.substring(i+1));
    478                 }
    479             } catch (FileNotFoundException e) {
    480             } catch (Exception e) {
    481                 System.err.println("Failed to load tile tags: " + e.getLocalizedMessage());
    482             }
    483 
    484             return true;
    485         }
    486     }
    487 
    488     public String getCacheDirBase() {
    489         return cacheDirBase;
    490     }
    491 
    492     public void setTileCacheDir(String tileCacheDir) {
    493         File dir = new File(tileCacheDir);
    494         dir.mkdirs();
    495         this.cacheDirBase = dir.getAbsolutePath();
    496     }
    497 
    498     @Override
    499     public void clearCache(TileSource source) {
    500         clearCache(source, null);
    501     }
    502 
    503     @Override
    504     public void clearCache(TileSource source, TileClearController controller) {
    505         File dir = getSourceCacheDir(source);
    506         if (dir != null) {
    507             if (controller != null) controller.initClearDir(dir);
    508             if (dir.isDirectory()) {
    509                 File[] files = dir.listFiles();
    510                 if (controller != null) controller.initClearFiles(files);
    511                 for (File file : files) {
    512                     if (controller != null && controller.cancel()) return;
    513                     file.delete();
    514                     if (controller != null) controller.fileDeleted(file);
    515                 }
    516             }
    517             dir.delete();
    518         }
    519         if (controller != null) controller.clearFinished();
    520     }
    521 }
  • src/org/openstreetmap/josm/Main.java

     
    6767import org.openstreetmap.josm.data.ProjectionBounds;
    6868import org.openstreetmap.josm.data.UndoRedoHandler;
    6969import org.openstreetmap.josm.data.ViewportData;
     70import org.openstreetmap.josm.data.cache.JCSCacheManager;
    7071import org.openstreetmap.josm.data.coor.CoordinateFormat;
    7172import org.openstreetmap.josm.data.coor.LatLon;
    7273import org.openstreetmap.josm.data.osm.DataSet;
     
    10881089     * @since 3378
    10891090     */
    10901091    public static boolean exitJosm(boolean exit, int exitCode) {
     1092        JCSCacheManager.shutdown();
    10911093        if (Main.saveUnsavedModifications()) {
    10921094            geometry.remember("gui.geometry");
    10931095            if (map != null) {
  • src/org/openstreetmap/josm/gui/layer/TMSLayer.java

     
    2121import java.net.URL;
    2222import java.util.ArrayList;
    2323import java.util.Collections;
    24 import java.util.HashSet;
     24import java.util.HashMap;
    2525import java.util.LinkedList;
    2626import java.util.List;
    2727import java.util.Map;
    28 import java.util.Map.Entry;
    2928import java.util.Scanner;
    30 import java.util.Set;
    3129import java.util.concurrent.Callable;
    3230import java.util.regex.Matcher;
    3331import java.util.regex.Pattern;
     
    4139
    4240import org.openstreetmap.gui.jmapviewer.AttributionSupport;
    4341import org.openstreetmap.gui.jmapviewer.Coordinate;
    44 import org.openstreetmap.gui.jmapviewer.JobDispatcher;
    4542import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    46 import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
    4743import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    48 import org.openstreetmap.gui.jmapviewer.TMSFileCacheTileLoader;
    4944import org.openstreetmap.gui.jmapviewer.Tile;
    5045import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
    51 import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;
     46import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
     47import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    5248import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    5349import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    5450import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
     
    6359import org.openstreetmap.josm.data.coor.LatLon;
    6460import org.openstreetmap.josm.data.imagery.ImageryInfo;
    6561import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
     62import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    6663import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    6764import org.openstreetmap.josm.data.preferences.BooleanProperty;
    6865import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    7572import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    7673import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    7774import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    78 import org.openstreetmap.josm.gui.progress.ProgressMonitor.CancelListener;
    7975import org.openstreetmap.josm.io.CacheCustomContent;
    8076import org.openstreetmap.josm.io.OsmTransferException;
    8177import org.openstreetmap.josm.io.UTFInputStreamReader;
     
    108106    public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
    109107    //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
    110108    public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
    111     public static final IntegerProperty PROP_TMS_JOBS = new IntegerProperty("tmsloader.maxjobs", 25);
    112109    public static final StringProperty PROP_TILECACHE_DIR;
    113110
    114111    static {
     
    122119    }
    123120
    124121    public interface TileLoaderFactory {
    125         OsmTileLoader makeTileLoader(TileLoaderListener listener);
     122        TileLoader makeTileLoader(TileLoaderListener listener);
     123        TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
    126124    }
    127125
    128     protected MemoryTileCache tileCache;
     126    protected TileCache tileCache;
    129127    protected TileSource tileSource;
    130     protected OsmTileLoader tileLoader;
     128    protected TileLoader tileLoader;
     129
    131130
    132131    public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
    133132        @Override
    134         public OsmTileLoader makeTileLoader(TileLoaderListener listener) {
    135             String cachePath = TMSLayer.PROP_TILECACHE_DIR.get();
    136             if (cachePath != null && !cachePath.isEmpty()) {
    137                 try {
    138                     OsmFileCacheTileLoader loader;
    139                     loader = new TMSFileCacheTileLoader(listener, new File(cachePath));
    140                     loader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
    141                     return loader;
    142                 } catch (IOException e) {
    143                     Main.warn(e);
    144                 }
     133        public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
     134            Map<String, String> headers = new HashMap<>();
     135            headers.put("User-Agent", Version.getInstance().getFullAgentString());
     136            headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
     137            if (inputHeaders != null)
     138                headers.putAll(inputHeaders);
     139
     140            try {
     141                return new TMSCachedTileLoader(listener, "TMS",
     142                        Main.pref.getInteger("socket.timeout.connect",15) * 1000,
     143                        Main.pref.getInteger("socket.timeout.read", 30) * 1000,
     144                        headers,
     145                        PROP_TILECACHE_DIR.get());
     146            } catch (IOException e) {
     147                Main.warn(e);
    145148            }
    146149            return null;
    147150        }
     151
     152        @Override
     153        public TileLoader makeTileLoader(TileLoaderListener listener) {
     154            return makeTileLoader(listener, null);
     155        }
    148156    };
    149157
    150158    /**
    151159     * Plugins that wish to set custom tile loader should call this method
    152160     */
     161
    153162    public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
    154163        TMSLayer.loaderFactory = loaderFactory;
    155164    }
    156165
    157     private Set<Tile> tileRequestsOutstanding = new HashSet<>();
    158 
    159166    @Override
    160167    public synchronized void tileLoadingFinished(Tile tile, boolean success) {
    161168        if (tile.hasError()) {
     
    165172        if (sharpenLevel != 0 && success) {
    166173            tile.setImage(sharpenImage(tile.getImage()));
    167174        }
    168         tile.setLoaded(true);
     175        tile.setLoaded(success);
    169176        needRedraw = true;
    170177        if (Main.map != null) {
    171178            Main.map.repaint(100);
    172179        }
    173         tileRequestsOutstanding.remove(tile);
    174180        if (Main.isDebugEnabled()) {
    175181            Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
    176182        }
    177183    }
    178184
    179     private static class TmsTileClearController implements TileClearController, CancelListener {
    180 
    181         private final ProgressMonitor monitor;
    182         private boolean cancel = false;
    183 
    184         public TmsTileClearController(ProgressMonitor monitor) {
    185             this.monitor = monitor;
    186             this.monitor.addCancelListener(this);
    187         }
    188 
    189         @Override
    190         public void initClearDir(File dir) {
    191         }
    192 
    193         @Override
    194         public void initClearFiles(File[] files) {
    195             monitor.setTicksCount(files.length);
    196             monitor.setTicks(0);
    197         }
    198 
    199         @Override
    200         public boolean cancel() {
    201             return cancel;
    202         }
    203 
    204         @Override
    205         public void fileDeleted(File file) {
    206             monitor.setTicks(monitor.getTicks()+1);
    207         }
    208 
    209         @Override
    210         public void clearFinished() {
    211             monitor.finishTask();
    212         }
    213 
    214         @Override
    215         public void operationCanceled() {
    216             cancel = true;
    217         }
    218     }
    219 
    220185    /**
    221186     * Clears the tile cache.
    222187     *
     
    225190     * method.
    226191     *
    227192     * @param monitor
    228      * @see MemoryTileCache#clear()
    229193     * @see OsmFileCacheTileLoader#clearCache(org.openstreetmap.gui.jmapviewer.interfaces.TileSource, org.openstreetmap.gui.jmapviewer.interfaces.TileClearController)
    230194     */
    231195    void clearTileCache(ProgressMonitor monitor) {
    232196        tileCache.clear();
    233197        if (tileLoader instanceof CachedTileLoader) {
    234             ((CachedTileLoader)tileLoader).clearCache(tileSource, new TmsTileClearController(monitor));
     198            ((CachedTileLoader)tileLoader).clearCache();
    235199        }
    236200    }
    237201
     
    415379
    416380        currentZoomLevel = getBestZoom();
    417381
    418         tileCache = new MemoryTileCache();
    419 
    420         tileLoader = loaderFactory.makeTileLoader(this);
    421         if (tileLoader == null) {
    422             tileLoader = new OsmTileLoader(this);
    423         }
    424         tileLoader.timeoutConnect = Main.pref.getInteger("socket.timeout.connect",15) * 1000;
    425         tileLoader.timeoutRead = Main.pref.getInteger("socket.timeout.read", 30) * 1000;
     382        Map<String, String> headers = null;
    426383        if (tileSource instanceof TemplatedTMSTileSource) {
    427             for(Entry<String, String> e : ((TemplatedTMSTileSource)tileSource).getHeaders().entrySet()) {
    428                 tileLoader.headers.put(e.getKey(), e.getValue());
    429             }
     384            headers = (((TemplatedTMSTileSource)tileSource).getHeaders());
    430385        }
    431         tileLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
    432     }
    433386
    434     @Override
    435     public void setOffset(double dx, double dy) {
    436         super.setOffset(dx, dy);
    437         needRedraw = true;
     387        // FIXME: tileCache = new MemoryTileCache();
     388        tileLoader = loaderFactory.makeTileLoader(this, headers);
     389        if (tileLoader instanceof TMSCachedTileLoader) {
     390            tileCache = (TileCache) tileLoader;
     391        } else {
     392            tileCache = new MemoryTileCache();
     393        }
     394        if (tileLoader == null)
     395            tileLoader = new OsmTileLoader(this);
    438396    }
    439397
    440398    /**
     
    471429        return intResult;
    472430    }
    473431
    474     /**
    475      * Function to set the maximum number of workers for tile loading to the value defined
    476      * in preferences.
    477      */
    478     public static void setMaxWorkers() {
    479         JobDispatcher.setMaxWorkers(PROP_TMS_JOBS.get());
    480         JobDispatcher.getInstance().setLIFO(true);
    481     }
    482 
    483432    @SuppressWarnings("serial")
    484433    public TMSLayer(ImageryInfo info) {
    485434        super(info);
    486435
    487         setMaxWorkers();
    488436        if(!isProjectionSupported(Main.getProjection())) {
    489437            JOptionPane.showMessageDialog(Main.parent,
    490                 tr("TMS layers do not support the projection {0}.\n{1}\n"
    491                 + "Change the projection or remove the layer.",
    492                 Main.getProjection().toCode(), nameSupportedProjections()),
    493                 tr("Warning"),
    494                 JOptionPane.WARNING_MESSAGE);
     438                    tr("TMS layers do not support the projection {0}.\n{1}\n"
     439                            + "Change the projection or remove the layer.",
     440                            Main.getProjection().toCode(), nameSupportedProjections()),
     441                            tr("Warning"),
     442                            JOptionPane.WARNING_MESSAGE);
    495443        }
    496444
    497445        setBackgroundLayer(true);
     
    684632            Main.debug("zoomChanged(): " + currentZoomLevel);
    685633        }
    686634        needRedraw = true;
    687         JobDispatcher.getInstance().cancelOutstandingJobs();
    688         tileRequestsOutstanding.clear();
    689635    }
    690636
    691637    int getMaxZoomLvl() {
     
    770716     * are temporary only and intentionally not inserted
    771717     * into the tileCache.
    772718     */
    773     synchronized Tile tempCornerTile(Tile t) {
     719    Tile tempCornerTile(Tile t) {
    774720        int x = t.getXtile() + 1;
    775721        int y = t.getYtile() + 1;
    776722        int zoom = t.getZoom();
     
    780726        return new Tile(tileSource, x, y, zoom);
    781727    }
    782728
    783     synchronized Tile getOrCreateTile(int x, int y, int zoom) {
     729    Tile getOrCreateTile(int x, int y, int zoom) {
    784730        Tile tile = getTile(x, y, zoom);
    785731        if (tile == null) {
    786732            tile = new Tile(tileSource, x, y, zoom);
     
    794740     * This can and will return null for tiles that are not
    795741     * already in the cache.
    796742     */
    797     synchronized Tile getTile(int x, int y, int zoom) {
     743    Tile getTile(int x, int y, int zoom) {
    798744        int max = (1 << zoom);
    799745        if (x < 0 || x >= max || y < 0 || y >= max)
    800746            return null;
    801747        return tileCache.getTile(tileSource, x, y, zoom);
    802748    }
    803749
    804     synchronized boolean loadTile(Tile tile, boolean force) {
     750    boolean loadTile(Tile tile, boolean force) {
    805751        if (tile == null)
    806752            return false;
    807         if (!force && (tile.hasError() || tile.isLoaded()))
     753        if (!force && (tile.isLoaded() || tile.hasError()))
    808754            return false;
    809755        if (tile.isLoading())
    810756            return false;
    811         if (tileRequestsOutstanding.contains(tile))
    812             return false;
    813         tileRequestsOutstanding.add(tile);
    814         JobDispatcher.getInstance().addJob(tileLoader.createTileLoaderJob(tile));
     757        tileLoader.createTileLoaderJob(tile).submit();
    815758        return true;
    816759    }
    817760
     
    12681211        public TileSet getTileSet(int zoom) {
    12691212            if (zoom < minZoom)
    12701213                return nullTileSet;
    1271             TileSet ts = tileSets[zoom-minZoom];
    1272             if (ts == null) {
    1273                 ts = new TileSet(topLeft, botRight, zoom);
    1274                 tileSets[zoom-minZoom] = ts;
     1214            synchronized (tileSets) {
     1215                TileSet ts = tileSets[zoom-minZoom];
     1216                if (ts == null) {
     1217                    ts = new TileSet(topLeft, botRight, zoom);
     1218                    tileSets[zoom-minZoom] = ts;
     1219                }
     1220                return ts;
    12751221            }
    1276             return ts;
    12771222        }
     1223
    12781224        public TileSetInfo getTileSetInfo(int zoom) {
    12791225            if (zoom < minZoom)
    12801226                return new TileSetInfo();
    1281             TileSetInfo tsi = tileSetInfos[zoom-minZoom];
    1282             if (tsi == null) {
    1283                 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
    1284                 tileSetInfos[zoom-minZoom] = tsi;
     1227            synchronized (tileSetInfos) {
     1228                TileSetInfo tsi = tileSetInfos[zoom-minZoom];
     1229                if (tsi == null) {
     1230                    tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
     1231                    tileSetInfos[zoom-minZoom] = tsi;
     1232                }
     1233                return tsi;
    12851234            }
    1286             return tsi;
    12871235        }
    12881236    }
    12891237
    12901238    @Override
    12911239    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
    1292         //long start = System.currentTimeMillis();
    12931240        EastNorth topLeft = mv.getEastNorth(0, 0);
    12941241        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    12951242
     
    14351382            myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
    14361383            myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
    14371384            myDrawString(g, tr("Best zoom: {0}", Math.log(getScaleFactor(1))/Math.log(2)/2+1), 50, 185);
     1385            if(tileLoader instanceof TMSCachedTileLoader) {
     1386                TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
     1387                int offset = 185;
     1388                for(String part: cachedTileLoader.getStats().split("\n")) {
     1389                    myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
     1390                }
     1391
     1392            }
    14381393        }
    14391394    }
    14401395
  • src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java

     
    1111import javax.swing.JSpinner;
    1212import javax.swing.SpinnerNumberModel;
    1313
     14import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
     15import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
    1416import org.openstreetmap.josm.gui.layer.TMSLayer;
    15 import org.openstreetmap.josm.tools.GBC;
    1617import org.openstreetmap.josm.gui.widgets.JosmTextField;
     18import org.openstreetmap.josm.tools.GBC;
    1719
    1820/**
    1921 * {@code JPanel} giving access to TMS settings.
     
    2830    private final JSpinner maxZoomLvl;
    2931    private final JCheckBox addToSlippyMapChosser = new JCheckBox();
    3032    private final JosmTextField tilecacheDir = new JosmTextField();
     33    private final JSpinner maxElementsOnDisk;
     34    private final JSpinner maxConcurrentDownloads;
     35
    3136
    3237    /**
    3338     * Constructs a new {@code TMSSettingsPanel}.
     
    3641        super(new GridBagLayout());
    3742        minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MIN_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
    3843        maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MAX_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1));
     44        maxElementsOnDisk = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get().intValue(), 0, Integer.MAX_VALUE, 1));
     45        maxConcurrentDownloads = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1));
    3946
    4047        add(new JLabel(tr("Auto zoom by default: ")), GBC.std());
    4148        add(GBC.glue(5, 0), GBC.std());
     
    6067        add(new JLabel(tr("Tile cache directory: ")), GBC.std());
    6168        add(GBC.glue(5, 0), GBC.std());
    6269        add(tilecacheDir, GBC.eol().fill(GBC.HORIZONTAL));
     70
     71        add(new JLabel(tr("Maximum concurrent downloads: ")), GBC.std());
     72        add(GBC.glue(5, 0), GBC.std());
     73        add(maxConcurrentDownloads, GBC.eol());
     74
     75        add(new JLabel(tr("Maximum elements in disk cache: ")), GBC.std());
     76        add(GBC.glue(5, 0), GBC.std());
     77        add(this.maxElementsOnDisk, GBC.eol());
     78
    6379    }
    64    
     80
    6581    /**
    6682     * Loads the TMS settings.
    6783     */
     
    7288        this.maxZoomLvl.setValue(TMSLayer.getMaxZoomLvl(null));
    7389        this.minZoomLvl.setValue(TMSLayer.getMinZoomLvl(null));
    7490        this.tilecacheDir.setText(TMSLayer.PROP_TILECACHE_DIR.get());
     91        this.maxElementsOnDisk.setValue(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get());
     92        this.maxConcurrentDownloads.setValue(TMSCachedTileLoaderJob.THREAD_LIMIT.get());
    7593    }
    76    
     94
    7795    /**
    7896     * Saves the TMS settings.
    7997     * @return true when restart is required
    8098     */
    8199    public boolean saveSettings() {
    82100        boolean restartRequired = false;
    83        
     101
    84102        if (TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get() != this.addToSlippyMapChosser.isSelected()) {
    85103            restartRequired = true;
    86104        }
     
    89107        TMSLayer.PROP_DEFAULT_AUTOLOAD.put(this.autoloadTiles.isSelected());
    90108        TMSLayer.setMaxZoomLvl((Integer)this.maxZoomLvl.getValue());
    91109        TMSLayer.setMinZoomLvl((Integer)this.minZoomLvl.getValue());
    92         TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());
    93        
     110
     111        TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.put((Integer) this.maxElementsOnDisk.getValue());
     112
     113        if (!TMSCachedTileLoaderJob.THREAD_LIMIT.get().equals(this.maxConcurrentDownloads.getValue())) {
     114            restartRequired = true;
     115            TMSCachedTileLoaderJob.THREAD_LIMIT.put((Integer) this.maxConcurrentDownloads.getValue());
     116        }
     117
     118        if (!TMSLayer.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) {
     119            restartRequired = true;
     120            TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText());
     121        }
     122
    94123        return restartRequired;
    95124    }
    96125}
  • src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java

     
    1111import java.util.ArrayList;
    1212import java.util.Arrays;
    1313import java.util.Collections;
     14import java.util.HashMap;
    1415import java.util.HashSet;
    1516import java.util.List;
     17import java.util.Map;
    1618import java.util.Set;
    1719import java.util.concurrent.CopyOnWriteArrayList;
    1820
     
    2527import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    2628import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    2729import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
     30import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    2831import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    2932import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
    3033import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
     
    113116    private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
    114117    public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
    115118
    116     private OsmTileLoader cachedLoader;
     119    private TileLoader cachedLoader;
    117120    private OsmTileLoader uncachedLoader;
    118121
    119122    private final SizeButton iSizeButton;
     
    131134        debug = Main.isDebugEnabled();
    132135        SpringLayout springLayout = new SpringLayout();
    133136        setLayout(springLayout);
    134         TMSLayer.setMaxWorkers();
    135         cachedLoader = TMSLayer.loaderFactory.makeTileLoader(this);
     137
     138        Map<String, String> headers = new HashMap<>();
     139        headers.put("User-Agent", Version.getInstance().getFullAgentString());
     140
     141        cachedLoader = TMSLayer.loaderFactory.makeTileLoader(this, headers);
    136142
    137143        uncachedLoader = new OsmTileLoader(this);
    138         uncachedLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
     144        uncachedLoader.headers.putAll(headers);
    139145        setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols",false));
    140146        setMapMarkerVisible(false);
    141147        setMinimumSize(new Dimension(350, 350 / 2));
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.io.ByteArrayInputStream;
     5import java.io.IOException;
     6import java.net.URL;
     7import java.util.Map;
     8import java.util.concurrent.Executor;
     9import java.util.concurrent.LinkedBlockingDeque;
     10import java.util.concurrent.ThreadPoolExecutor;
     11import java.util.concurrent.TimeUnit;
     12import java.util.logging.Level;
     13import java.util.logging.Logger;
     14
     15import org.apache.commons.jcs.access.behavior.ICacheAccess;
     16import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
     17import org.openstreetmap.gui.jmapviewer.Tile;
     18import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     19import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     20import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     21import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
     22import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     23import org.openstreetmap.josm.data.cache.CacheEntry;
     24import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
     25import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     26import org.openstreetmap.josm.data.preferences.IntegerProperty;
     27
     28/**
     29 * @author Wiktor Niesiobędzki
     30 *
     31 * Class bridging TMS requests to JCS cache requests
     32 *
     33 */
     34public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener  {
     35    private static final Logger log = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName());
     36    private Tile tile;
     37    private TileLoaderListener listener;
     38    private volatile URL url;
     39
     40    /**
     41     * overrides the THREAD_LIMIT in superclass, as we want to have separate limit and pool for TMS
     42     */
     43    public static IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.tms.tmsloader.maxjobs", 25);
     44    /**
     45     * separate from JCS thread pool for TMS loader, so we can have different thread pools for default JCS
     46     * and for TMS imagery
     47     */
     48    private static ThreadPoolExecutor DOWNLOAD_JOB_DISPATCHER = new ThreadPoolExecutor(
     49            THREAD_LIMIT.get().intValue(), // keep the thread number constant
     50            THREAD_LIMIT.get().intValue(), // do not this number of threads
     51            30, // keepalive for thread
     52            TimeUnit.SECONDS,
     53            // make queue of LIFO type - so recently requested tiles will be loaded first (assuming that these are which user is waiting to see)
     54            new LinkedBlockingDeque<Runnable>(5) {
     55                /* keep the queue size fairly small, we do not want to
     56                 download a lot of tiles, that user is not seeing anyway */
     57                @Override
     58                public boolean offer(Runnable t) {
     59                    return super.offerFirst(t);
     60                }
     61
     62                @Override
     63                public Runnable remove() {
     64                    return super.removeFirst();
     65                }
     66            }
     67            );
     68
     69    /**
     70     * Constructor for creating a job, to get a specific tile from cache
     71     * @param listener
     72     * @param tile to be fetched from cache
     73     * @param cache object
     74     * @param connectTimeout when connecting to remote resource
     75     * @param readTimeout when connecting to remote resource
     76     * @param headers to be sent together with request
     77     */
     78    public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile, ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout,
     79            Map<String, String> headers) {
     80        super(cache, connectTimeout, readTimeout, headers);
     81        this.tile = tile;
     82        this.listener = listener;
     83    }
     84
     85    @Override
     86    public Tile getTile() {
     87        return getCachedTile();
     88    }
     89
     90    @Override
     91    public String getCacheKey() {
     92        if (tile != null)
     93            return tile.getKey();
     94        return null;
     95    }
     96
     97    /*
     98     *  this doesn't needs to be synchronized, as it's not that costly to keep only one execution
     99     *  in parallel, but URL creation and Tile.getUrl() are costly and are not needed when fetching
     100     *  data from cache
     101     *
     102     *  We need to have static url value for TileLoaderJob, as for some TileSources we might get different
     103     *  URL's each call we made (servers switching), and URL's are used below as a key for duplicate detection
     104     *
     105     */
     106    @Override
     107    public URL getUrl() {
     108        if (url == null) {
     109            try {
     110                synchronized (this) {
     111                    if (url == null)
     112                        url = new URL(tile.getUrl());
     113                }
     114            } catch (IOException e) {
     115                log.log(Level.WARNING, "JCS TMS Cache - error creating URL for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
     116                log.log(Level.INFO, "Exception: ", e);
     117            }
     118        }
     119        return url;
     120    }
     121
     122    @Override
     123    public boolean isObjectLoadable() {
     124        if (cacheData != null) {
     125            byte[] content = cacheData.getContent();
     126            try {
     127                return (content != null && content.length > 0) || cacheData.getImage() != null || cacheAsEmpty();
     128            } catch (IOException e) {
     129                log.log(Level.WARNING, "JCS TMS - error loading from cache for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
     130            }
     131        }
     132        return false;
     133    }
     134
     135    @Override
     136    protected boolean cacheAsEmpty() {
     137        if (attributes != null && attributes.isNoTileAtZoom()) {
     138            // do not remove file - keep the information, that there is no tile, for further requests
     139            // the code above will check, if this information is still valid
     140            log.log(Level.FINE, "JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
     141            tile.setError("No tile at this zoom level");
     142            tile.putValue("tile-info", "no-tile");
     143            return true;
     144        }
     145        return false;
     146    }
     147
     148    @Override
     149    protected Executor getDownloadExecutor() {
     150        return DOWNLOAD_JOB_DISPATCHER;
     151    }
     152
     153    public void submit() {
     154        tile.initLoading();
     155        super.submit(this);
     156    }
     157
     158    @Override
     159    public void loadingFinished(CacheEntry object, boolean success) {
     160        try {
     161            loadTile(object);
     162            if (listener != null) {
     163                listener.tileLoadingFinished(tile, success);
     164            }
     165        } catch (IOException e) {
     166            log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
     167            tile.setError(e.getMessage());
     168            tile.setLoaded(false);
     169            if (listener != null) {
     170                listener.tileLoadingFinished(tile, false);
     171            }
     172        }
     173    }
     174
     175    /**
     176     * Method for getting the tile from cache only, without trying to reach remote resource
     177     * @return tile or null, if nothing (useful) was found in cache
     178     */
     179    public Tile getCachedTile() {
     180        BufferedImageCacheEntry data = super.get();
     181        if (isObjectLoadable()) {
     182            try {
     183                loadTile(data);
     184                return tile;
     185            } catch (IOException e) {
     186                log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
     187                return null;
     188            }
     189
     190        } else {
     191            return null;
     192        }
     193    }
     194
     195    private void loadTile(CacheEntry object) throws IOException {
     196        tile.finishLoading();
     197        if (object != null) {
     198            byte[] content = object.getContent();
     199            if (content != null && content.length > 0) {
     200                tile.loadImage(new ByteArrayInputStream(content));
     201            }
     202        }
     203    }
     204
     205    private void loadTile(BufferedImageCacheEntry object) throws IOException {
     206        tile.finishLoading();
     207        if (object != null) {
     208            if (object.getImage() != null) {
     209                tile.setImage(object.getImage());
     210            }
     211        }
     212    }
     213
     214    @Override
     215    protected void handleNotFound() {
     216        tile.setError("No tile at this zoom level");
     217        tile.putValue("tile-info", "no-tile");
     218    }
     219
     220    @Override
     221    protected String getServerKey() {
     222        TileSource ts = tile.getSource();
     223        if (ts instanceof AbstractTMSTileSource) {
     224            return ((AbstractTMSTileSource) ts).getBaseUrl();
     225        }
     226        return super.getServerKey();
     227    }
     228
     229    @Override
     230    protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
     231        return new BufferedImageCacheEntry(content);
     232    }
     233}
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.io.IOException;
     5import java.util.Map;
     6
     7import org.apache.commons.jcs.access.behavior.ICacheAccess;
     8import org.openstreetmap.gui.jmapviewer.Tile;
     9import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
     11import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     12import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     13import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     14import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
     15import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
     16import org.openstreetmap.josm.data.cache.JCSCacheManager;
     17import org.openstreetmap.josm.data.preferences.IntegerProperty;
     18
     19/**
     20 * @author Wiktor Niesiobędzki
     21 *
     22 * Wrapper class that bridges between JCS cache and Tile Loaders
     23 *
     24 */
     25public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCache {
     26
     27    private ICacheAccess<String, BufferedImageCacheEntry> cache;
     28    private int connectTimeout;
     29    private int readTimeout;
     30    private Map<String, String> headers;
     31    private TileLoaderListener listener;
     32    public static final String PREFERENCE_PREFIX   = "imagery.tms.cache.";
     33    // average tile size is about 20kb
     34    public static IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); // 25000 is around 500MB under this assumptions
     35
     36
     37    /**
     38     * Constructor
     39     * @param listener          called when tile loading has finished
     40     * @param name              of the cache
     41     * @param connectTimeout    to remote resource
     42     * @param readTimeout       to remote resource
     43     * @param headers           to be sent along with request
     44     * @param cacheDir          where cache file shall reside
     45     * @throws IOException      when cache initialization fails
     46     */
     47    public TMSCachedTileLoader(TileLoaderListener listener, String name, int connectTimeout, int readTimeout, Map<String, String> headers, String cacheDir) throws IOException {
     48        this.cache = JCSCacheManager.getCache(name,
     49                1000, // use JCS memory cache instead of MemoryTileCache
     50                MAX_OBJECTS_ON_DISK.get(),
     51                cacheDir);
     52        this.connectTimeout = connectTimeout;
     53        this.readTimeout = readTimeout;
     54        this.headers = headers;
     55        this.listener = listener;
     56    }
     57
     58    @Override
     59    public TileJob createTileLoaderJob(Tile tile) {
     60        return new TMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers);
     61    }
     62
     63    @Override
     64    public void clearCache() {
     65        this.cache.clear();
     66    }
     67
     68    @Override
     69    public Tile getTile(TileSource source, int x, int y, int z) {
     70        return createTileLoaderJob(new Tile(source,x, y, z)).getTile();
     71    }
     72
     73    @Override
     74    public void addTile(Tile tile) {
     75        createTileLoaderJob(tile).submit();
     76    }
     77
     78    @Override
     79    public int getTileCount() {
     80        return 0;
     81    }
     82
     83    @Override
     84    public void clear() {
     85        cache.clear();
     86    }
     87
     88    public String getStats() {
     89        return cache.getStats();
     90    }
     91}
  • src/org/openstreetmap/josm/data/cache/CacheEntryAttributes.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.util.HashMap;
     5import java.util.Map;
     6
     7import org.apache.commons.jcs.engine.ElementAttributes;
     8
     9/**
     10 * Class that contains attirubtes for JCS cache entries. Parameters are used to properly handle HTTP caching
     11 *
     12 * @author Wiktor Niesiobędzki
     13 *
     14 */
     15public class CacheEntryAttributes extends ElementAttributes {
     16    private static final long serialVersionUID = 1L; //version
     17    private Map<String, String> attrs = new HashMap<String, String>();
     18    private final static String NO_TILE_AT_ZOOM = "noTileAtZoom";
     19    private final static String ETAG = "Etag";
     20    private final static String LAST_MODIFICATION = "lastModification";
     21    private final static String EXPIRATION_TIME = "expirationTime";
     22
     23    public CacheEntryAttributes() {
     24        super();
     25        attrs.put(NO_TILE_AT_ZOOM, "false");
     26        attrs.put(ETAG, null);
     27        attrs.put(LAST_MODIFICATION, "0");
     28        attrs.put(EXPIRATION_TIME, "0");
     29    }
     30
     31    public boolean isNoTileAtZoom() {
     32        return Boolean.toString(true).equals(attrs.get(NO_TILE_AT_ZOOM));
     33    }
     34    public void setNoTileAtZoom(boolean noTileAtZoom) {
     35        attrs.put(NO_TILE_AT_ZOOM, Boolean.toString(noTileAtZoom));
     36    }
     37    public String getEtag() {
     38        return attrs.get(ETAG);
     39    }
     40    public void setEtag(String etag) {
     41        attrs.put(ETAG, etag);
     42    }
     43
     44    private long getLongAttr(String key) {
     45        try {
     46            return Long.parseLong(attrs.get(key));
     47        } catch (NumberFormatException e) {
     48            attrs.put(key, "0");
     49            return 0;
     50        }
     51    }
     52
     53    public long getLastModification() {
     54        return getLongAttr(LAST_MODIFICATION);
     55    }
     56    public void setLastModification(long lastModification) {
     57        attrs.put(LAST_MODIFICATION, Long.toString(lastModification));
     58    }
     59    public long getExpirationTime() {
     60        return getLongAttr(EXPIRATION_TIME);
     61    }
     62    public void setExpirationTime(long expirationTime) {
     63        attrs.put(EXPIRATION_TIME, Long.toString(expirationTime));
     64    }
     65
     66}
  • src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.awt.image.BufferedImage;
     5import java.io.ByteArrayInputStream;
     6import java.io.IOException;
     7
     8import javax.imageio.ImageIO;
     9
     10
     11/**
     12 * Cache Entry that has methods to get the BufferedImage, that will be cached along in memory
     13 * but will be not serialized when saved to the disk (to avoid duplication of data)
     14 * @author Wiktor Niesiobêdzki
     15 *
     16 */
     17public class BufferedImageCacheEntry extends CacheEntry {
     18    private static final long serialVersionUID = 1L; //version
     19    // transient to avoid serialization, volatile to avoid synchronization of whole getImage() method
     20    private transient volatile BufferedImage img = null;
     21    private transient volatile boolean writtenToDisk = false;
     22
     23    /**
     24     *
     25     * @param content byte array containing image
     26     */
     27    public BufferedImageCacheEntry(byte[] content) {
     28        super(content);
     29    }
     30
     31    /**
     32     * Returns BufferedImage from for the content. Subsequent calls will return the same instance,
     33     * to reduce overhead of ImageIO
     34     *
     35     * @return BufferedImage of cache entry content
     36     * @throws IOException
     37     */
     38    public BufferedImage getImage() throws IOException {
     39        if (img != null)
     40            return img;
     41        synchronized(this) {
     42            if (img != null)
     43                return img;
     44            byte[] content = getContent();
     45            if (content != null) {
     46                img = ImageIO.read(new ByteArrayInputStream(content));
     47
     48                if (writtenToDisk)
     49                    content = null;
     50            }
     51
     52        }
     53        return img;
     54    }
     55
     56
     57    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
     58        /*
     59         * This method below will be needed, if Apache Commons JCS (or any other caching system), will update
     60         * disk representation of object from memory, once it is put into the cache (for example - at closing the cache)
     61         *
     62         * For now it is not the case, as we use DiskUsagePattern.UPDATE, which on JCS shutdown doesn't write again memory
     63         * contents to file, so the fact, that we've cleared never gets saved to the disk
     64         *
     65         * This method is commented out, as it will convert all cache entries to PNG files regardless of what was returned.
     66         * It might cause recompression/change of format which may result in decreased quality of imagery
     67         */
     68        /* synchronized (this) {
     69            if (content == null && img != null) {
     70                ByteArrayOutputStream restoredData = new ByteArrayOutputStream();
     71                ImageIO.write(img, "png", restoredData);
     72                content = restoredData.toByteArray();
     73            }
     74            out.writeObject(this);
     75        }
     76         */
     77        synchronized (this) {
     78            if (content == null && img != null) {
     79                throw new AssertionError("Trying to serialize (save to disk?) an BufferedImageCacheEntry that was converted to BufferedImage and no raw data is present anymore");
     80            }
     81            out.writeObject(this);
     82            // ugly hack to wait till element will get to disk to clean the memory
     83            writtenToDisk = true;
     84
     85            if (img != null) {
     86                content = null;
     87            }
     88
     89        }
     90    }
     91}
  • src/org/openstreetmap/josm/data/cache/ICachedLoaderJob.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.net.URL;
     5
     6
     7/**
     8 *
     9 * @author Wiktor Niesiobędzki
     10 *
     11 * @param <K> cache key type
     12 */
     13public interface ICachedLoaderJob<K> {
     14    /**
     15     * returns cache entry key
     16     *
     17     * @param tile
     18     * @return cache key for tile
     19     */
     20    public K getCacheKey();
     21
     22    /**
     23     * method to get download URL for Job
     24     * @return URL that should be fetched
     25     *
     26     */
     27    public URL getUrl();
     28    /**
     29     * implements the main algorithm for fetching
     30     */
     31    public void run();
     32
     33    /**
     34     * fetches object from cache, or returns null when object is not found
     35     *
     36     * @return filled tile with data or null when no cache entry found
     37     */
     38    public CacheEntry get();
     39
     40    /**
     41     * Submit job for background fetch, and listener will be
     42     * fed with value object
     43     *
     44     * @param listener
     45     */
     46    public void submit(ICachedLoaderListener listener);
     47}
  • src/org/openstreetmap/josm/data/cache/ICachedLoaderListener.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4public interface ICachedLoaderListener {
     5    /**
     6     * Will be called when K object was successfully downloaded
     7     *
     8     * @param data
     9     * @param success
     10     */
     11    public void loadingFinished(CacheEntry data, boolean success);
     12
     13}
  • src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.io.ByteArrayOutputStream;
     5import java.io.FileNotFoundException;
     6import java.io.IOException;
     7import java.io.InputStream;
     8import java.net.HttpURLConnection;
     9import java.net.MalformedURLException;
     10import java.net.URLConnection;
     11import java.util.HashSet;
     12import java.util.Map;
     13import java.util.Random;
     14import java.util.Set;
     15import java.util.concurrent.ConcurrentHashMap;
     16import java.util.concurrent.ConcurrentMap;
     17import java.util.concurrent.Executor;
     18import java.util.concurrent.LinkedBlockingDeque;
     19import java.util.concurrent.RejectedExecutionException;
     20import java.util.concurrent.ThreadPoolExecutor;
     21import java.util.concurrent.TimeUnit;
     22import java.util.logging.Level;
     23import java.util.logging.Logger;
     24
     25import org.apache.commons.jcs.access.behavior.ICacheAccess;
     26import org.apache.commons.jcs.engine.behavior.ICacheElement;
     27import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
     28import org.openstreetmap.josm.data.preferences.IntegerProperty;
     29
     30/**
     31 * @author Wiktor Niesiobędzki
     32 *
     33 * @param <K> cache entry key type
     34 *
     35 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired
     36 * according to HTTP headers sent with tile. If so, it tries to verify using Etags
     37 * or If-Modified-Since / Last-Modified.
     38 *
     39 * If the tile is not valid, it will try to download it from remote service and put it
     40 * to cache. If remote server will fail it will try to use stale entry.
     41 *
     42 * This class will keep only one Job running for specified tile. All others will just finish, but
     43 * listeners will be gathered and notified, once download job will be finished
     44 */
     45public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements ICachedLoaderJob<K>, Runnable {
     46    private static final Logger log = FeatureAdapter.getLogger(JCSCachedTileLoaderJob.class.getCanonicalName());
     47    protected static final long DEFAULT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 7; // 7 days
     48    // Limit for the max-age value send by the server.
     49    protected static final long EXPIRE_TIME_SERVER_LIMIT = 1000L * 60 * 60 * 24 * 28; // 4 weeks
     50    // Absolute expire time limit. Cached tiles that are older will not be used,
     51    // even if the refresh from the server fails.
     52    protected static final long ABSOLUTE_EXPIRE_TIME_LIMIT = Long.MAX_VALUE; // unlimited
     53
     54    /**
     55     * maximum download threads that will be started
     56     */
     57    public static IntegerProperty THREAD_LIMIT = new IntegerProperty("cache.jcs.max_threads", 10);
     58    private static Executor DOWNLOAD_JOB_DISPATCHER = new ThreadPoolExecutor(
     59            2, // we have a small queue, so threads will be quickly started (threads are started only, when queue is full)
     60            THREAD_LIMIT.get().intValue(), // do not this number of threads
     61            30, // keepalive for thread
     62            TimeUnit.SECONDS,
     63            // make queue of LIFO type - so recently requested tiles will be loaded first (assuming that these are which user is waiting to see)
     64            new LinkedBlockingDeque<Runnable>(5) {
     65                /* keep the queue size fairly small, we do not want to
     66                 download a lot of tiles, that user is not seeing anyway */
     67                @Override
     68                public boolean offer(Runnable t) {
     69                    return super.offerFirst(t);
     70                }
     71
     72                @Override
     73                public Runnable remove() {
     74                    return super.removeFirst();
     75                }
     76            }
     77            );
     78    private static ConcurrentMap<String,Set<ICachedLoaderListener>> inProgress = new ConcurrentHashMap<>();
     79    private static ConcurrentMap<String, Boolean> useHead = new ConcurrentHashMap<>();
     80
     81    private long now; // when the job started
     82
     83    private ICacheAccess<K, V> cache;
     84    private ICacheElement<K, V> cacheElement;
     85    protected V cacheData = null;
     86    protected CacheEntryAttributes attributes = null;
     87
     88    // HTTP connection parameters
     89    private int connectTimeout;
     90    private int readTimeout;
     91    private Map<String, String> headers;
     92
     93    /**
     94     * @param cache cache instance that we will work on
     95     * @param headers
     96     * @param readTimeout
     97     * @param connectTimeout
     98     */
     99    public JCSCachedTileLoaderJob(ICacheAccess<K,V> cache,
     100            int connectTimeout, int readTimeout,
     101            Map<String, String> headers) {
     102
     103        this.cache = cache;
     104        this.now = System.currentTimeMillis();
     105        this.connectTimeout = connectTimeout;
     106        this.readTimeout = readTimeout;
     107        this.headers = headers;
     108    }
     109
     110    private void ensureCacheElement() {
     111        if (cacheElement == null && getCacheKey() != null) {
     112            cacheElement = cache.getCacheElement(getCacheKey());
     113            if (cacheElement != null) {
     114                attributes = (CacheEntryAttributes) cacheElement.getElementAttributes();
     115                cacheData = cacheElement.getVal();
     116            }
     117        }
     118    }
     119
     120    public V get() {
     121        ensureCacheElement();
     122        return cacheData;
     123    }
     124
     125    @Override
     126    public void submit(ICachedLoaderListener listener) {
     127        boolean first = false;
     128        String url = getUrl().toString();
     129        if (url == null) {
     130            log.log(Level.WARNING, "No url returned for: {0}, skipping", getCacheKey());
     131            return;
     132        }
     133        synchronized (inProgress) {
     134            Set<ICachedLoaderListener> newListeners = inProgress.get(url);
     135            if (newListeners == null) {
     136                newListeners = new HashSet<>();
     137                inProgress.put(url, newListeners);
     138                first = true;
     139            }
     140            newListeners.add(listener);
     141        }
     142
     143        if (first) {
     144            ensureCacheElement();
     145            if (cacheElement != null && isCacheElementValid() && (isObjectLoadable())) {
     146                // we got something in cache, and it's valid, so lets return it
     147                log.log(Level.FINE, "JCS - Returning object from cache: {0}", getCacheKey());
     148                finishLoading(true);
     149                return;
     150            }
     151            // object not in cache, so submit work to separate thread
     152            try {
     153                // use getter method, so subclasses may override executors, to get separate thread pool
     154                getDownloadExecutor().execute(JCSCachedTileLoaderJob.this);
     155            } catch (RejectedExecutionException e) {
     156                // queue was full, try again later
     157                log.log(Level.FINE, "JCS - rejected job for: {0}", getCacheKey());
     158                finishLoading(false);
     159            }
     160        }
     161    }
     162
     163    /**
     164     *
     165     * @return checks if object from cache has sufficient data to be returned
     166     */
     167    protected boolean isObjectLoadable() {
     168        byte[] content = cacheData.getContent();
     169        return content != null && content.length > 0;
     170    }
     171
     172    /**
     173     *
     174     * @return cache object as empty, regardless of what remote resource has returned (ex. based on headers)
     175     */
     176    protected boolean cacheAsEmpty() {
     177        return false;
     178    }
     179
     180    /**
     181     * @return key under which discovered server settings will be kept
     182     */
     183    protected String getServerKey() {
     184        return getUrl().getHost();
     185    }
     186
     187    /**
     188     * this needs to be non-static, so it can be overridden by subclasses
     189     */
     190    protected Executor getDownloadExecutor() {
     191        return DOWNLOAD_JOB_DISPATCHER;
     192    }
     193
     194
     195    public void run() {
     196        final Thread currentThread = Thread.currentThread();
     197        final String oldName = currentThread.getName();
     198        currentThread.setName("JCS Downloading: " + getUrl());
     199        try {
     200            // try to load object from remote resource
     201            if (loadObject()) {
     202                finishLoading(true);
     203            } else {
     204                // if loading failed - check if we can return stale entry
     205                if (isObjectLoadable()) {
     206                    // try to get stale entry in cache
     207                    finishLoading(true);
     208                    log.log(Level.FINE, "JCS - found stale object in cache: {0}", getUrl());
     209                } else {
     210                    // failed completely
     211                    finishLoading(false);
     212                }
     213            }
     214        } finally {
     215            currentThread.setName(oldName);
     216        }
     217    }
     218
     219
     220    private void finishLoading(boolean success) {
     221        Set<ICachedLoaderListener> listeners = null;
     222        synchronized (inProgress) {
     223            listeners = inProgress.remove(getUrl().toString());
     224        }
     225        if (listeners == null) {
     226            log.log(Level.WARNING, "Listener not found for URL: {0}. Listener not notified!", getUrl());
     227            return;
     228        }
     229        try {
     230            for (ICachedLoaderListener l: listeners) {
     231                l.loadingFinished(cacheData, success);
     232            }
     233        } catch (Exception e) {
     234            log.log(Level.WARNING, "JCS - Error while loading object from cache: {0}; {1}", new Object[]{e.getMessage(), getUrl()});
     235            log.log(Level.FINE, "Stacktrace", e);
     236            for (ICachedLoaderListener l: listeners) {
     237                l.loadingFinished(cacheData, false);
     238            }
     239
     240        }
     241
     242    }
     243
     244    private boolean isCacheElementValid() {
     245        long expires = attributes.getExpirationTime();
     246
     247        // check by expire date set by server
     248        if (expires != 0L) {
     249            // put a limit to the expire time (some servers send a value
     250            // that is too large)
     251            expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT);
     252            if (now > expires) {
     253                log.log(Level.FINE, "JCS - Object {0} has expired -> valid to {1}, now is: {2}", new Object[]{getUrl(), Long.toString(expires), Long.toString(now)});
     254                return false;
     255            }
     256        } else {
     257            // check by file modification date
     258            if (now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {
     259                log.log(Level.FINE, "JCS - Object has expired, maximum file age reached {0}", getUrl());
     260                return false;
     261            }
     262        }
     263        return true;
     264    }
     265
     266    private boolean loadObject() {
     267        try {
     268            // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match
     269            // then just use HEAD request and check returned values
     270            if (isObjectLoadable() &&
     271                    Boolean.TRUE.equals(useHead.get(getServerKey())) &&
     272                    isCacheValidUsingHead()) {
     273                log.log(Level.FINE, "JCS - cache entry verified using HEAD request: {0}", getUrl());
     274                return true;
     275            }
     276            URLConnection urlConn = getURLConnection();
     277
     278            if (isObjectLoadable()  &&
     279                    (now - attributes.getLastModification()) <= ABSOLUTE_EXPIRE_TIME_LIMIT) {
     280                urlConn.setIfModifiedSince(attributes.getLastModification());
     281            }
     282            if (isObjectLoadable() && attributes.getEtag() != null) {
     283                urlConn.addRequestProperty("If-None-Match", attributes.getEtag());
     284            }
     285            if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) {
     286                // If isModifiedSince or If-None-Match has been set
     287                // and the server answers with a HTTP 304 = "Not Modified"
     288                log.log(Level.FINE, "JCS - IfModifiedSince/Etag test: local version is up to date: {0}", getUrl());
     289                return true;
     290            } else if (isObjectLoadable()) {
     291                // we have an object in cache, but we haven't received 304 resposne code
     292                // check if we should use HEAD request to verify
     293                if((attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) ||
     294                        attributes.getLastModification() == urlConn.getLastModified()) {
     295                    // we sent ETag or If-Modified-Since, but didn't get 304 response code
     296                    // for further requests - use HEAD
     297                    String serverKey = getServerKey();
     298                    log.log(Level.INFO, "JCS - Host: {0} found not to return 304 codes for If-Modifed-Since or If-None-Match headers", serverKey);
     299                    useHead.put(serverKey, Boolean.TRUE);
     300                }
     301            }
     302
     303            attributes = parseHeaders(urlConn);
     304
     305            for (int i = 0; i < 5; ++i) {
     306                if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {
     307                    Thread.sleep(5000+(new Random()).nextInt(5000));
     308                    continue;
     309                }
     310                byte[] raw = read(urlConn);
     311
     312                if (!cacheAsEmpty() && raw != null && raw.length > 0) {
     313                    cacheData = createCacheEntry(raw);
     314                    cache.put(getCacheKey(), cacheData, attributes);
     315                    log.log(Level.FINE, "JCS - downloaded key: {0}, length: {1}, url: {2}",
     316                            new Object[] {getCacheKey(), raw.length, getUrl()});
     317                    return true;
     318                } else {
     319                    cacheData = createCacheEntry(new byte[]{});
     320                    cache.put(getCacheKey(), cacheData, attributes);
     321                    log.log(Level.FINE, "JCS - Caching empty object {0}", getUrl());
     322                    return true;
     323                }
     324            }
     325        } catch (FileNotFoundException e) {
     326            log.log(Level.FINE, "JCS - Caching empty object as server returned 404 for: {0}", getUrl());
     327            cache.put(getCacheKey(), createCacheEntry(new byte[]{}), attributes);
     328            handleNotFound();
     329            return true;
     330        } catch (Exception e) {
     331            log.log(Level.WARNING, "JCS - Exception during download " + getUrl(), e);
     332        }
     333        log.log(Level.WARNING, "JCS - Silent failure during download: {0}", getUrl());
     334        return false;
     335
     336    }
     337
     338    protected abstract void handleNotFound();
     339
     340    protected abstract V createCacheEntry(byte[] content);
     341
     342    private CacheEntryAttributes parseHeaders(URLConnection urlConn) {
     343        CacheEntryAttributes ret = new CacheEntryAttributes();
     344        ret.setNoTileAtZoom("no-tile".equals(urlConn.getHeaderField("X-VE-Tile-Info")));
     345
     346        Long lng = urlConn.getExpiration();
     347        if (lng.equals(0L)) {
     348            try {
     349                String str = urlConn.getHeaderField("Cache-Control");
     350                if (str != null) {
     351                    for (String token: str.split(",")) {
     352                        if (token.startsWith("max-age=")) {
     353                            lng = Long.parseLong(token.substring(8)) * 1000 +
     354                                    System.currentTimeMillis();
     355                        }
     356                    }
     357                }
     358            } catch (NumberFormatException e) {} //ignore malformed Cache-Control headers
     359        }
     360
     361        ret.setExpirationTime(lng);
     362        ret.setLastModification(now);
     363        ret.setEtag(urlConn.getHeaderField("ETag"));
     364        return ret;
     365    }
     366
     367    private HttpURLConnection getURLConnection() throws IOException, MalformedURLException {
     368        HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection();
     369        urlConn.setRequestProperty("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
     370        urlConn.setReadTimeout(readTimeout); // 30 seconds read timeout
     371        urlConn.setConnectTimeout(connectTimeout);
     372        for(Map.Entry<String, String> e: headers.entrySet()) {
     373            urlConn.setRequestProperty(e.getKey(), e.getValue());
     374        }
     375        return urlConn;
     376    }
     377
     378    private boolean isCacheValidUsingHead() throws IOException {
     379        HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection();
     380        urlConn.setRequestMethod("HEAD");
     381        long lastModified = urlConn.getLastModified();
     382        return (
     383                (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) ||
     384                (lastModified != 0 && lastModified <= attributes.getLastModification())
     385                );
     386    }
     387
     388    private static byte[] read(URLConnection urlConn) throws IOException {
     389        InputStream input = urlConn.getInputStream();
     390        try {
     391            ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available());
     392            byte[] buffer = new byte[2048];
     393            boolean finished = false;
     394            do {
     395                int read = input.read(buffer);
     396                if (read >= 0) {
     397                    bout.write(buffer, 0, read);
     398                } else {
     399                    finished = true;
     400                }
     401            } while (!finished);
     402            if (bout.size() == 0)
     403                return null;
     404            return bout.toByteArray();
     405        } finally {
     406            input.close();
     407        }
     408    }
     409}
  • src/org/openstreetmap/josm/data/cache/JCSCacheManager.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.io.File;
     5import java.io.IOException;
     6import java.text.MessageFormat;
     7import java.util.Properties;
     8import java.util.logging.Handler;
     9import java.util.logging.Level;
     10import java.util.logging.LogRecord;
     11import java.util.logging.Logger;
     12
     13import org.apache.commons.jcs.access.CacheAccess;
     14import org.apache.commons.jcs.auxiliary.AuxiliaryCache;
     15import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCache;
     16import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes;
     17import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheManager;
     18import org.apache.commons.jcs.engine.CompositeCacheAttributes;
     19import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes.DiskUsagePattern;
     20import org.apache.commons.jcs.engine.control.CompositeCache;
     21import org.apache.commons.jcs.engine.control.CompositeCacheManager;
     22import org.apache.commons.jcs.utils.serialization.StandardSerializer;
     23import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
     24import org.openstreetmap.josm.Main;
     25import org.openstreetmap.josm.data.preferences.IntegerProperty;
     26
     27
     28/**
     29 * @author Wiktor Niesiobędzki
     30 *
     31 * Wrapper class for JCS Cache. Sets some sane environment and returns instances of cache objects.
     32 * Static configuration for now assumes some small LRU cache in memory and larger LRU cache on disk
     33 *
     34 */
     35public class JCSCacheManager {
     36    private static final Logger log = FeatureAdapter.getLogger(JCSCacheManager.class.getCanonicalName());
     37
     38    private static volatile CompositeCacheManager cacheManager = null;
     39    private static long maxObjectTTL        = Long.MAX_VALUE;
     40    private final static String PREFERENCE_PREFIX = "jcs.cache";
     41
     42    /**
     43     * default objects to be held in memory by JCS caches (per region)
     44     */
     45    public static final IntegerProperty DEFAULT_MAX_OBJECTS_IN_MEMORY  = new IntegerProperty(PREFERENCE_PREFIX + ".max_objects_in_memory", 1000);
     46
     47    private static void initialize() throws IOException {
     48        File cacheDir = new File(Main.pref.getCacheDirectory(), "jcs");
     49
     50        if ((!cacheDir.exists() && !cacheDir.mkdirs()))
     51            throw new IOException("Cannot access cache directory");
     52
     53        // raising logging level gives ~500x performance gain
     54        // http://westsworld.dk/blog/2008/01/jcs-and-performance/
     55        Logger jcsLog = Logger.getLogger("org.apache.commons.jcs");
     56        jcsLog.setLevel(Level.INFO);
     57        jcsLog.setUseParentHandlers(false);
     58        //Logger.getLogger("org.apache.common").setUseParentHandlers(false);
     59        // we need a separate handler from Main's, as we  downgrade LEVEL.INFO to DEBUG level
     60        jcsLog.addHandler(new Handler() {
     61            @Override
     62            public void publish(LogRecord record) {
     63                String msg = MessageFormat.format(record.getMessage(), record.getParameters());
     64                if (record.getLevel().intValue() >= Level.SEVERE.intValue()) {
     65                    Main.error(msg);
     66                } else if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
     67                    Main.warn(msg);
     68                    // downgrade INFO level to debug, as JCS is too verbose at INFO level
     69                } else if (record.getLevel().intValue() >= Level.INFO.intValue()) {
     70                    Main.debug(msg);
     71                } else {
     72                    Main.trace(msg);
     73                }
     74            }
     75
     76            @Override
     77            public void flush() {
     78            }
     79
     80            @Override
     81            public void close() throws SecurityException {
     82            }
     83        });
     84
     85
     86        CompositeCacheManager cm  = CompositeCacheManager.getUnconfiguredInstance();
     87        // this could be moved to external file
     88        Properties props = new Properties();
     89        // these are default common to all cache regions
     90        // use of auxiliary cache and sizing of the caches is done with giving proper geCache(...) params
     91        props.setProperty("jcs.default.cacheattributes",                            org.apache.commons.jcs.engine.CompositeCacheAttributes.class.getCanonicalName());
     92        props.setProperty("jcs.default.cacheattributes.MaxObjects",                 DEFAULT_MAX_OBJECTS_IN_MEMORY.get().toString());
     93        props.setProperty("jcs.default.cacheattributes.UseMemoryShrinker",          "true");
     94        props.setProperty("jcs.default.cacheattributes.DiskUsagePatternName",       "UPDATE"); // store elements on disk on put
     95        props.setProperty("jcs.default.elementattributes",                          CacheEntryAttributes.class.getCanonicalName());
     96        props.setProperty("jcs.default.elementattributes.IsEternal",                "false");
     97        props.setProperty("jcs.default.elementattributes.MaxLife",                  Long.toString(maxObjectTTL));
     98        props.setProperty("jcs.default.elementattributes.IdleTime",                 Long.toString(maxObjectTTL));
     99        props.setProperty("jcs.default.elementattributes.IsSpool",                  "true");
     100        cm.configure(props);
     101        cacheManager = cm;
     102
     103    }
     104
     105    /**
     106     * Returns configured cache object for named cache region
     107     * @param cacheName region name
     108     * @return cache access object
     109     * @throws IOException if directory is not found
     110     */
     111    public static <K,V> CacheAccess<K, V> getCache(String cacheName) throws IOException {
     112        return getCache(cacheName, DEFAULT_MAX_OBJECTS_IN_MEMORY.get().intValue(), 0, null);
     113    }
     114
     115    /**
     116     * Returns configured cache object with defined limits of memory cache and disk cache
     117     * @param cacheName         region name
     118     * @param maxMemoryObjects  number of objects to keep in memory
     119     * @param maxDiskObjects    number of objects to keep on disk (if cachePath provided)
     120     * @param cachePath         path to disk cache. if null, no disk cache will be created
     121     * @return cache access object
     122     * @throws IOException if directory is not found
     123     */
     124    public static <K,V> CacheAccess<K, V> getCache(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) throws IOException {
     125        if (cacheManager != null)
     126            return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath);
     127
     128        synchronized (JCSCacheManager.class) {
     129            if (cacheManager == null)
     130                initialize();
     131            return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath);
     132        }
     133    }
     134
     135
     136    @SuppressWarnings("unchecked")
     137    private static <K,V> CacheAccess<K, V> getCacheInner(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) {
     138        CompositeCache<K, V> cc = cacheManager.getCache(cacheName, getCacheAttributes(maxMemoryObjects));
     139
     140        if (cachePath != null) {
     141            IndexedDiskCacheAttributes diskAttributes = getDiskCacheAttributes(maxDiskObjects, cachePath);
     142            diskAttributes.setCacheName(cacheName);
     143            IndexedDiskCache<K, V> diskCache = IndexedDiskCacheManager.getInstance(null, null, new StandardSerializer()).getCache(diskAttributes);
     144            cc.setAuxCaches(new AuxiliaryCache[]{diskCache});
     145        }
     146        return new CacheAccess<K, V>(cc);
     147    }
     148
     149    /**
     150     * Close all files to ensure, that all indexes and data are properly written
     151     */
     152    public static void shutdown() {
     153        // use volatile semantics to get consistent object
     154        CompositeCacheManager localCacheManager = cacheManager;
     155        if (localCacheManager != null) {
     156            localCacheManager.shutDown();
     157        }
     158    }
     159
     160    private static IndexedDiskCacheAttributes getDiskCacheAttributes(int maxDiskObjects, String cachePath) {
     161        IndexedDiskCacheAttributes ret = new IndexedDiskCacheAttributes();
     162        ret.setMaxKeySize(maxDiskObjects);
     163        if (cachePath != null) {
     164            File path = new File(cachePath);
     165            if (!path.exists() && !path.mkdirs()) {
     166                log.log(Level.WARNING, "Failed to create cache path: {0}", cachePath);
     167            } else {
     168                ret.setDiskPath(path);
     169            }
     170        }
     171        return ret;
     172    }
     173
     174    private static CompositeCacheAttributes getCacheAttributes(int maxMemoryElements) {
     175        CompositeCacheAttributes ret = new CompositeCacheAttributes();
     176        ret.setMaxObjects(maxMemoryElements);
     177        ret.setDiskUsagePattern(DiskUsagePattern.UPDATE);
     178        return ret;
     179    }
     180}
  • src/org/openstreetmap/josm/data/cache/CacheEntry.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.io.Serializable;
     5
     6/**
     7 * @author Wiktor Niesiobędzki
     8 *
     9 * Class that will hold JCS cache entries
     10 *
     11 */
     12public class CacheEntry implements Serializable {
     13    private static final long serialVersionUID = 1L; //version
     14    protected byte[] content;
     15
     16    /**
     17     * @param content of the cache entry
     18     */
     19    public CacheEntry(byte[] content) {
     20        this.content = content;
     21    }
     22
     23    /**
     24     * @return cache entry content
     25     */
     26    public byte[] getContent() {
     27        return content;
     28    }
     29}