001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.imagery;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagLayout;
007import java.awt.event.ActionEvent;
008import java.io.File;
009import java.util.ArrayList;
010import java.util.Comparator;
011import java.util.List;
012import java.util.Locale;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Set;
016import java.util.concurrent.ConcurrentHashMap;
017
018import javax.swing.AbstractAction;
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021import javax.swing.JScrollPane;
022import javax.swing.JSpinner;
023import javax.swing.JTable;
024import javax.swing.SpinnerNumberModel;
025import javax.swing.table.DefaultTableModel;
026import javax.swing.table.TableColumn;
027import javax.swing.table.TableModel;
028
029import org.apache.commons.jcs.access.CacheAccess;
030import org.apache.commons.jcs.engine.stats.behavior.ICacheStats;
031import org.apache.commons.jcs.engine.stats.behavior.IStatElement;
032import org.apache.commons.jcs.engine.stats.behavior.IStats;
033import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
034import org.openstreetmap.josm.data.cache.JCSCacheManager;
035import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory;
036import org.openstreetmap.josm.gui.MainApplication;
037import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
038import org.openstreetmap.josm.gui.layer.TMSLayer;
039import org.openstreetmap.josm.gui.layer.WMSLayer;
040import org.openstreetmap.josm.gui.layer.WMTSLayer;
041import org.openstreetmap.josm.gui.util.GuiHelper;
042import org.openstreetmap.josm.gui.widgets.ButtonColumn;
043import org.openstreetmap.josm.gui.widgets.JosmTextField;
044import org.openstreetmap.josm.tools.GBC;
045import org.openstreetmap.josm.tools.Logging;
046import org.openstreetmap.josm.tools.Pair;
047import org.openstreetmap.josm.tools.Utils;
048
049/**
050 * Panel for cache size, location and content management.
051 *
052 * @author Wiktor Niesiobędzki
053 *
054 */
055public class CacheSettingsPanel extends JPanel {
056
057    private final JosmTextField cacheDir = new JosmTextField(11);
058    private final JSpinner maxElementsOnDisk = new JSpinner(new SpinnerNumberModel(
059            AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.get().intValue(), 0, Integer.MAX_VALUE, 1));
060
061    /**
062     * Creates cache content panel
063     */
064    public CacheSettingsPanel() {
065        super(new GridBagLayout());
066
067        add(new JLabel(tr("Tile cache directory: ")), GBC.std());
068        add(GBC.glue(5, 0), GBC.std());
069        add(cacheDir, GBC.eol().fill(GBC.HORIZONTAL));
070
071        add(new JLabel(tr("Maximum size of disk cache (per imagery) in MB: ")), GBC.std());
072        add(GBC.glue(5, 0), GBC.std());
073        add(maxElementsOnDisk, GBC.eop());
074
075        MainApplication.worker.submit(() -> {
076            addToPanel(TMSLayer.getCache(), "TMS");
077            addToPanel(WMSLayer.getCache(), "WMS");
078            addToPanel(WMTSLayer.getCache(), "WMTS");
079        });
080    }
081
082    private void addToPanel(final CacheAccess<String, BufferedImageCacheEntry> cache, final String name) {
083        final Long cacheSize = getCacheSize(cache);
084        final String sizeString = Utils.getSizeString(cacheSize, Locale.getDefault());
085        final TableModel tableModel = getTableModel(cache);
086
087        GuiHelper.runInEDT(() -> {
088            /* I18n: {0} is cache name (TMS/WMS/WMTS), {1} is size string */
089            add(new JLabel(tr("{0} cache, total cache size: {1}", name, sizeString)),
090                GBC.eol().insets(5, 5, 0, 0));
091            add(new JScrollPane(getTableForCache(cache, tableModel)),
092                GBC.eol().fill(GBC.BOTH));
093        });
094    }
095
096    private static Long getCacheSize(CacheAccess<String, BufferedImageCacheEntry> cache) {
097        ICacheStats stats = cache.getStatistics();
098        for (IStats cacheStats: stats.getAuxiliaryCacheStats()) {
099            for (IStatElement<?> statElement: cacheStats.getStatElements()) {
100                if ("Data File Length".equals(statElement.getName())) {
101                    Object val = statElement.getData();
102                    if (val instanceof Long) {
103                        return (Long) val;
104                    }
105                }
106            }
107        }
108        return Long.valueOf(-1);
109    }
110
111    /**
112     * Returns the cache stats.
113     * @param cache imagery cache
114     * @return the cache stats
115     */
116    public static String[][] getCacheStats(CacheAccess<String, BufferedImageCacheEntry> cache) {
117        Set<String> keySet = cache.getCacheControl().getKeySet();
118        Map<String, int[]> temp = new ConcurrentHashMap<>(); // use int[] as a Object reference to int, gives better performance
119        for (String key: keySet) {
120            String[] keyParts = key.split(":", 2);
121            if (keyParts.length == 2) {
122                int[] counter = temp.get(keyParts[0]);
123                if (counter == null) {
124                    temp.put(keyParts[0], new int[]{1});
125                } else {
126                    counter[0]++;
127                }
128            } else {
129                Logging.warn("Could not parse the key: {0}. No colon found", key);
130            }
131        }
132
133        List<Pair<String, Integer>> sortedStats = new ArrayList<>();
134        for (Entry<String, int[]> e: temp.entrySet()) {
135            sortedStats.add(new Pair<>(e.getKey(), e.getValue()[0]));
136        }
137        sortedStats.sort(Comparator.comparing(o -> o.b, Comparator.reverseOrder()));
138        String[][] ret = new String[sortedStats.size()][3];
139        int index = 0;
140        for (Pair<String, Integer> e: sortedStats) {
141            ret[index] = new String[]{e.a, e.b.toString(), tr("Clear")};
142            index++;
143        }
144        return ret;
145    }
146
147    private static JTable getTableForCache(final CacheAccess<String, BufferedImageCacheEntry> cache, final TableModel tableModel) {
148        final JTable ret = new JTable(tableModel);
149
150        ButtonColumn buttonColumn = new ButtonColumn(
151                new AbstractAction() {
152                    @Override
153                    public void actionPerformed(ActionEvent e) {
154                        int row = ret.convertRowIndexToModel(ret.getEditingRow());
155                        tableModel.setValueAt("0", row, 1);
156                        cache.remove(ret.getValueAt(row, 0).toString() + ':');
157                    }
158                });
159        TableColumn tableColumn = ret.getColumnModel().getColumn(2);
160        tableColumn.setCellRenderer(buttonColumn);
161        tableColumn.setCellEditor(buttonColumn);
162        return ret;
163    }
164
165    private static DefaultTableModel getTableModel(final CacheAccess<String, BufferedImageCacheEntry> cache) {
166        return new DefaultTableModel(
167                getCacheStats(cache),
168                new String[]{tr("Cache name"), tr("Object Count"), tr("Clear")}) {
169            @Override
170            public boolean isCellEditable(int row, int column) {
171                return column == 2;
172            }
173        };
174    }
175
176    /**
177     * Loads the common settings.
178     */
179    void loadSettings() {
180        this.cacheDir.setText(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get());
181        this.maxElementsOnDisk.setValue(AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.get());
182    }
183
184    /**
185     * Saves the common settings.
186     * @return true when restart is required
187     */
188    boolean saveSettings() {
189        boolean restartRequired = removeCacheFiles(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get(),
190                1024L * 1024L * ((Integer) this.maxElementsOnDisk.getValue()));
191
192        if (!AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.get().equals(this.maxElementsOnDisk.getValue())) {
193            AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.put((Integer) this.maxElementsOnDisk.getValue());
194            restartRequired = true;
195        }
196
197
198        if (!CachedTileLoaderFactory.PROP_TILECACHE_DIR.get().equals(this.cacheDir.getText())) {
199            restartRequired = true;
200            removeCacheFiles(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get(), 0); // clear old cache directory
201            CachedTileLoaderFactory.PROP_TILECACHE_DIR.put(this.cacheDir.getText());
202        }
203
204        return restartRequired;
205    }
206
207    private static boolean removeCacheFiles(String path, long maxSize) {
208        File directory = new File(path);
209        File[] cacheFiles = directory.listFiles((dir, name) -> name.endsWith(".data") || name.endsWith(".key"));
210        boolean restartRequired = false;
211        if (cacheFiles != null) {
212            for (File cacheFile: cacheFiles) {
213                if (cacheFile.length() > maxSize) {
214                    if (!restartRequired) {
215                        JCSCacheManager.shutdown(); // shutdown Cache - so files can by safely deleted
216                        restartRequired = true;
217                    }
218                    Utils.deleteFile(cacheFile);
219                    File otherFile = null;
220                    if (cacheFile.getName().endsWith(".data")) {
221                        otherFile = new File(cacheFile.getPath().replaceAll("\\.data$", ".key"));
222                    } else if (cacheFile.getName().endsWith(".key")) {
223                        otherFile = new File(cacheFile.getPath().replaceAll("\\.key$", ".data"));
224                    }
225                    if (otherFile != null) {
226                        Utils.deleteFileIfExists(otherFile);
227                    }
228                }
229            }
230        }
231        return restartRequired;
232    }
233}