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(Action action) {
074            this.action = action;
075            renderButton = new JButton();
076            editButton = new JButton();
077            editButton.setFocusPainted(false);
078            editButton.addActionListener(this);
079            editButton.setBorder(new LineBorder(Color.BLUE));
080        }
081
082        @Override
083        public Object getCellEditorValue() {
084            return editorValue;
085        }
086
087        @Override
088        public void actionPerformed(ActionEvent e) {
089            this.action.actionPerformed(e);
090        }
091
092        @Override
093        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
094            this.editorValue = value;
095            if (value == null) {
096                editButton.setText("");
097                editButton.setIcon(null);
098            } else if (value instanceof Icon) {
099                editButton.setText("");
100                editButton.setIcon((Icon) value);
101            } else {
102                editButton.setText(value.toString());
103                editButton.setIcon(null);
104            }
105            this.editorValue = value;
106            return editButton;
107        }
108
109        @Override
110        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
111                boolean hasFocus, int row, int column) {
112            if (isSelected) {
113                renderButton.setForeground(table.getSelectionForeground());
114                renderButton.setBackground(table.getSelectionBackground());
115            } else {
116                renderButton.setForeground(table.getForeground());
117                renderButton.setBackground(UIManager.getColor("Button.background"));
118            }
119
120            renderButton.setFocusPainted(hasFocus);
121
122            if (value == null) {
123                renderButton.setText("");
124                renderButton.setIcon(null);
125            } else if (value instanceof Icon) {
126                renderButton.setText("");
127                renderButton.setIcon((Icon) value);
128            } else {
129                renderButton.setText(value.toString());
130                renderButton.setIcon(null);
131            }
132            return renderButton;
133        }
134
135    }
136
137    private final transient ExecutorService executor =
138            Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
139
140    /**
141     * Creates cache content panel
142     */
143    public CacheContentsPanel() {
144        super(new GridBagLayout());
145        executor.submit(new Runnable() {
146            @Override
147            public void run() {
148                addToPanel(TMSLayer.getCache(), "TMS");
149                addToPanel(WMSLayer.getCache(), "WMS");
150                addToPanel(WMTSLayer.getCache(), "WMTS");
151            }
152        });
153        executor.shutdown();
154    }
155
156    private void addToPanel(final CacheAccess<String, BufferedImageCacheEntry> cache, final String name) {
157        final Long cacheSize = getCacheSize(cache);
158        final TableModel tableModel = getTableModel(cache);
159
160        GuiHelper.runInEDT(new Runnable() {
161            @Override
162            public void run() {
163                add(
164                        new JLabel(tr("{0} cache, total cache size: {1} bytes", name, cacheSize)),
165                        GBC.eol().insets(5, 5, 0, 0));
166
167                add(
168                        new JScrollPane(getTableForCache(cache, tableModel)),
169                        GBC.eol().fill(GBC.BOTH));
170            }
171        });
172    }
173
174    private static Long getCacheSize(CacheAccess<String, BufferedImageCacheEntry> cache) {
175        ICacheStats stats = cache.getStatistics();
176        for (IStats cacheStats: stats.getAuxiliaryCacheStats()) {
177            for (IStatElement<?> statElement: cacheStats.getStatElements()) {
178                if ("Data File Length".equals(statElement.getName())) {
179                    Object val = statElement.getData();
180                    if (val instanceof Long) {
181                        return (Long) val;
182                    }
183
184                }
185            }
186        }
187        return Long.valueOf(-1);
188    }
189
190    private static String[][] getCacheStats(CacheAccess<String, BufferedImageCacheEntry> cache) {
191        Set<String> keySet = cache.getCacheControl().getKeySet();
192        Map<String, int[]> temp = new ConcurrentHashMap<>(); // use int[] as a Object reference to int, gives better performance
193        for (String key: keySet) {
194            String[] keyParts = key.split(":", 2);
195            if (keyParts.length == 2) {
196                int[] counter = temp.get(keyParts[0]);
197                if (counter == null) {
198                    temp.put(keyParts[0], new int[]{1});
199                } else {
200                    counter[0]++;
201                }
202            } else {
203                Main.warn("Could not parse the key: {0}. No colon found", key);
204            }
205        }
206
207        List<Pair<String, Integer>> sortedStats = new ArrayList<>();
208        for (Entry<String, int[]> e: temp.entrySet()) {
209            sortedStats.add(new Pair<>(e.getKey(), e.getValue()[0]));
210        }
211        Collections.sort(sortedStats, new Comparator<Pair<String, Integer>>() {
212            @Override
213            public int compare(Pair<String, Integer> o1, Pair<String, Integer> o2) {
214                return -1 * o1.b.compareTo(o2.b);
215            }
216        });
217        String[][] ret = new String[sortedStats.size()][3];
218        int index = 0;
219        for (Pair<String, Integer> e: sortedStats) {
220            ret[index] = new String[]{e.a, e.b.toString(), tr("Clear")};
221            index++;
222        }
223        return ret;
224    }
225
226    private JTable getTableForCache(final CacheAccess<String, BufferedImageCacheEntry> cache, final TableModel tableModel) {
227        final JTable ret = new JTable(tableModel);
228
229        ButtonColumn buttonColumn = new ButtonColumn(
230                new AbstractAction() {
231                    @Override
232                    public void actionPerformed(ActionEvent e) {
233                        int row = ret.convertRowIndexToModel(ret.getEditingRow());
234                        tableModel.setValueAt("0", row, 1);
235                        cache.remove(ret.getValueAt(row, 0) + ":");
236                    }
237                });
238        TableColumn tableColumn = ret.getColumnModel().getColumn(2);
239        tableColumn.setCellRenderer(buttonColumn);
240        tableColumn.setCellEditor(buttonColumn);
241        return ret;
242    }
243
244    private static DefaultTableModel getTableModel(final CacheAccess<String, BufferedImageCacheEntry> cache) {
245        final DefaultTableModel tableModel = new DefaultTableModel(
246                getCacheStats(cache),
247                new String[]{tr("Cache name"), tr("Object Count"), tr("Clear")}) {
248            @Override
249            public boolean isCellEditable(int row, int column) {
250                return column == 2;
251            }
252        };
253        return tableModel;
254    }
255}