001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License. You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.logging.log4j.core.config.status;
019    
020    import java.io.File;
021    import java.io.FileNotFoundException;
022    import java.io.FileOutputStream;
023    import java.io.PrintStream;
024    import java.net.URI;
025    import java.net.URISyntaxException;
026    import java.util.Collection;
027    import java.util.Collections;
028    import java.util.LinkedList;
029    
030    import org.apache.logging.log4j.Level;
031    import org.apache.logging.log4j.core.util.FileUtils;
032    import org.apache.logging.log4j.status.StatusConsoleListener;
033    import org.apache.logging.log4j.status.StatusListener;
034    import org.apache.logging.log4j.status.StatusLogger;
035    
036    /**
037     * Configuration for setting up {@link StatusConsoleListener} instances.
038     */
039    public class StatusConfiguration {
040    
041        @SuppressWarnings("UseOfSystemOutOrSystemErr")
042        private static final PrintStream DEFAULT_STREAM = System.out;
043        private static final Level DEFAULT_STATUS = Level.ERROR;
044        private static final Verbosity DEFAULT_VERBOSITY = Verbosity.QUIET;
045    
046        private final Collection<String> errorMessages = Collections.synchronizedCollection(new LinkedList<String>());
047        private final StatusLogger logger = StatusLogger.getLogger();
048    
049        private volatile boolean initialized = false;
050    
051        private PrintStream destination = DEFAULT_STREAM;
052        private Level status = DEFAULT_STATUS;
053        private Verbosity verbosity = DEFAULT_VERBOSITY;
054        private String[] verboseClasses;
055    
056        /**
057         * Specifies how verbose the StatusLogger should be.
058         */
059        public static enum Verbosity {
060            QUIET, VERBOSE;
061    
062            /**
063             * Parses the verbosity property into an enum.
064             *
065             * @param value property value to parse.
066             * @return enum corresponding to value, or QUIET by default.
067             */
068            public static Verbosity toVerbosity(final String value) {
069                return Boolean.parseBoolean(value) ? VERBOSE : QUIET;
070            }
071        }
072    
073        /**
074         * Logs an error message to the StatusLogger. If the StatusLogger hasn't been set up yet, queues the message to be
075         * logged after initialization.
076         *
077         * @param message error message to log.
078         */
079        public void error(final String message) {
080            if (!this.initialized) {
081                this.errorMessages.add(message);
082            } else {
083                this.logger.error(message);
084            }
085        }
086    
087        /**
088         * Specifies the destination for StatusLogger events. This can be {@code out} (default) for using
089         * {@link System#out standard out}, {@code err} for using {@link System#err standard error}, or a file URI to
090         * which log events will be written. If the provided URI is invalid, then the default destination of standard
091         * out will be used.
092         *
093         * @param destination where status log messages should be output.
094         * @return {@code this}
095         */
096        public StatusConfiguration withDestination(final String destination) {
097            try {
098                this.destination = parseStreamName(destination);
099            } catch (final URISyntaxException e) {
100                this.error("Could not parse URI [" + destination + "]. Falling back to default of stdout.");
101                this.destination = DEFAULT_STREAM;
102            } catch (final FileNotFoundException e) {
103                this.error("File could not be found at [" + destination + "]. Falling back to default of stdout.");
104                this.destination = DEFAULT_STREAM;
105            }
106            return this;
107        }
108    
109        private PrintStream parseStreamName(final String name) throws URISyntaxException, FileNotFoundException {
110            if (name == null || name.equalsIgnoreCase("out")) {
111                return DEFAULT_STREAM;
112            }
113            if (name.equalsIgnoreCase("err")) {
114                return System.err;
115            }
116            final URI destination = FileUtils.getCorrectedFilePathUri(name);
117            final File output = FileUtils.fileFromUri(destination);
118            if (output == null) {
119                // don't want any NPEs, no sir
120                return DEFAULT_STREAM;
121            }
122            final FileOutputStream fos = new FileOutputStream(output);
123            return new PrintStream(fos, true);
124        }
125    
126        /**
127         * Specifies the logging level by name to use for filtering StatusLogger messages.
128         *
129         * @param status name of logger level to filter below.
130         * @return {@code this}
131         * @see Level
132         */
133        public StatusConfiguration withStatus(final String status) {
134            this.status = Level.toLevel(status, null);
135            if (this.status == null) {
136                this.error("Invalid status level specified: " + status + ". Defaulting to ERROR.");
137                this.status = Level.ERROR;
138            }
139            return this;
140        }
141    
142        /**
143         * Specifies the logging level to use for filtering StatusLogger messages.
144         *
145         * @param status logger level to filter below.
146         * @return {@code this}
147         */
148        public StatusConfiguration withStatus(final Level status) {
149            this.status = status;
150            return this;
151        }
152    
153        /**
154         * Specifies the verbosity level to log at. This only applies to classes configured by
155         * {@link #withVerboseClasses(String...) verboseClasses}.
156         *
157         * @param verbosity basic filter for status logger messages.
158         * @return {@code this}
159         */
160        public StatusConfiguration withVerbosity(final String verbosity) {
161            this.verbosity = Verbosity.toVerbosity(verbosity);
162            return this;
163        }
164    
165        /**
166         * Specifies which class names to filter if the configured verbosity level is QUIET.
167         *
168         * @param verboseClasses names of classes to filter if not using VERBOSE.
169         * @return {@code this}
170         */
171        public StatusConfiguration withVerboseClasses(final String... verboseClasses) {
172            this.verboseClasses = verboseClasses;
173            return this;
174        }
175    
176        /**
177         * Configures and initializes the StatusLogger using the configured options in this instance.
178         */
179        public void initialize() {
180            if (!this.initialized) {
181                if (this.status == Level.OFF) {
182                    this.initialized = true;
183                } else {
184                    final boolean configured = configureExistingStatusConsoleListener();
185                    if (!configured) {
186                        registerNewStatusConsoleListener();
187                    }
188                    migrateSavedLogMessages();
189                }
190            }
191        }
192    
193        private boolean configureExistingStatusConsoleListener() {
194            boolean configured = false;
195            for (final StatusListener statusListener : this.logger.getListeners()) {
196                if (statusListener instanceof StatusConsoleListener) {
197                    final StatusConsoleListener listener = (StatusConsoleListener) statusListener;
198                    listener.setLevel(this.status);
199                    if (this.verbosity == Verbosity.QUIET) {
200                        listener.setFilters(this.verboseClasses);
201                    }
202                    configured = true;
203                }
204            }
205            return configured;
206        }
207    
208    
209        private void registerNewStatusConsoleListener() {
210            final StatusConsoleListener listener = new StatusConsoleListener(this.status, this.destination);
211            if (this.verbosity == Verbosity.QUIET) {
212                listener.setFilters(this.verboseClasses);
213            }
214            this.logger.registerListener(listener);
215        }
216    
217        private void migrateSavedLogMessages() {
218            for (final String message : this.errorMessages) {
219                this.logger.error(message);
220            }
221            this.initialized = true;
222            this.errorMessages.clear();
223        }
224    }