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.Color;
007import java.awt.Component;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.ActionListener;
011import java.util.ArrayList;
012import java.util.Collections;
013import java.util.Comparator;
014import java.util.List;
015import java.util.Map;
016import java.util.Map.Entry;
017import java.util.Set;
018import java.util.concurrent.ConcurrentHashMap;
019import java.util.concurrent.ExecutorService;
020import java.util.concurrent.Executors;
021
022import javax.swing.AbstractAction;
023import javax.swing.AbstractCellEditor;
024import javax.swing.Action;
025import javax.swing.Icon;
026import javax.swing.JButton;
027import javax.swing.JLabel;
028import javax.swing.JPanel;
029import javax.swing.JScrollPane;
030import javax.swing.JTable;
031import javax.swing.UIManager;
032import javax.swing.border.LineBorder;
033import javax.swing.table.DefaultTableModel;
034import javax.swing.table.TableCellEditor;
035import javax.swing.table.TableCellRenderer;
036import javax.swing.table.TableColumn;
037import javax.swing.table.TableModel;
038
039import org.apache.commons.jcs.access.CacheAccess;
040import org.apache.commons.jcs.engine.stats.behavior.ICacheStats;
041import org.apache.commons.jcs.engine.stats.behavior.IStatElement;
042import org.apache.commons.jcs.engine.stats.behavior.IStats;
043import org.openstreetmap.josm.Main;
044import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
045import org.openstreetmap.josm.gui.layer.TMSLayer;
046import org.openstreetmap.josm.gui.layer.WMSLayer;
047import org.openstreetmap.josm.gui.layer.WMTSLayer;
048import org.openstreetmap.josm.gui.util.GuiHelper;
049import org.openstreetmap.josm.tools.GBC;
050import org.openstreetmap.josm.tools.Pair;
051import org.openstreetmap.josm.tools.Utils;
052
053/**
054 * Panel for cache content management.
055 *
056 * @author Wiktor Niesiobędzki
057 *
058 */
059public class CacheContentsPanel extends JPanel {
060
061    /**
062     *
063     * Class based on:  http://www.camick.com/java/source/ButtonColumn.java
064     * https://tips4java.wordpress.com/2009/07/12/table-button-column/
065     *
066     */
067    private static final class ButtonColumn extends AbstractCellEditor implements TableCellRenderer, TableCellEditor, ActionListener {
068        private final Action action;
069        private final JButton renderButton;
070        private final JButton editButton;
071        private Object editorValue;
072
073        private ButtonColumn() {
074            this(null);
075        }
076
077        private ButtonColumn(Action action) {
078            this.action = action;
079            renderButton = new JButton();
080            editButton = new JButton();
081            editButton.setFocusPainted(false);
082            editButton.addActionListener(this);
083            editButton.setBorder(new LineBorder(Color.BLUE));
084        }
085
086        @Override
087        public Object getCellEditorValue() {
088            return editorValue;
089        }
090
091        @Override
092        public void actionPerformed(ActionEvent e) {
093            this.action.actionPerformed(e);
094        }
095
096        @Override
097        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
098            this.editorValue = value;
099            if (value == null) {
100                editButton.setText("");
101                editButton.setIcon(null);
102            } else if (value instanceof Icon) {
103                editButton.setText("");
104                editButton.setIcon((Icon) value);
105            } else {
106                editButton.setText(value.toString());
107                editButton.setIcon(null);
108            }
109            this.editorValue = value;
110            return editButton;
111        }
112
113        @Override
114        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
115                boolean hasFocus, int row, int column) {
116            if (isSelected) {
117                renderButton.setForeground(table.getSelectionForeground());
118                renderButton.setBackground(table.getSelectionBackground());
119            } else {
120                renderButton.setForeground(table.getForeground());
121                renderButton.setBackground(UIManager.getColor("Button.background"));
122            }
123
124            renderButton.setFocusPainted(hasFocus);
125
126            if (value == null) {
127                renderButton.setText("");
128                renderButton.setIcon(null);
129            } else if (value instanceof Icon) {
130                renderButton.setText("");
131                renderButton.setIcon((Icon) value);
132            } else {
133                renderButton.setText(value.toString());
134                renderButton.setIcon(null);
135            }
136            return renderButton;
137        }
138
139    }
140
141    private final transient ExecutorService executor =
142            Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
143
144    /**
145     * Creates cache content panel
146     */
147    public CacheContentsPanel() {
148        super(new GridBagLayout());
149        executor.submit(new Runnable() {
150            @Override
151            public void run() {
152                addToPanel(TMSLayer.getCache(), "TMS");
153                addToPanel(WMSLayer.getCache(), "WMS");
154                addToPanel(WMTSLayer.getCache(), "WMTS");
155            }
156        });
157        executor.shutdown();
158    }
159
160    private void addToPanel(final CacheAccess<String, BufferedImageCacheEntry> cache, final String name) {
161        final Long cacheSize = getCacheSize(cache);
162        final TableModel tableModel = getTableModel(cache);
163
164        GuiHelper.runInEDT(new Runnable() {
165            @Override
166            public void run() {
167                add(
168                        new JLabel(tr("{0} cache, total cache size: {1} bytes", name, cacheSize)),
169                        GBC.eol().insets(5, 5, 0, 0));
170
171                add(
172                        new JScrollPane(getTableForCache(cache, tableModel)),
173                        GBC.eol().fill(GBC.BOTH));
174            }
175        });
176    }
177
178    private static Long getCacheSize(CacheAccess<String, BufferedImageCacheEntry> cache) {
179        ICacheStats stats = cache.getStatistics();
180        for (IStats cacheStats: stats.getAuxiliaryCacheStats()) {
181            for (IStatElement<?> statElement: cacheStats.getStatElements()) {
182                if ("Data File Length".equals(statElement.getName())) {
183                    Object val = statElement.getData();
184                    if (val instanceof Long) {
185                        return (Long) val;
186                    }
187
188                }
189            }
190        }
191        return Long.valueOf(-1);
192    }
193
194    private static String[][] getCacheStats(CacheAccess<String, BufferedImageCacheEntry> cache) {
195        Set<String> keySet = cache.getCacheControl().getKeySet();
196        Map<String, int[]> temp = new ConcurrentHashMap<>(); // use int[] as a Object reference to int, gives better performance
197        for (String key: keySet) {
198            String[] keyParts = key.split(":", 2);
199            if (keyParts.length == 2) {
200                int[] counter = temp.get(keyParts[0]);
201                if (counter == null) {
202                    temp.put(keyParts[0], new int[]{1});
203                } else {
204                    counter[0]++;
205                }
206            } else {
207                Main.warn("Could not parse the key: {0}. No colon found", key);
208            }
209        }
210
211        List<Pair<String, Integer>> sortedStats = new ArrayList<>();
212        for (Entry<String, int[]> e: temp.entrySet()) {
213            sortedStats.add(new Pair<>(e.getKey(), e.getValue()[0]));
214        }
215        Collections.sort(sortedStats, new Comparator<Pair<String, Integer>>() {
216            @Override
217            public int compare(Pair<String, Integer> o1, Pair<String, Integer> o2) {
218                return -1 * o1.b.compareTo(o2.b);
219            }
220        });
221        String[][] ret = new String[sortedStats.size()][3];
222        int index = 0;
223        for (Pair<String, Integer> e: sortedStats) {
224            ret[index] = new String[]{e.a, e.b.toString(), tr("Clear")};
225            index++;
226        }
227        return ret;
228    }
229
230    private static JTable getTableForCache(final CacheAccess<String, BufferedImageCacheEntry> cache, final TableModel tableModel) {
231        final JTable ret = new JTable(tableModel);
232
233        ButtonColumn buttonColumn = new ButtonColumn(
234                new AbstractAction() {
235                    @Override
236                    public void actionPerformed(ActionEvent e) {
237                        int row = ret.convertRowIndexToModel(ret.getEditingRow());
238                        tableModel.setValueAt("0", row, 1);
239                        cache.remove(ret.getValueAt(row, 0).toString() + ':');
240                    }
241                });
242        TableColumn tableColumn = ret.getColumnModel().getColumn(2);
243        tableColumn.setCellRenderer(buttonColumn);
244        tableColumn.setCellEditor(buttonColumn);
245        return ret;
246    }
247
248    private static DefaultTableModel getTableModel(final CacheAccess<String, BufferedImageCacheEntry> cache) {
249        return new DefaultTableModel(
250                getCacheStats(cache),
251                new String[]{tr("Cache name"), tr("Object Count"), tr("Clear")}) {
252            @Override
253            public boolean isCellEditable(int row, int column) {
254                return column == 2;
255            }
256        };
257    }
258}