+ * If @code source} and {@code destination} are not on the default file system, we need to make sure + * that {@link java.nio.file.Paths#get(URI)} is called instead of the + * {@link java.nio.file.Paths#get(String, String...)} variant. + *
+ */ + protected static void verifyCompressionUsingStringParams(String algorithm, Map+ * When applicable, it should correspond to the name used by Apache Commons Compress. + *
+ */ + protected abstract String getAlgorithmName(); + + /** + * Wraps an output stream into a compressing output stream. + * + * @param stream The stream to wrap. + * @return A compressing output stream. + */ + protected abstract OutputStream wrapOutputStream(final OutputStream stream) throws IOException; + + @Override + public boolean execute() throws IOException { + if (Files.exists(source)) { + LOGGER.debug("Starting {} compression from {} to {}.", getAlgorithmName(), source, destination); + try (final OutputStream output = wrapOutputStream(Files.newOutputStream(destination))) { + Files.copy(source, output); + } + LOGGER.debug("Finished {} compression from {} to {}.", getAlgorithmName(), source, destination); + try { + Files.delete(source); + LOGGER.debug("File {} deleted successfully.", source); + } catch (final IOException ioe) { + LOGGER.warn("Unable to delete {}.", source, ioe); + } + return true; + } + return false; + } + + /** + * Capture exception. + * + * @param ex exception. + */ + @Override + protected void reportException(final Exception ex) { + LOGGER.warn("Exception during compression of '{}'.", source, ex); + } + + @Override + public String toString() { + return getClass().getSimpleName() + '[' + source + " to " + destination + ']'; + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java deleted file mode 100644 index 526e0b9e56d..00000000000 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CommonsCompressAction.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.core.appender.rolling.action; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Objects; -import org.apache.commons.compress.compressors.CompressorException; -import org.apache.commons.compress.compressors.CompressorStreamFactory; -import org.apache.commons.compress.utils.IOUtils; - -/** - * Compresses a file using bzip2 compression. - */ -public final class CommonsCompressAction extends AbstractAction { - - private static final int BUF_SIZE = 8192; - - /** - * Compressor name. One of "gz", "bzip2", "xz", "pack200" or "deflate". - */ - private final String name; - - /** - * Source file. - */ - private final File source; - - /** - * Destination file. - */ - private final File destination; - - /** - * If true, attempt to delete file on completion. - */ - private final boolean deleteSource; - - /** - * Creates new instance of Bzip2CompressAction. - * - * @param name the compressor name. One of "gz", "bzip2", "xz", "pack200", or "deflate". - * @param source file to compress, may not be null. - * @param destination compressed file, may not be null. - * @param deleteSource if true, attempt to delete file on completion. Failure to delete does not cause an exception - * to be thrown or affect return value. - */ - public CommonsCompressAction( - final String name, final File source, final File destination, final boolean deleteSource) { - Objects.requireNonNull(source, "source"); - Objects.requireNonNull(destination, "destination"); - this.name = name; - this.source = source; - this.destination = destination; - this.deleteSource = deleteSource; - } - - /** - * Compresses. - * - * @return true if successfully compressed. - * @throws IOException on IO exception. - */ - @Override - public boolean execute() throws IOException { - return execute(name, source, destination, deleteSource); - } - - /** - * Compresses a file. - * - * @param name the compressor name, i.e. "gz", "bzip2", "xz", "pack200", or "deflate". - * @param source file to compress, may not be null. - * @param destination compressed file, may not be null. - * @param deleteSource if true, attempt to delete file on completion. Failure to delete does not cause an exception - * to be thrown or affect return value. - * - * @return true if source file compressed. - * @throws IOException on IO exception. - */ - public static boolean execute( - final String name, final File source, final File destination, final boolean deleteSource) - throws IOException { - if (!source.exists()) { - return false; - } - LOGGER.debug("Starting {} compression of {}", name, source.getPath()); - try (final FileInputStream input = new FileInputStream(source); - final FileOutputStream fileOutput = new FileOutputStream(destination); - final BufferedOutputStream output = new BufferedOutputStream( - new CompressorStreamFactory().createCompressorOutputStream(name, fileOutput))) { - IOUtils.copy(input, output, BUF_SIZE); - LOGGER.debug("Finished {} compression of {}", name, source.getPath()); - } catch (final CompressorException e) { - throw new IOException(e); - } - - if (deleteSource) { - try { - if (Files.deleteIfExists(source.toPath())) { - LOGGER.debug("Deleted {}", source.toString()); - } else { - LOGGER.warn( - "Unable to delete {} after {} compression. File did not exist", source.toString(), name); - } - } catch (final Exception ex) { - LOGGER.warn("Unable to delete {} after {} compression, {}", source.toString(), name, ex.getMessage()); - } - } - - return true; - } - - /** - * Reports exception. - * - * @param ex exception. - */ - @Override - protected void reportException(final Exception ex) { - LOGGER.warn("Exception during " + name + " compression of '" + source.toString() + "'.", ex); - } - - @Override - public String toString() { - return CommonsCompressAction.class.getSimpleName() + '[' + source + " to " + destination + ", deleteSource=" - + deleteSource + ']'; - } - - public String getName() { - return name; - } - - public File getSource() { - return source; - } - - public File getDestination() { - return destination; - } - - public boolean isDeleteSource() { - return deleteSource; - } -} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CompressActionFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CompressActionFactory.java new file mode 100644 index 00000000000..3cc48d41e9e --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CompressActionFactory.java @@ -0,0 +1,76 @@ +/* + * 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.core.appender.rolling.action; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import org.apache.logging.log4j.core.util.FileUtils; +import org.apache.logging.log4j.core.util.NetUtils; +import org.jspecify.annotations.NullMarked; + +/** + * A factory to produce actions that compress log files. + */ +@NullMarked +public interface CompressActionFactory { + + /** + * Returns an action that compresses a file. + * + * @param source The location of the file to compress. + * @param destination The location of the compressed file. + * @param options Compression options to pass to the compression library. + * @return An action the compresses the source file. + */ + Action createCompressAction(Path source, Path destination, Map+ * When applicable, it should correspond to the name used by Apache Commons Compress. + *
+ */ + String getAlgorithmName(); + + /** + * Returns the natural file extension for this compression algorithm. + *+ * The extension must start with a dot. + *
+ * @return A file extension. + */ + default String getExtension() { + return "." + getAlgorithmName(); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CompressActionFactoryProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CompressActionFactoryProvider.java new file mode 100644 index 00000000000..d8118b586c0 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/CompressActionFactoryProvider.java @@ -0,0 +1,62 @@ +/* + * 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.core.appender.rolling.action; + +import org.apache.logging.log4j.core.appender.rolling.action.internal.CompositeCompressActionFactoryProvider; +import org.apache.logging.log4j.core.config.Configuration; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Interface for plugins that provide additional compression algorithms. + * + * @since 3.0.0 + */ +@NullMarked +public interface CompressActionFactoryProvider { + + String NAMESPACE = "compress"; + + /** + * Creates the appropriate {@link CompressActionFactory} for the given compression algorithm. + *+ * When applicable the algorithm should correspond to the name used by Apache Commons Compress. + *
+ * @param algorithm The compression algorithm. + * @return A {@link CompressActionFactory} or {@code null} if the extension is not supported. + */ + @Nullable + CompressActionFactory createFactoryForAlgorithm(String algorithm); + + /** + * Creates the appropriate {@link CompressActionFactory} for the given file name. + * + * @param fileName The file name. + * @return A {@link CompressActionFactory} or {@code null} if the extension is not supported. + */ + default @Nullable CompressActionFactory createFactoryForFileName(String fileName) { + final int idx = fileName.lastIndexOf('.'); + if (idx != -1) { + return createFactoryForAlgorithm(fileName.substring(idx + 1)); + } + return null; + } + + static CompressActionFactoryProvider newInstance(final @Nullable Configuration configuration) { + return new CompositeCompressActionFactoryProvider(configuration); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java deleted file mode 100644 index ccf1898f4f7..00000000000 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/GzCompressAction.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.core.appender.rolling.action; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Objects; -import java.util.zip.Deflater; -import java.util.zip.GZIPOutputStream; - -/** - * Compresses a file using GZ compression. - */ -public final class GzCompressAction extends AbstractAction { - - private static final int BUF_SIZE = 8192; - - /** - * Source file. - */ - private final File source; - - /** - * Destination file. - */ - private final File destination; - - /** - * If true, attempt to delete file on completion. - */ - private final boolean deleteSource; - - /** - * GZIP compression level to use. - * - * @see Deflater#setLevel(int) - */ - private final int compressionLevel; - - /** - * Create new instance of GzCompressAction. - * - * @param source file to compress, may not be null. - * @param destination compressed file, may not be null. - * @param deleteSource if true, attempt to delete file on completion. Failure to delete - * does not cause an exception to be thrown or affect return value. - * @param compressionLevel - * Gzip deflater compression level. - */ - public GzCompressAction( - final File source, final File destination, final boolean deleteSource, final int compressionLevel) { - Objects.requireNonNull(source, "source"); - Objects.requireNonNull(destination, "destination"); - - this.source = source; - this.destination = destination; - this.deleteSource = deleteSource; - this.compressionLevel = compressionLevel; - } - - /** - * Compress. - * - * @return true if successfully compressed. - * @throws IOException on IO exception. - */ - @Override - public boolean execute() throws IOException { - return execute(source, destination, deleteSource, compressionLevel); - } - - /** - * Compress a file. - * - * @param source file to compress, may not be null. - * @param destination compressed file, may not be null. - * @param deleteSource if true, attempt to delete file on completion. Failure to delete - * does not cause an exception to be thrown or affect return value. - * @param compressionLevel - * Gzip deflater compression level. - * @return true if source file compressed. - * @throws IOException on IO exception. - */ - public static boolean execute( - final File source, final File destination, final boolean deleteSource, final int compressionLevel) - throws IOException { - if (source.exists()) { - try (final FileInputStream fis = new FileInputStream(source); - final OutputStream fos = new FileOutputStream(destination); - final OutputStream gzipOut = - new ConfigurableLevelGZIPOutputStream(fos, BUF_SIZE, compressionLevel); - // Reduce native invocations by buffering data into GZIPOutputStream - final OutputStream os = new BufferedOutputStream(gzipOut, BUF_SIZE)) { - final byte[] inbuf = new byte[BUF_SIZE]; - int n; - - while ((n = fis.read(inbuf)) != -1) { - os.write(inbuf, 0, n); - } - } - - if (deleteSource && !source.delete()) { - LOGGER.warn("Unable to delete {}.", source); - } - - return true; - } - - return false; - } - - private static final class ConfigurableLevelGZIPOutputStream extends GZIPOutputStream { - - ConfigurableLevelGZIPOutputStream(final OutputStream out, final int bufSize, final int level) - throws IOException { - super(out, bufSize); - def.setLevel(level); - } - } - - /** - * Capture exception. - * - * @param ex exception. - */ - @Override - protected void reportException(final Exception ex) { - LOGGER.warn("Exception during compression of '" + source.toString() + "'.", ex); - } - - @Override - public String toString() { - return GzCompressAction.class.getSimpleName() + '[' + source + " to " + destination + ", deleteSource=" - + deleteSource + ']'; - } - - public File getSource() { - return source; - } - - public File getDestination() { - return destination; - } - - public boolean isDeleteSource() { - return deleteSource; - } -} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java deleted file mode 100644 index f29a7391ceb..00000000000 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/ZipCompressAction.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.core.appender.rolling.action; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Objects; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** - * Compresses a file using Zip compression. - */ -public final class ZipCompressAction extends AbstractAction { - - private static final int BUF_SIZE = 8192; - - /** - * Source file. - */ - private final File source; - - /** - * Destination file. - */ - private final File destination; - - /** - * If true, attempts to delete file on completion. - */ - private final boolean deleteSource; - - /** - * Compression level. - */ - private final int level; - - /** - * Creates new instance of GzCompressAction. - * - * @param source file to compress, may not be null. - * @param destination compressed file, may not be null. - * @param deleteSource if true, attempt to delete file on completion. Failure to delete does not cause an exception - * to be thrown or affect return value. - * @param level TODO - */ - public ZipCompressAction(final File source, final File destination, final boolean deleteSource, final int level) { - Objects.requireNonNull(source, "source"); - Objects.requireNonNull(destination, "destination"); - - this.source = source; - this.destination = destination; - this.deleteSource = deleteSource; - this.level = level; - } - - /** - * Compresses. - * - * @return true if successfully compressed. - * @throws IOException on IO exception. - */ - @Override - public boolean execute() throws IOException { - return execute(source, destination, deleteSource, level); - } - - /** - * Compresses a file. - * - * @param source file to compress, may not be null. - * @param destination compressed file, may not be null. - * @param deleteSource if true, attempt to delete file on completion. Failure to delete does not cause an exception - * to be thrown or affect return value. - * @param level the compression level - * @return true if source file compressed. - * @throws IOException on IO exception. - */ - public static boolean execute( - final File source, final File destination, final boolean deleteSource, final int level) throws IOException { - if (source.exists()) { - try (final FileInputStream fis = new FileInputStream(source); - final ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(destination))) { - zos.setLevel(level); - - final ZipEntry zipEntry = new ZipEntry(source.getName()); - zos.putNextEntry(zipEntry); - - final byte[] inbuf = new byte[BUF_SIZE]; - int n; - - while ((n = fis.read(inbuf)) != -1) { - zos.write(inbuf, 0, n); - } - } - - if (deleteSource && !source.delete()) { - LOGGER.warn("Unable to delete " + source.toString() + '.'); - } - - return true; - } - - return false; - } - - /** - * Captures exception. - * - * @param ex exception. - */ - @Override - protected void reportException(final Exception ex) { - LOGGER.warn("Exception during compression of '" + source.toString() + "'.", ex); - } - - @Override - public String toString() { - return ZipCompressAction.class.getSimpleName() + '[' + source + " to " + destination + ", level=" + level - + ", deleteSource=" + deleteSource + ']'; - } - - public File getSource() { - return source; - } - - public File getDestination() { - return destination; - } - - public boolean isDeleteSource() { - return deleteSource; - } - - public int getLevel() { - return level; - } -} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/internal/CompositeCompressActionFactoryProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/internal/CompositeCompressActionFactoryProvider.java new file mode 100644 index 00000000000..ec4e7589061 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/action/internal/CompositeCompressActionFactoryProvider.java @@ -0,0 +1,50 @@ +/* + * 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.core.appender.rolling.action.internal; + +import java.util.List; +import java.util.Objects; +import org.apache.logging.log4j.core.appender.rolling.action.CompressActionFactory; +import org.apache.logging.log4j.core.appender.rolling.action.CompressActionFactoryProvider; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.plugins.Namespace; +import org.apache.logging.log4j.plugins.di.Key; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class CompositeCompressActionFactoryProvider implements CompressActionFactoryProvider { + + private final List+ * + *
+ * @param uri The {@link URI} to transform, never {@code null} + * @return A {@link Path} or {@code null} if the URI does not use a valid {@link java.nio.file.FileSystem} scheme. + */ + public static Path pathFromUri(final URI uri) { + Objects.requireNonNull(uri, "uri"); + if (uri.isAbsolute()) { + return Paths.get(uri); + } else { + return Paths.get(uri.getPath()); + } + } + public static boolean isFile(final URL url) { return url != null && (url.getProtocol().equals(PROTOCOL_FILE) || url.getProtocol().equals(JBOSS_FILE)); diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml index 8b020703f63..20022e32cb1 100644 --- a/log4j-parent/pom.xml +++ b/log4j-parent/pom.xml @@ -97,7 +97,6 @@