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