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.core.util.NetUtils;
033import org.apache.logging.log4j.status.StatusConsoleListener;
034import org.apache.logging.log4j.status.StatusListener;
035import org.apache.logging.log4j.status.StatusLogger;
036
037/**
038 * Configuration for setting up {@link StatusConsoleListener} instances.
039 */
040public class StatusConfiguration {
041
042    @SuppressWarnings("UseOfSystemOutOrSystemErr")
043    private static final PrintStream DEFAULT_STREAM = System.out;
044    private static final Level DEFAULT_STATUS = Level.ERROR;
045    private static final Verbosity DEFAULT_VERBOSITY = Verbosity.QUIET;
046
047    private final Collection<String> errorMessages = Collections.synchronizedCollection(new LinkedList<String>());
048    private final StatusLogger logger = StatusLogger.getLogger();
049
050    private volatile boolean initialized = false;
051
052    private PrintStream destination = DEFAULT_STREAM;
053    private Level status = DEFAULT_STATUS;
054    private Verbosity verbosity = DEFAULT_VERBOSITY;
055    private String[] verboseClasses;
056
057    /**
058     * Specifies how verbose the StatusLogger should be.
059     */
060    public enum Verbosity {
061        QUIET, VERBOSE;
062
063        /**
064         * Parses the verbosity property into an enum.
065         *
066         * @param value property value to parse.
067         * @return enum corresponding to value, or QUIET by default.
068         */
069        public static Verbosity toVerbosity(final String value) {
070            return Boolean.parseBoolean(value) ? VERBOSE : QUIET;
071        }
072    }
073
074    /**
075     * Logs an error message to the StatusLogger. If the StatusLogger hasn't been set up yet, queues the message to be
076     * logged after initialization.
077     *
078     * @param message error message to log.
079     */
080    public void error(final String message) {
081        if (!this.initialized) {
082            this.errorMessages.add(message);
083        } else {
084            this.logger.error(message);
085        }
086    }
087
088    /**
089     * Specifies the destination for StatusLogger events. This can be {@code out} (default) for using
090     * {@link System#out standard out}, {@code err} for using {@link System#err standard error}, or a file URI to
091     * which log events will be written. If the provided URI is invalid, then the default destination of standard
092     * out will be used.
093     *
094     * @param destination where status log messages should be output.
095     * @return {@code this}
096     */
097    public StatusConfiguration withDestination(final String destination) {
098        try {
099            this.destination = parseStreamName(destination);
100        } catch (final URISyntaxException e) {
101            this.error("Could not parse URI [" + destination + "]. Falling back to default of stdout.");
102            this.destination = DEFAULT_STREAM;
103        } catch (final FileNotFoundException e) {
104            this.error("File could not be found at [" + destination + "]. Falling back to default of stdout.");
105            this.destination = DEFAULT_STREAM;
106        }
107        return this;
108    }
109
110    private PrintStream parseStreamName(final String name) throws URISyntaxException, FileNotFoundException {
111        if (name == null || name.equalsIgnoreCase("out")) {
112            return DEFAULT_STREAM;
113        }
114        if (name.equalsIgnoreCase("err")) {
115            return System.err;
116        }
117        final URI destUri = NetUtils.toURI(name);
118        final File output = FileUtils.fileFromUri(destUri);
119        if (output == null) {
120            // don't want any NPEs, no sir
121            return DEFAULT_STREAM;
122        }
123        final FileOutputStream fos = new FileOutputStream(output);
124        return new PrintStream(fos, true);
125    }
126
127    /**
128     * Specifies the logging level by name to use for filtering StatusLogger messages.
129     *
130     * @param status name of logger level to filter below.
131     * @return {@code this}
132     * @see Level
133     */
134    public StatusConfiguration withStatus(final String status) {
135        this.status = Level.toLevel(status, null);
136        if (this.status == null) {
137            this.error("Invalid status level specified: " + status + ". Defaulting to ERROR.");
138            this.status = Level.ERROR;
139        }
140        return this;
141    }
142
143    /**
144     * Specifies the logging level to use for filtering StatusLogger messages.
145     *
146     * @param status logger level to filter below.
147     * @return {@code this}
148     */
149    public StatusConfiguration withStatus(final Level status) {
150        this.status = status;
151        return this;
152    }
153
154    /**
155     * Specifies the verbosity level to log at. This only applies to classes configured by
156     * {@link #withVerboseClasses(String...) verboseClasses}.
157     *
158     * @param verbosity basic filter for status logger messages.
159     * @return {@code this}
160     */
161    public StatusConfiguration withVerbosity(final String verbosity) {
162        this.verbosity = Verbosity.toVerbosity(verbosity);
163        return this;
164    }
165
166    /**
167     * Specifies which class names to filter if the configured verbosity level is QUIET.
168     *
169     * @param verboseClasses names of classes to filter if not using VERBOSE.
170     * @return {@code this}
171     */
172    public StatusConfiguration withVerboseClasses(final String... verboseClasses) {
173        this.verboseClasses = verboseClasses;
174        return this;
175    }
176
177    /**
178     * Configures and initializes the StatusLogger using the configured options in this instance.
179     */
180    public void initialize() {
181        if (!this.initialized) {
182            if (this.status == Level.OFF) {
183                this.initialized = true;
184            } else {
185                final boolean configured = configureExistingStatusConsoleListener();
186                if (!configured) {
187                    registerNewStatusConsoleListener();
188                }
189                migrateSavedLogMessages();
190            }
191        }
192    }
193
194    private boolean configureExistingStatusConsoleListener() {
195        boolean configured = false;
196        for (final StatusListener statusListener : this.logger.getListeners()) {
197            if (statusListener instanceof StatusConsoleListener) {
198                final StatusConsoleListener listener = (StatusConsoleListener) statusListener;
199                listener.setLevel(this.status);
200                this.logger.updateListenerLevel(this.status);
201                if (this.verbosity == Verbosity.QUIET) {
202                    listener.setFilters(this.verboseClasses);
203                }
204                configured = true;
205            }
206        }
207        return configured;
208    }
209
210
211    private void registerNewStatusConsoleListener() {
212        final StatusConsoleListener listener = new StatusConsoleListener(this.status, this.destination);
213        if (this.verbosity == Verbosity.QUIET) {
214            listener.setFilters(this.verboseClasses);
215        }
216        this.logger.registerListener(listener);
217    }
218
219    private void migrateSavedLogMessages() {
220        for (final String message : this.errorMessages) {
221            this.logger.error(message);
222        }
223        this.initialized = true;
224        this.errorMessages.clear();
225    }
226}