001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.startupmanager.options;
029
030import java.awt.event.ItemEvent;
031
032import javax.swing.JComboBox;
033import javax.swing.SwingUtilities;
034
035import org.slf4j.LoggerFactory;
036
037import ch.qos.logback.classic.Level;
038import ch.qos.logback.classic.Logger;
039
040import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.OptionPlatform;
041import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Type;
042import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Visibility;
043import edu.wisc.ssec.mcidasv.util.MakeToString;
044import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
045
046/**
047 * Representation of a choice allowing the user to select the global McIDAS-V
048 * logging level.
049 */
050public class LoggerLevelOption extends AbstractOption {
051    
052    /** 
053     * {@code String} representation of Logback's {@literal "TRACE"} logging 
054     * level. 
055     */
056    private static final String TRACE = Level.TRACE.toString();
057    
058    /** 
059     * {@code String} representation of Logback's {@literal "DEBUG"} logging 
060     * level. 
061     */
062    private static final String DEBUG = Level.DEBUG.toString();
063    
064    /** 
065     * {@code String} representation of Logback's {@literal "INFO"} logging 
066     * level. 
067     */
068    private static final String INFO = Level.INFO.toString();
069    
070    /** 
071     * {@code String} representation of Logback's {@literal "WARN"} logging 
072     * level. 
073     */
074    private static final String WARN = Level.WARN.toString();
075    
076    /** 
077     * {@code String} representation of Logback's {@literal "ERROR"} logging 
078     * level. 
079     */
080    private static final String ERROR = Level.ERROR.toString();
081    
082    /** 
083     * {@code String} representation of Logback's {@literal "OFF"} logging 
084     * level. 
085     */
086    private static final String OFF = Level.OFF.toString();
087    
088    /** 
089     * {@code JComboBox} that will eventually contain logging levels to 
090     * select. May be {@code null}. 
091     */
092    private JComboBox<String> comboBox;
093    
094    /** 
095     * {@code String} representation of the user's selection, or the default
096     * value provided to the constructor. 
097     */
098    private String currentChoice;
099    
100    /**
101     * Create a startup option that allows the user to manipulate the global
102     * McIDAS-V logging level. <B>NOTE:</b> {@code null} is not a permitted 
103     * value for any of this constructor's parameters.
104     * 
105     * @param id Identifier for this startup option.
106     * @param label Brief description suitable for a GUI label.
107     * @param defaultValue Default value for this startup option.
108     * @param optionPlatform Platforms where this option may be applied.
109     * @param optionVisibility Whether or not the option is presented via the GUI.
110     * 
111     * @throws IllegalArgumentException if {@code defaultValue} failed {@link #isValidValue(String)}.
112     */
113    public LoggerLevelOption(String id, String label, String defaultValue,
114            OptionPlatform optionPlatform, Visibility optionVisibility) {
115        super(id, label, Type.LOGLEVEL, optionPlatform, optionVisibility);
116        if (!isValidValue(defaultValue)) {
117            throw new IllegalArgumentException("Default value '"+defaultValue+"' is not one of: TRACE, DEBUG, INFO, WARN, ERROR, or OFF.");
118        }
119        currentChoice = defaultValue;
120    }
121    
122    /**
123     * Builds a {@link JComboBox} containing the logging levels to select.
124     * Defaults to the {@code String} specified in the constructor.
125     * 
126     * @return {@code JComboBox} to present to the user.
127     */
128    @Override public JComboBox<String> getComponent() {
129        comboBox = new JComboBox<>(new String[] { TRACE, DEBUG, INFO, WARN, ERROR, OFF });
130        comboBox.setSelectedItem(currentChoice);
131        comboBox.addItemListener(e -> setValue(comboBox.getSelectedItem().toString()));
132        
133        McVGuiUtils.setComponentWidth(comboBox, McVGuiUtils.Width.ONEHALF);
134        return comboBox;
135    }
136    
137    /**
138     * Returns the user's current selection (or the default value).
139     * 
140     * @return Current selection or default value.
141     */
142    @Override public String getValue() {
143        return currentChoice;
144    }
145    
146    /**
147     * Stores the user's selected logging level. Note that this can be called
148     * from third-party or the GUI! If the call originates from the GUI, an
149     * infinite loop is avoided by using the {@link JComboBox#setSelectedItem(Object)}
150     * behavior that does <b>not</b> generate {@link ItemEvent ItemEvents} if
151     * the selection did not actually change.
152     * 
153     * @param value {@code String} representation of the desired logging 
154     * level. Should not be {@code null}.
155     * 
156     * @throws IllegalArgumentException if {@code value} failed
157     * {@link #isValidValue(String)}.
158     */
159    @Override public void setValue(String value) {
160        if (!isValidValue(value)) {
161            throw new IllegalArgumentException("Value '"+value+"' is not one of: TRACE, DEBUG, INFO, WARN, ERROR, or OFF.");
162        }
163        currentChoice = value;
164        Logger rootLogger = (Logger)LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
165        rootLogger.setLevel(stringToLogback(value));
166        SwingUtilities.invokeLater(() -> {
167            if (comboBox != null) {
168                comboBox.setSelectedItem(currentChoice);
169            }
170        });
171    }
172    
173    /**
174     * Converts a {@code String} value to the corresponding logging level.
175     * 
176     * <p>This functionality is similar to 
177     * {@link Level#toLevel(String, Level)}, but for this use case it is 
178     * preferable to know if an invalid {@code value} was provided.</p>
179     * 
180     * @param value Value to convert.
181     * 
182     * @return Logging level.
183     * 
184     * @throws IllegalArgumentException if {@code value} did not have a 
185     * corresponding logging level.
186     */
187    private static Level stringToLogback(final String value) {
188        Level level;
189        if (TRACE.equalsIgnoreCase(value)) {
190            level = Level.TRACE;
191        } else if (DEBUG.equalsIgnoreCase(value)) {
192            level = Level.DEBUG;
193        } else if (INFO.equalsIgnoreCase(value)) {
194            level = Level.INFO;
195        } else if (WARN.equalsIgnoreCase(value)) {
196            level = Level.WARN;
197        } else if (ERROR.equalsIgnoreCase(value)) {
198            level = Level.ERROR;
199        } else if (OFF.equalsIgnoreCase(value)) {
200            level = Level.OFF;
201        } else {
202            throw new IllegalArgumentException();
203        }
204        return level;
205    }
206    
207    /**
208     * Tests a {@code String} value to see if it has a corresponding logging
209     * level.
210     * 
211     * @param value Value to test.
212     * 
213     * @return {@code true} if-and-only-if passes a 
214     * {@link String#equalsIgnoreCase(String)} check against {@link #TRACE}, 
215     * {@link #DEBUG}, {@link #INFO}, {@link #WARN}, {@link #ERROR}, or 
216     * {@link #OFF}.
217     */
218    private static boolean isValidValue(final String value) {
219        return (TRACE.equalsIgnoreCase(value) || 
220                DEBUG.equalsIgnoreCase(value) || 
221                INFO.equalsIgnoreCase(value) || 
222                WARN.equalsIgnoreCase(value) || 
223                ERROR.equalsIgnoreCase(value) || 
224                OFF.equalsIgnoreCase(value));
225    }
226    
227    /**
228     * {@code String} representation of the user's logging level selection.
229     * 
230     * @return {@code String} that looks something like 
231     * {@literal "[LoggerLevel@7825114a: currentChoice=INFO]"}.
232     */
233    public String toString() {
234        return MakeToString.fromInstance(this)
235                           .add("currentChoice", currentChoice).toString();
236    }
237}