blob: 08d75d4fde9e3376adee318acc1f4f03fc61e4fe [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache license, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the license for the specific language governing permissions and
* limitations under the license.
*/
package org.apache.logging.log4j.io;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Objects;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LoggingException;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.spi.ExtendedLogger;
import org.apache.logging.log4j.util.StackLocatorUtil;
/**
* Builder class to wrap {@link Logger Loggers} into Java IO compatible classes.
*
* <p>Both the {@link InputStream}/{@link OutputStream} and {@link Reader}/{@link Writer} family of classes are
* supported. {@link OutputStream} and {@link Writer} instances can be wrapped by a filtered version of their
* corresponding classes ({@link java.io.FilterOutputStream} and {@link java.io.FilterWriter}) in order to log all
* lines written to these instances. {@link InputStream} and {@link Reader} instances can be wrapped by a sort of
* wiretapped version of their respective classes; all lines read from these instances will be logged.</p>
*
* <p>The main feature, however, is the ability to create a {@link PrintWriter}, {@link PrintStream}, {@link Writer},
* {@link java.io.BufferedWriter}, {@link OutputStream}, or {@link java.io.BufferedOutputStream} that is backed by a
* {@link Logger}. The main inspiration for this feature is the JDBC API which uses a PrintWriter to perform debug
* logging. In order to properly integrate APIs like JDBC into Log4j, create a PrintWriter using this class.</p>
*
* <p>The IoBuilder support configuration of the logging {@link Level} it should use (defaults to the level of
* the underlying Logger), and an optional {@link Marker}. The other configurable objects are explained in more
* detail below.</p>
*
* @since 2.1
*/
public class IoBuilder {
private final ExtendedLogger logger;
private Level level;
private Marker marker;
private String fqcn;
private boolean autoFlush;
private boolean buffered;
private int bufferSize;
private Charset charset;
private Reader reader;
private Writer writer;
private InputStream inputStream;
private OutputStream outputStream;
/**
* Creates a new builder for a given {@link Logger}. The Logger instance must implement {@link ExtendedLogger} or
* an exception will be thrown.
*
* @param logger the Logger to wrap into a LoggerStream
* @return a new IoBuilder
* @throws UnsupportedOperationException if {@code logger} does not implement {@link ExtendedLogger} or if
* {@code logger} is {@code null}
*/
public static IoBuilder forLogger(final Logger logger) {
return new IoBuilder(logger);
}
/**
* Creates a new builder using a Logger name. The name provided is used to get a Logger from
* {@link LogManager#getLogger(String)} which will be wrapped into a LoggerStream.
*
* @param loggerName the name of the Logger to wrap into a LoggerStream
* @return a new IoBuilder
*/
public static IoBuilder forLogger(final String loggerName) {
return new IoBuilder(LogManager.getLogger(loggerName));
}
/**
* Creates a new builder using a Logger named after a given Class. The Class provided is used to get a Logger from
* {@link LogManager#getLogger(Class)} which will be wrapped into a LoggerStream.
*
* @param clazz the Class to use as the Logger name to wrap into a LoggerStream
* @return a new IoBuilder
*/
public static IoBuilder forLogger(final Class<?> clazz) {
return new IoBuilder(LogManager.getLogger(clazz));
}
/**
* Creates a new builder using a Logger named after the calling Class. This is equivalent to the following:
* <pre>
* IoBuilder builder = IoBuilder.forLogger(LogManager.getLogger());
* </pre>
*
* @return a new IoBuilder
*/
public static IoBuilder forLogger() {
return new IoBuilder(LogManager.getLogger(StackLocatorUtil.getCallerClass(2)));
}
/**
* Constructs a new IoBuilder for the given Logger. This method is provided for extensibility of this builder
* class. The static factory methods should be used normally.
*
* @param logger the {@link ExtendedLogger} to wrap
*/
protected IoBuilder(final Logger logger) {
if (!(logger instanceof ExtendedLogger)) {
throw new UnsupportedOperationException("The provided Logger [" + String.valueOf(logger) +
"] does not implement " + ExtendedLogger.class.getName());
}
this.logger = (ExtendedLogger) logger;
}
/**
* Specifies the {@link Level} to log at. If no Level is configured, then the Level of the wrapped Logger will be
* used.
*
* @param level the Level to use for logging
* @return {@code this}
*/
public IoBuilder setLevel(final Level level) {
this.level = level;
return this;
}
/**
* Specifies an optional {@link Marker} to use in all logging messages. If no Marker is specified, then no Marker
* will be used.
*
* @param marker the Marker to associate with all logging messages
* @return {@code this}
*/
public IoBuilder setMarker(final Marker marker) {
this.marker = marker;
return this;
}
/**
* Specifies the fully qualified class name of the IO wrapper class implementation. This method should only be
* used when making significant extensions to the provided classes in this component and is normally unnecessary.
*
* @param fqcn the fully qualified class name of the IO wrapper class being built
* @return {@code this}
*/
public IoBuilder setWrapperClassName(final String fqcn) {
this.fqcn = fqcn;
return this;
}
/**
* Indicates whether or not a built {@link PrintWriter} or {@link PrintStream} should automatically flush when
* one of the {@code println}, {@code printf}, or {@code format} methods are invoked, or when a new line character
* is printed.
*
* @param autoFlush if {@code true}, then {@code println}, {@code printf}, and {@code format} will auto flush
* @return {@code this}
*/
public IoBuilder setAutoFlush(final boolean autoFlush) {
this.autoFlush = autoFlush;
return this;
}
/**
* Enables or disables using a buffered variant of the desired IO class. If this is set to {@code true}, then the
* instances returned by {@link #buildReader()} and {@link #buildInputStream()} can be safely cast (if necessary)
* to {@link java.io.BufferedReader} and {@link java.io.BufferedInputStream} respectively. This option does not
* have any effect on the other built variants.
*
* @param buffered indicates whether or not a wrapped {@link InputStream} or {@link Reader} should be buffered
* @return {@code this}
*/
public IoBuilder setBuffered(final boolean buffered) {
this.buffered = buffered;
return this;
}
/**
* Configures the buffer size to use when building a {@link java.io.BufferedReader} or
* {@link java.io.BufferedInputStream} LoggerStream.
*
* @param bufferSize the buffer size to use or a non-positive integer to use the default size
* @return {@code this}
*/
public IoBuilder setBufferSize(final int bufferSize) {
this.bufferSize = bufferSize;
return this;
}
/**
* Specifies the character set to use when building an {@link InputStream}, {@link OutputStream}, or
* {@link PrintStream}. If no character set is specified, then {@link java.nio.charset.Charset#defaultCharset()}
* is used.
*
* @param charset the character set to use when building an InputStream, OutputStream, or PrintStream
* @return {@code this}
*/
public IoBuilder setCharset(final Charset charset) {
this.charset = charset;
return this;
}
/**
* Configures a {@link Reader} to be wiretapped when building a Reader. This must be set to a non-{@code null}
* value in order to call {@link #buildReader()}.
*
* @param reader the Reader to wiretap
* @return {@code this}
*/
public IoBuilder filter(final Reader reader) {
this.reader = reader;
return this;
}
/**
* Configures a {@link Writer} to be written to in addition to the underlying Logger. If no Writer is specified,
* then the built Writer or PrintWriter will only write to the underlying Logger.
*
* @param writer the Writer to write to in addition to the Logger
* @return {@code this}
*/
public IoBuilder filter(final Writer writer) {
this.writer = writer;
return this;
}
/**
* Configures an {@link InputStream} to be wiretapped when building an InputStream. This must be set to a
* non-{@code null} value in order to call {@link #buildInputStream()}.
*
* @param inputStream the InputStream to wiretap
* @return {@code this}
*/
public IoBuilder filter(final InputStream inputStream) {
this.inputStream = inputStream;
return this;
}
/**
* Configures an {@link OutputStream} to be written to in addition to the underlying Logger. If no OutputStream is
* specified, then the built OutputStream or PrintStream will only write to the underlying Logger.
*
* @param outputStream the OutputStream to write to in addition to the Logger
* @return {@code this}
*/
public IoBuilder filter(final OutputStream outputStream) {
this.outputStream = outputStream;
return this;
}
// TODO: could this builder use generics to infer the desired IO class?
/**
* Builds a new {@link Reader} that is wiretapped by its underlying Logger. If buffering is enabled, then a
* {@link java.io.BufferedReader} will be returned.
*
* @return a new Reader wiretapped by a Logger
* @throws IllegalStateException if no Reader was configured for this builder
*/
public Reader buildReader() {
final Reader in = Objects.requireNonNull(this.reader, "reader");
if (this.buffered) {
if (this.bufferSize > 0) {
return new LoggerBufferedReader(in, this.bufferSize, this.logger, this.fqcn, this.level, this.marker);
}
return new LoggerBufferedReader(in, this.logger, this.fqcn, this.level, this.marker);
}
return new LoggerReader(in, this.logger, this.fqcn, this.level, this.marker);
}
/**
* Builds a new {@link Writer} that is backed by a Logger and optionally writes to another Writer as well. If no
* Writer is configured for this builder, then the returned Writer will only write to its underlying Logger.
*
* @return a new Writer or {@link java.io.FilterWriter} backed by a Logger
*/
public Writer buildWriter() {
if (this.writer == null) {
return new LoggerWriter(this.logger, this.fqcn, this.level, this.marker);
}
return new LoggerFilterWriter(this.writer, this.logger, this.fqcn, this.level, this.marker);
}
/**
* Builds a new {@link PrintWriter} that is backed by a Logger and optionally writes to another Writer as well. If
* no Writer is configured for this builder, then the returned PrintWriter will only write to its underlying
* Logger.
*
* @return a new PrintWriter that optionally writes to another Writer in addition to its underlying Logger
*/
public PrintWriter buildPrintWriter() {
if (this.writer == null) {
return new LoggerPrintWriter(this.logger, this.autoFlush, this.fqcn, this.level, this.marker);
}
return new LoggerPrintWriter(this.writer, this.autoFlush, this.logger, this.fqcn, this.level, this.marker);
}
/**
* Builds a new {@link InputStream} that is wiretapped by its underlying Logger. If buffering is enabled, then a
* {@link java.io.BufferedInputStream} will be returned.
*
* @return a new InputStream wiretapped by a Logger
* @throws IllegalStateException if no InputStream was configured for this builder
*/
public InputStream buildInputStream() {
final InputStream in = Objects.requireNonNull(this.inputStream, "inputStream");
if (this.buffered) {
if (this.bufferSize > 0) {
return new LoggerBufferedInputStream(in, this.charset, this.bufferSize, this.logger, this.fqcn,
this.level, this.marker);
}
return new LoggerBufferedInputStream(in, this.charset, this.logger, this.fqcn, this.level, this.marker);
}
return new LoggerInputStream(in, this.charset, this.logger, this.fqcn, this.level, this.marker);
}
/**
* Builds a new {@link OutputStream} that is backed by a Logger and optionally writes to another OutputStream as
* well. If no OutputStream is configured for this builder, then the returned OutputStream will only write to its
* underlying Logger.
*
* @return a new OutputStream that optionally writes to another OutputStream in addition to its underlying Logger
*/
public OutputStream buildOutputStream() {
if (this.outputStream == null) {
return new LoggerOutputStream(this.logger, this.level, this.marker, this.charset, this.fqcn);
}
return new LoggerFilterOutputStream(this.outputStream, this.charset, this.logger, this.fqcn, this.level,
this.marker);
}
/**
* Builds a new {@link PrintStream} that is backed by a Logger and optionally writes to another OutputStream as
* well. If no OutputStream is configured for this builder, then the returned PrintStream will only write to its
* underlying Logger.
*
* @return a new PrintStream that optionally writes to another OutputStream in addition to its underlying Logger
* @throws LoggingException if the configured character set is unsupported by {@link PrintStream}
*/
public PrintStream buildPrintStream() {
try {
if (this.outputStream == null) {
return new LoggerPrintStream(this.logger, this.autoFlush, this.charset, this.fqcn, this.level,
this.marker);
}
return new LoggerPrintStream(this.outputStream, this.autoFlush, this.charset, this.logger, this.fqcn,
this.level, this.marker);
} catch (final UnsupportedEncodingException e) {
// this exception shouldn't really happen since we use Charset and not String
throw new LoggingException(e);
}
}
}