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
018package org.apache.logging.log4j.core.config.status;
019
020import java.io.File;
021import java.io.FileNotFoundException;
022import java.io.FileOutputStream;
023import java.io.PrintStream;
024import java.net.URI;
025import java.net.URISyntaxException;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.LinkedList;
029
030import org.apache.logging.log4j.Level;
031import org.apache.logging.log4j.core.util.FileUtils;
032import org.apache.logging.log4j.status.StatusConsoleListener;
033import org.apache.logging.log4j.status.StatusListener;
034import org.apache.logging.log4j.status.StatusLogger;
035
036/**
037 * Configuration for setting up {@link StatusConsoleListener} instances.
038 */
039public 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}