001package apps.jmrit.log;
002
003import java.awt.BorderLayout;
004import java.awt.FlowLayout;
005import java.awt.Dimension;
006import java.awt.event.ActionEvent;
007
008import java.util.*;
009
010import javax.swing.*;
011
012import org.apache.logging.log4j.Level;
013import org.apache.logging.log4j.Logger;
014import org.apache.logging.log4j.LogManager;
015import org.apache.logging.log4j.core.LoggerContext;
016import org.apache.logging.log4j.core.config.*;
017
018/**
019 * Show the current Log4J Logger tree.
020 *
021 * @author Bob Jacobsen Copyright 2010
022 * @author Steve Young Copyright(C) 2023
023 * @since 2.9.4
024 */
025public class Log4JTreePane extends jmri.util.swing.JmriPanel {
026
027    private JTextArea text;
028    private JScrollPane scroll;
029    private JComboBox<Level> levelSelectionComboBox;
030    private JComboBox<String> categoryComboBox;
031    private final static String ROOT_LEVEL_STRING = Bundle.getMessage("DataItemRootLoggingLevel");
032    private final static Level[] SELECTABLE_LEVELS = new Level[]{ Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.OFF};
033
034    /**
035     * Provide a recommended title for an enclosing frame.
036     */
037    @Override
038    public String getTitle() {
039        return Bundle.getMessage("MenuItemLogTreeAction");
040    }
041
042    public Log4JTreePane() {
043        setLayout(new BorderLayout());
044    }
045
046    /**
047     * 2nd stage of initialization, invoked after the constructor is complete.
048     * 
049     * Sets up the entire dialog box
050     */
051    @SuppressWarnings("unchecked")
052    @Override
053    public void initComponents() {
054
055        add(getEditLoggingLevelPanel(), BorderLayout.SOUTH);
056
057        text = new JTextArea();
058        text.setEditable(false);
059        scroll = new JScrollPane(text);
060        updateTextAreaAndCategorySelect();
061
062        add(scroll, BorderLayout.CENTER);
063
064        JPanel topP = new JPanel(new FlowLayout());
065        JButton refreshButton = new JButton(Bundle.getMessage("ButtonRefreshCategories"));
066        refreshButton.addActionListener(this::refreshButtonPressed);
067        topP.add(refreshButton);
068        topP.add(new JLabel(Bundle.getMessage("FieldLogConfiguredInherited")));
069        add(topP,BorderLayout.NORTH);
070        
071        // start scrolled to top
072        text.setCaretPosition(0);
073        JScrollBar b = scroll.getVerticalScrollBar();
074        b.setValue(b.getMaximum());
075    }
076
077    /**
078     * Set up the bottom part of the dialog where the user can change logging levels.
079     * @return The JPanel, ready to use.
080     */
081    private JPanel getEditLoggingLevelPanel() {
082
083        JPanel p = new JPanel();
084        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
085
086        // Create a label explaining the combobox.
087        JPanel categoryLabelPanel = new JPanel(new FlowLayout());
088        categoryLabelPanel.add(new JLabel(Bundle.getMessage("LabelCategoryToChange")));
089
090        // Create a JComboBox and populate it with logger names
091        categoryComboBox = new JComboBox<>();
092        jmri.util.swing.JComboBoxUtil.setupComboBoxMaxRows(categoryComboBox);
093        categoryComboBox.setToolTipText(Bundle.getMessage("ToolTipSelectLoggingLevel"));
094        categoryComboBox.setEditable(true);
095
096        JPanel catergoryComboboxPanel = new JPanel(new FlowLayout());
097        catergoryComboboxPanel.add(categoryComboBox);
098
099        levelSelectionComboBox = new JComboBox<>(SELECTABLE_LEVELS);
100        jmri.util.swing.JComboBoxUtil.setupComboBoxMaxRows(levelSelectionComboBox);
101        levelSelectionComboBox.setSelectedItem(Level.DEBUG);
102
103        // Expand the button slightly to avoid elipsis
104        Dimension preferredSize = levelSelectionComboBox.getPreferredSize();
105        preferredSize.width = (int) (preferredSize.width * 1.2);
106        levelSelectionComboBox.setPreferredSize(preferredSize);
107        levelSelectionComboBox.setToolTipText(Bundle.getMessage("ToolTipSelectLoggingLevelValue"));
108
109        JPanel levelSelectPanel = new JPanel(new FlowLayout());
110        levelSelectPanel.add(new JLabel(Bundle.getMessage("LabelNewLevelForAboveCategory")));
111        levelSelectPanel.add(levelSelectionComboBox);
112
113        JButton setLevelButton = new JButton(Bundle.getMessage("ButtonEditLoggingLevel"));
114        setLevelButton.setToolTipText(Bundle.getMessage("ToolTipEditLoggingLevel"));
115        setLevelButton.addActionListener(this::editButtonPressed);
116
117        JPanel setButtonPanel = new JPanel(new FlowLayout());
118        setButtonPanel.add(setLevelButton);
119
120        p.add(categoryLabelPanel);
121        p.add(catergoryComboboxPanel);
122        p.add(levelSelectPanel);
123        p.add(setButtonPanel);
124        return p;
125    }
126
127    private void updateTextAreaAndCategorySelect(){
128        List<LoggerInfo> loggersWithLevels = Log4JTreePane.getAllLoggersWithLevels();
129        populateTextArea(loggersWithLevels);
130        updateCategoryComboBox(loggersWithLevels);
131    }
132
133    private static List<LoggerInfo> getAllLoggersWithLevels() {
134        HashMap<String, LoggerInfo> hs = new HashMap<>();
135        LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
136        for (Logger category : loggerContext.getLoggers()) {
137            hs.put(category.getName(), new LoggerInfo(category.getName() , category.getLevel()));
138        }
139
140        Configuration config = loggerContext.getConfiguration();
141        Map<String, LoggerConfig> mm = config.getLoggers(); // does not include root logger
142        mm.forEach((key, value) -> {
143            hs.putIfAbsent(key, new LoggerInfo(value.getName() , value.getLevel()));
144            hs.get(key).setInConfig();
145        });
146        // ensure root logger in list
147        hs.putIfAbsent("", new LoggerInfo("" , config.getRootLogger().getLevel()));
148        hs.get("").setInConfig();
149
150        List<LoggerInfo> valuesList = new ArrayList<>(hs.values());
151        Collections.sort(valuesList); // sorts by real name alphabetically
152        return valuesList;
153    }
154
155    private void populateTextArea(List<LoggerInfo> loggers){
156        StringBuilder result = new StringBuilder();
157        for (LoggerInfo s : loggers) {
158            result.append("  ").append(s.toString()).append(System.lineSeparator());
159        }
160
161        JScrollBar b = scroll.getVerticalScrollBar();
162        int beforeScroll = b.getValue();
163        int caret = text.getCaretPosition();
164        text.setText(result.toString());
165        text.setCaretPosition(caret);
166        b.setValue(beforeScroll);
167    }
168
169    private void updateCategoryComboBox(List<LoggerInfo> loggers) {
170        String f = (String)categoryComboBox.getSelectedItem();
171        categoryComboBox.removeAllItems();
172        for ( LoggerInfo l : loggers ) {
173            categoryComboBox.addItem(l.getLoggerName());
174        }
175        categoryComboBox.setSelectedItem(f == null ? ROOT_LEVEL_STRING : f);
176    }
177
178    private void editButtonPressed(ActionEvent e) {
179        log.debug("{} pressed", e.getActionCommand());
180        String f = (String)categoryComboBox.getSelectedItem();
181        Level l = (Level)levelSelectionComboBox.getSelectedItem();
182        log.info("changing Logging for {} to {}",f,l);
183        if ( ROOT_LEVEL_STRING.equals(f) ){
184            f=""; // empty String is actual name for root logger.
185        }
186        Configurator.setLevel(LogManager.getLogger(f), l);
187        updateTextAreaAndCategorySelect();
188    }
189
190    private void refreshButtonPressed(ActionEvent e){
191        log.debug("{} pressed", e.getActionCommand());
192        updateTextAreaAndCategorySelect();
193    }
194
195    /**
196     * 3rd stage of initialization, invoked after Swing components exist.
197     */
198    @Override
199    public void initContext(Object context) {
200    }
201
202    private static class LoggerInfo implements Comparable<LoggerInfo> {
203
204        private final String loggerName;
205        private final Level level;
206        private boolean inConfig = false;
207
208        private LoggerInfo(String loggerName, Level level) {
209            this.loggerName = loggerName;
210            this.level = level;
211        }
212
213        String getLoggerName() {
214            return (loggerName.isBlank() ? ROOT_LEVEL_STRING : loggerName);
215        }
216
217        private void setInConfig(){
218            inConfig = true;
219        }
220
221        @Override
222        public int compareTo(LoggerInfo other) {
223            return this.loggerName.compareTo(other.loggerName);
224        }
225
226        @Override
227        public String toString() {
228            StringBuilder s = new StringBuilder();
229            s.append(getLoggerName()).append("  ");
230            if ( !inConfig ){
231                s.append("{ ").append(level.name()).append(" }");
232            } else {
233                s.append("[ ").append(level.name()).append(" ]");
234            }
235            return  s.toString();
236        }
237
238        @Override
239        public boolean equals(Object obj) {
240            if (this == obj) {
241                return true;
242            }
243            if (!(obj instanceof LoggerInfo)) {
244                return false;
245            }
246            LoggerInfo other = (LoggerInfo) obj;
247            return Objects.equals(loggerName, other.loggerName) && Objects.equals(level, other.level);
248        }
249
250        @Override
251        public int hashCode() {
252            return 13 * Objects.hashCode(loggerName) * Objects.hashCode(level);
253        }
254
255    }
256
257    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Log4JTreePane.class);
258
259}