From 069a1256c6ebdc1142e525a44733bc32fd834061 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 04 Aug 2026 11:37:29 +0000
Subject: [PATCH] Fix CodeQL note-severity alerts: ignored error status of file and stream calls (#814)
---
opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java | 59 ++++
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java | 6
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java | 4
opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java | 21 -
opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java | 10
opendj-server-legacy/src/messages/org/opends/messages/core.properties | 4
opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java | 20 -
opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java | 23 -
opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java | 9
opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java | 31 -
opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java | 76 ++++-
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java | 8
opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java | 17
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java | 19 +
opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java | 72 +++++
opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java | 20 +
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java | 11
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java | 9
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java | 6
opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java | 20
opendj-server-legacy/src/messages/org/opends/messages/logger.properties | 8
opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java | 8
opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java | 45 ++
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties | 5
opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java | 316 ++++++++++++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java | 2
26 files changed, 662 insertions(+), 167 deletions(-)
diff --git a/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java b/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
index a7afa17..a6571e4 100644
--- a/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
+++ b/opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2009 Sun Microsystems, Inc.
* Portions Copyright 2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package com.forgerock.opendj.util;
@@ -130,7 +131,12 @@
n = readLimit - bytesRead;
}
- bytesRead += n;
- return parentStream.skip(n);
+ // The parent stream is allowed to skip fewer bytes than requested, so only account for the
+ // bytes which were actually skipped.
+ final long skipped = parentStream.skip(n);
+ if (skipped > 0) {
+ bytesRead += (int) skipped;
+ }
+ return skipped;
}
}
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java b/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
index c6a1522..15ced3a 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.forgerock.opendj.io;
@@ -145,10 +146,14 @@
// Ignore all unused trailing components.
final SizeLimitInputStream subSq = (SizeLimitInputStream) in;
- if (subSq.getSizeLimit() - subSq.getBytesRead() > 0) {
- logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE",
- subSq.getSizeLimit() - subSq.getBytesRead());
- subSq.skip(subSq.getSizeLimit() - subSq.getBytesRead());
+ final long unusedBytes = subSq.getSizeLimit() - subSq.getBytesRead();
+ if (unusedBytes > 0) {
+ logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE", unusedBytes);
+ if (skipFully(subSq, unusedBytes) != unusedBytes) {
+ // The remaining components could not be skipped, so the reader would be left
+ // positioned in the middle of the sequence.
+ throw DecodeException.fatalError(ERR_ASN1_SKIP_TRUNCATED_VALUE.get(unusedBytes));
+ }
}
logger.trace("READ ASN.1 END SEQUENCE");
@@ -385,7 +390,7 @@
// Read the header if haven't done so already
peekLength();
- final long bytesSkipped = in.skip(peekLength);
+ final long bytesSkipped = skipFully(in, peekLength);
if (bytesSkipped != peekLength) {
final LocalizableMessage message = ERR_ASN1_SKIP_TRUNCATED_VALUE.get(peekLength);
throw DecodeException.fatalError(message);
@@ -395,6 +400,36 @@
}
/**
+ * Skips the requested number of bytes, retrying as needed since {@link InputStream#skip} is
+ * allowed to skip fewer bytes than requested even when the end of the stream has not been
+ * reached.
+ *
+ * @param stream
+ * The stream to skip bytes from.
+ * @param length
+ * The number of bytes to skip.
+ * @return The number of bytes actually skipped, which is smaller than {@code length} only if the
+ * end of the stream was reached.
+ * @throws IOException
+ * If an error occurs while skipping bytes.
+ */
+ private static long skipFully(final InputStream stream, final long length) throws IOException {
+ long remaining = length;
+ while (remaining > 0) {
+ final long skipped = stream.skip(remaining);
+ if (skipped > 0) {
+ remaining -= skipped;
+ } else if (stream.read() < 0) {
+ // End of stream reached: no more bytes can be skipped.
+ break;
+ } else {
+ remaining--;
+ }
+ }
+ return length - remaining;
+ }
+
+ /**
* Internal helper method reading the additional ASN.1 length bytes and
* transition to the next state if successful.
*
diff --git a/opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java b/opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java
new file mode 100644
index 0000000..39b629c
--- /dev/null
+++ b/opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package com.forgerock.opendj.util;
+
+import static org.fest.assertions.Assertions.*;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.testng.annotations.Test;
+
+/** Test {@code SizeLimitInputStream}. */
+@SuppressWarnings("javadoc")
+public final class SizeLimitInputStreamTestCase extends UtilTestCase {
+
+ @Test
+ public void testSkipAccountsForTheBytesActuallySkipped() throws Exception {
+ // A BufferedInputStream only skips what its buffer holds, without reaching the end of the
+ // parent stream, which is exactly the case getBytesRead() used to get wrong.
+ final InputStream parent = new BufferedInputStream(new ByteArrayInputStream(new byte[32]), 4);
+ // Fill the buffer with 4 bytes and consume one of them, leaving 3 skippable bytes in it.
+ parent.read();
+
+ final SizeLimitInputStream stream = new SizeLimitInputStream(parent, 32);
+ assertThat(stream.skip(16)).isEqualTo(3);
+ assertThat(stream.getBytesRead()).isEqualTo(3);
+ }
+
+ @Test
+ public void testSkipIsCappedToTheSizeLimit() throws Exception {
+ final SizeLimitInputStream stream =
+ new SizeLimitInputStream(new ByteArrayInputStream(new byte[32]), 8);
+
+ assertThat(stream.skip(20)).isEqualTo(8);
+ assertThat(stream.getBytesRead()).isEqualTo(8);
+ assertThat(stream.read()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testSkipStopsAtTheEndOfTheParentStream() throws Exception {
+ final SizeLimitInputStream stream =
+ new SizeLimitInputStream(new ByteArrayInputStream(new byte[2]), 8);
+
+ assertThat(stream.skip(8)).isEqualTo(2);
+ assertThat(stream.getBytesRead()).isEqualTo(2);
+ }
+
+ @Test
+ public void testReadAccountsForTheBytesActuallyRead() throws Exception {
+ final SizeLimitInputStream stream =
+ new SizeLimitInputStream(new ByteArrayInputStream(new byte[32]), 8);
+
+ assertThat(stream.read()).isEqualTo(0);
+ assertThat(stream.read(new byte[16])).isEqualTo(7);
+ assertThat(stream.getBytesRead()).isEqualTo(8);
+ assertThat(stream.read()).isEqualTo(-1);
+ }
+}
diff --git a/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java b/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
index b8c0687..3ae1bdd 100644
--- a/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
+++ b/opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
@@ -13,11 +13,17 @@
*
* Copyright 2010 Sun Microsystems, Inc.
* Portions copyright 2013 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.forgerock.opendj.io;
+import static org.testng.Assert.*;
+
+import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
+import org.testng.annotations.Test;
+
/**
* Test class for ASN1InputStreamReader.
*/
@@ -27,4 +33,57 @@
final ByteArrayInputStream inStream = new ByteArrayInputStream(b);
return new ASN1InputStreamReader(inStream, maxElementSize);
}
+
+ /**
+ * Returns a reader whose underlying stream skips fewer bytes than requested without having
+ * reached its end, which is what {@code BufferedInputStream} does once its buffer is partially
+ * consumed.
+ */
+ private ASN1Reader getBufferedReader(final byte[] b) {
+ return new ASN1InputStreamReader(new BufferedInputStream(new ByteArrayInputStream(b), 4), 0);
+ }
+
+ /**
+ * Tests that the trailing components of a sequence are fully skipped even when the underlying
+ * stream skips fewer bytes than requested.
+ *
+ * @throws Exception
+ * If an unexpected problem occurs.
+ */
+ @Test
+ public void testDecodeSequenceIncompleteReadOverBufferedStream() throws Exception {
+ // A sequence holding ten booleans, of which only the first one is read, followed by an
+ // integer which must still be decoded correctly once the sequence has been skipped.
+ final byte[] b =
+ new byte[] { 0x30, 0x0C, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+ 0x01, 0x01, 0x02, 0x01, 0x7F };
+ final ASN1Reader reader = getBufferedReader(b);
+
+ reader.readStartSequence();
+ assertFalse(reader.readBoolean());
+ reader.readEndSequence();
+
+ assertEquals(reader.readInteger(), 127);
+ }
+
+ /**
+ * Tests that {@code skipElement} does not report a truncated value when the underlying stream
+ * skips fewer bytes than requested.
+ *
+ * @throws Exception
+ * If an unexpected problem occurs.
+ */
+ @Test
+ public void testSkipElementOverBufferedStream() throws Exception {
+ final byte[] b =
+ new byte[] { 0x30, 0x0C, 0x02, 0x01, 0x05, 0x04, 0x04, 0x61, 0x62, 0x63, 0x64, 0x02,
+ 0x01, 0x7F };
+ final ASN1Reader reader = getBufferedReader(b);
+
+ reader.readStartSequence();
+ assertEquals(reader.readInteger(), 5);
+ reader.skipElement();
+ assertEquals(reader.readInteger(), 127);
+ reader.readEndSequence();
+ }
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
index e7d6212..f15ec37 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.quicksetup;
@@ -22,7 +23,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.nio.file.Files;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.quicksetup.util.Utils;
import org.opends.server.util.ServerConstants;
@@ -33,6 +37,8 @@
*/
public class LicenseFile
{
+ private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
private static final String INSTALL_ROOT_SYSTEM_PROPERTY = "INSTALL_ROOT";
/** The license file name in Legal directory. */
private static final String LICENSE_FILE_NAME = "Forgerock_License.txt";
@@ -61,9 +67,9 @@
String instanceLegalDirName = Utils.getInstancePathFromInstallPath(getInstallDirectory())
+ File.separator + LEGAL_FOLDER_NAME;
File instanceLegalDir = new File(instanceLegalDirName);
- if (!instanceLegalDir.exists())
+ if (!instanceLegalDir.isDirectory() && !instanceLegalDir.mkdirs())
{
- instanceLegalDir.mkdir();
+ logger.warn(LocalizableMessage.raw("Unable to create the legal directory %s", instanceLegalDirName));
}
return instanceLegalDirName;
}
@@ -164,17 +170,19 @@
String instanceLegalDirName = instanceDirname + File.separator + LEGAL_FOLDER_NAME;
File instanceLegalDir = new File(instanceLegalDirName);
+ File approvalFile = new File(instanceLegalDir, ACCEPTED_LICENSE_FILE_NAME);
try
{
- if (!instanceLegalDir.exists())
+ Files.createDirectories(instanceLegalDir.toPath());
+ if (!approvalFile.createNewFile() && !approvalFile.isFile())
{
- instanceLegalDir.mkdir();
+ logger.warn(LocalizableMessage.raw("Unable to create the license approval file %s", approvalFile));
}
- new File(instanceLegalDir, ACCEPTED_LICENSE_FILE_NAME).createNewFile();
}
catch (IOException e)
{
- // do nothing
+ logger.warn(LocalizableMessage.raw(
+ "Unable to create the license approval file %s: %s", approvalFile, e.getLocalizedMessage()));
}
}
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
index 49c38ae..919ff3d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
@@ -977,17 +977,6 @@
writer.write(getJavaArgPropertyForScript(scriptName) + "=" + argument);
}
}
-
- String libDir = Utils.getPath(
- Utils.getInstancePathFromInstallPath(installPath), LIBRARIES_PATH_RELATIVE);
- // Create directory if it doesn't exist yet
- File fLib = new File(libDir);
- if (!fLib.exists())
- {
- fLib.mkdir();
- }
-// final String destinationFile = Utils.getPath(libDir, isWindows() ? SET_JAVA_PROPERTIES_FILE_WINDOWS
-// : SET_JAVA_PROPERTIES_FILE_UNIX);
}
/**
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java b/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
index e15de6a..33e2c12 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
@@ -119,7 +119,7 @@
generationDir = Files.createTempDirectory(CONFIG_GUIDE_DIR).toString();
} else {
// Create new dir if necessary
- new File(generationDir).mkdir();
+ Files.createDirectories(new File(generationDir).toPath());
}
} catch (Exception e) {
e.printStackTrace();
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
index bbcded9..0249d60 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
@@ -280,25 +280,13 @@
throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), m);
}
- if (tempFile.exists())
- {
- // Rename the existing "live" file out of the way and move the new file
- // into place.
- try
- {
- oldFile.delete();
- }
- catch (Exception e)
- {
- logger.traceException(e);
- }
- }
-
+ // Rename the existing "live" file out of the way and move the new file into place.
+ // renameFile() deletes an existing ".old" file and reports a failure to do so.
try
{
if (ldifFile.exists())
{
- ldifFile.renameTo(oldFile);
+ renameFile(ldifFile, oldFile);
}
}
catch (Exception e)
@@ -308,7 +296,7 @@
try
{
- tempFile.renameTo(ldifFile);
+ renameFile(tempFile, ldifFile);
}
catch (Exception e)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
index 97e006d..8f2216d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
@@ -1249,28 +1249,15 @@
writer.close();
- // See if there is a ".save" file. If so, then delete it.
- File saveFile = getFileForPath(backingFilePath + ".save");
- try
- {
- if (saveFile.exists())
- {
- saveFile.delete();
- }
- }
- catch (Exception e)
- {
- logger.traceException(e);
- }
-
-
// If there is an existing backing file, then rename it to ".save".
+ // renameFile() deletes an existing ".save" file and reports a failure to do so.
+ File saveFile = getFileForPath(backingFilePath + ".save");
File backingFile = getFileForPath(backingFilePath);
try
{
if (backingFile.exists())
{
- backingFile.renameTo(saveFile);
+ renameFile(backingFile, saveFile);
}
}
catch (Exception e)
@@ -1289,7 +1276,7 @@
File tmpFile = getFileForPath(tmpFilePath);
try
{
- tmpFile.renameTo(backingFile);
+ renameFile(tmpFile, backingFile);
}
catch (Exception e)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
index 892643d..011b250 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
@@ -817,25 +817,14 @@
}
// If a ".startok" file already exists, then move it to an ".old" file.
+ // renameFile() deletes an existing ".old" file and reports a failure to do so.
File oldFile = new File(oldFilePath);
- try
- {
- if (oldFile.exists())
- {
- oldFile.delete();
- }
- }
- catch (Exception e)
- {
- logger.traceException(e);
- }
-
File startOKFile = new File(startOKFilePath);
try
{
if (startOKFile.exists())
{
- startOKFile.renameTo(oldFile);
+ renameFile(startOKFile, oldFile);
}
}
catch (Exception e)
@@ -846,7 +835,7 @@
// Rename the temp file to the ".startok" file.
try
{
- tempFile.renameTo(startOKFile);
+ renameFile(tempFile, startOKFile);
}
catch (Exception e)
{
@@ -1609,20 +1598,12 @@
// Move the current config file out of the way and replace it with the updated version.
File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges");
- if (oldSource.exists())
- {
- oldSource.delete();
- }
- sourceFile.renameTo(oldSource);
- new File(tempFilePath).renameTo(sourceFile);
+ renameFile(sourceFile, oldSource);
+ renameFile(new File(tempFilePath), sourceFile);
// Move the changes file out of the way so it doesn't get applied again.
File newChanges = new File(changesFile.getAbsolutePath() + ".applied");
- if (newChanges.exists())
- {
- newChanges.delete();
- }
- changesFile.renameTo(newChanges);
+ renameFile(changesFile, newChanges);
}
private void applyConfigChangesIfNeeded(File configFileToUse) throws InitializationException
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java b/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
index 70e131d..04f975f 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
@@ -13,6 +13,7 @@
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.core;
@@ -23,6 +24,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@@ -228,14 +230,21 @@
if (liveFile.exists())
{
+ // Keeping a copy of the previous version is best effort only: save() is called from the
+ // entry encoding path, so failing here would turn a backup problem into a failed update.
final File saveFile = new File(liveFile.getAbsolutePath() + ".save");
- if (saveFile.exists())
+ try
{
- saveFile.delete();
+ renameFile(liveFile, saveFile);
}
- liveFile.renameTo(saveFile);
+ catch (final IOException e)
+ {
+ logger.traceException(e);
+ logger.warn(WARN_COMPRESSEDSCHEMA_CANNOT_SAVE_PREVIOUS_DATA, liveFile, saveFile,
+ stackTraceToSingleLineString(e));
+ }
}
- tempFile.renameTo(liveFile);
+ renameFile(tempFile, liveFile);
}
catch (final Exception e)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java b/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
index 72fdca0..644e0b4 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2013-2015 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.core;
@@ -93,9 +94,9 @@
File f = getFileForPath(lockFile);
try
{
- if (! f.exists())
+ if (! f.createNewFile())
{
- f.createNewFile();
+ logger.trace("Lock file %s already exists", lockFile);
}
}
catch (Exception e)
@@ -207,9 +208,9 @@
File f = getFileForPath(lockFile);
try
{
- if (! f.exists())
+ if (! f.createNewFile())
{
- f.createNewFile();
+ logger.trace("Lock file %s already exists", lockFile);
}
}
catch (Exception e)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java b/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
index 30372b1..b21b6b3 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
@@ -84,6 +84,16 @@
private long totalFilesRotated;
private long totalFilesCleaned;
+ /**
+ * Set when the current file could not be renamed by {@link #rotate()} and cleared as soon as a
+ * rotation succeeds again. While it is set, size based rotations are not triggered from
+ * {@link #writeRecord(String)} and the failure is not logged again: the rotation is only retried
+ * from the {@code RotaterThread}, once per interval.
+ */
+ private boolean rotationFailed;
+ /** Same latch as {@link #rotationFailed}, for the log files a retention policy cannot delete. */
+ private boolean cleanupFailed;
+
/** The underlying output stream. */
private MeteredStream outputStream;
/** The underlying buffered writer using the output stream. */
@@ -172,12 +182,7 @@
int bufferSize)
throws IOException, DirectoryException
{
- // Create new file if it doesn't exist
- if(!file.exists())
- {
- file.createNewFile();
- }
-
+ // The file is created by the output stream below if it does not exist yet.
FileOutputStream stream = new FileOutputStream(file, append);
outputStream = new MeteredStream(stream, file.length());
@@ -426,17 +431,28 @@
File[] files =
retentionPolicy.deleteFiles(writer.getNamingPolicy());
+ int cleanedCount = 0;
for(File file : files)
{
- file.delete();
- totalFilesCleaned++;
- logger.trace("%s cleaned up log file %s", retentionPolicy, file);
+ if (file.delete())
+ {
+ cleanedCount++;
+ totalFilesCleaned++;
+ cleanupFailed = false;
+ logger.trace("%s cleaned up log file %s", retentionPolicy, file);
+ }
+ else if (!cleanupFailed)
+ {
+ // Only report the first failure: the same files are returned on every interval.
+ cleanupFailed = true;
+ logger.warn(WARN_LOGGER_ERROR_DELETING_FILE, file, retentionPolicy);
+ }
}
- if(files.length > 0)
+ if(cleanedCount > 0)
{
lastCleanTime = TimeThread.getCalendar();
- lastCleanCount = files.length;
+ lastCleanCount = cleanedCount;
}
}
catch(DirectoryException de)
@@ -554,7 +570,9 @@
synchronized(this)
{
- if(sizeLimit > 0 && outputStream.written + size + 1 >= sizeLimit)
+ // Once a rotation has failed the file stays over the size limit, so rotating it again for
+ // every single record would only repeat the failure. Leave the retry to the RotaterThread.
+ if(sizeLimit > 0 && !rotationFailed && outputStream.written + size + 1 >= sizeLimit)
{
rotate();
}
@@ -590,9 +608,12 @@
}
/**
- * Tries to rotate the log files. If the new log file already exists, it
- * tries to rename the file. On failure, all subsequent log write requests
- * will throw exceptions.
+ * Tries to rotate the log files by renaming the current file to the name provided by the naming
+ * policy. When the rename fails, the current file is kept and appended to rather than truncated,
+ * the failure is reported once and the rotation is retried on the next interval.
+ * <p>
+ * Note that {@code File.renameTo} silently replaces the target on most platforms, so a rotation
+ * happening within the same second as a previous one overwrites the file it just rotated.
*/
private synchronized void rotate()
{
@@ -609,11 +630,17 @@
File currentFile = namingPolicy.getInitialName();
File newFile = namingPolicy.getNextName();
- currentFile.renameTo(newFile);
+ final boolean renamed = currentFile.renameTo(newFile);
+ // The latch must be set before the writer is re-opened: constructWriter() logs warnings of its
+ // own, and when this writer backs the error log they come straight back into writeRecord() on
+ // this thread, where they must not trigger another rotation attempt.
+ final boolean report = !renamed && !rotationFailed;
+ rotationFailed = !renamed;
try
{
- constructWriter(currentFile, filePermissions, encoding, append,
+ // If the file could not be rotated then keep appending to it rather than truncating it.
+ constructWriter(currentFile, filePermissions, encoding, append || !renamed,
bufferSize);
}
catch (Exception e)
@@ -622,9 +649,18 @@
errorHandler.handleOpenError(currentFile, e);
}
- logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
- totalFilesRotated++;
- lastRotationTime = TimeThread.getCalendar();
+ if (renamed)
+ {
+ logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
+ totalFilesRotated++;
+ lastRotationTime = TimeThread.getCalendar();
+ }
+ else if (report)
+ {
+ logger.error(ERR_LOGGER_ERROR_ROTATING_FILE, currentFile, newFile);
+ }
+ // lastRotationTime is left untouched on failure so that a time based policy keeps asking for a
+ // rotation on every interval instead of waiting for a whole new period.
}
@Override
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
index 0413aa9..f9198c2 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
@@ -755,9 +755,9 @@
try
{
- if(!logFile.exists())
+ if(!logFile.createNewFile())
{
- logFile.createNewFile();
+ logger.trace("Referential integrity update log file %s already exists", logFileName);
}
}
catch (IOException io)
@@ -865,8 +865,9 @@
}
}
}
- logFile.delete();
- logFile.createNewFile();
+ if (!logFile.delete() || !logFile.createNewFile()) {
+ logger.error(ERR_PLUGIN_REFERENT_CANNOT_REPLACE_LOGFILE, logFileName);
+ }
} catch (IOException io) {
logger.error(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE, io.getMessage());
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
index 285381d..98f5e21 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
@@ -21,6 +21,7 @@
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
+import java.nio.file.Files;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -144,10 +145,7 @@
final File dbDirectory = getFileForPath(dbDirName);
try
{
- if (!dbDirectory.exists())
- {
- dbDirectory.mkdir();
- }
+ Files.createDirectories(dbDirectory.toPath());
return dbDirectory;
}
catch (Exception e)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
index 16cb340..a385f58 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
@@ -199,9 +199,9 @@
{
try
{
- if (!logfile.exists())
+ if (!logfile.createNewFile())
{
- logfile.createNewFile();
+ logger.trace("Log file %s already exists", logfile.getPath());
}
}
catch (IOException e)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
index ee1282c..651da13 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
@@ -949,7 +949,11 @@
final File newRotationFile = getLastRotationTimePath(lastRotationTime);
try
{
- newRotationFile.createNewFile();
+ if (!newRotationFile.createNewFile() && !newRotationFile.isFile())
+ {
+ throw new ChangelogException(ERR_CHANGELOG_UNABLE_TO_CREATE_LAST_LOG_ROTATION_TIME_FILE.get(
+ newRotationFile.getPath(), lastRotationTime));
+ }
}
catch (IOException e)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java b/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
index 8f74982..fa4f9c1 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
@@ -291,7 +291,7 @@
matchingRuleUses, ldapSyntaxes);
File upgradeDirectory = getUpgradeDirectory();
- upgradeDirectory.mkdir();
+ Files.createDirectories(upgradeDirectory.toPath());
File concatFile = new File(upgradeDirectory, SCHEMA_CONCAT_FILE_NAME);
concatFilePath = concatFile.getAbsolutePath();
@@ -313,11 +313,7 @@
writeLines(writer, ATTR_LDAP_SYNTAXES, ldapSyntaxes);
}
- if (concatFile.exists())
- {
- concatFile.delete();
- }
- tempFile.renameTo(concatFile);
+ renameFile(tempFile, concatFile);
}
catch (Exception e)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
index 1cd2940..eab5eab 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.tools.upgrade;
@@ -21,6 +22,8 @@
import java.io.FileReader;
import java.io.IOException;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.util.StaticUtils;
/**
@@ -31,6 +34,8 @@
*/
class LicenseFile
{
+ private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
private static final String INSTALL_ROOT_SYSTEM_PROPERTY = "INSTALL_ROOT";
/**
@@ -80,9 +85,9 @@
final String instanceDirname = UpgradeUtils.getInstancePathFromInstallPath(getInstallRootPathFromSystem("."));
final String instanceLegalDirName = instanceDirname + File.separator + LEGAL_FOLDER_NAME;
final File instanceLegalDir = new File(instanceLegalDirName);
- if (!instanceLegalDir.exists())
+ if (!instanceLegalDir.isDirectory() && !instanceLegalDir.mkdirs())
{
- instanceLegalDir.mkdir();
+ logger.warn(LocalizableMessage.raw("Unable to create the legal directory %s", instanceLegalDirName));
}
return instanceLegalDirName;
}
@@ -197,12 +202,18 @@
{
if (getApproval())
{
+ final File approvalFile = new File(getInstanceLegalDirectory(), ACCEPTED_LICENSE_FILE_NAME);
try
{
- new File(getInstanceLegalDirectory(), ACCEPTED_LICENSE_FILE_NAME).createNewFile();
+ if (!approvalFile.createNewFile() && !approvalFile.isFile())
+ {
+ logger.warn(LocalizableMessage.raw("Unable to create the license approval file %s", approvalFile));
+ }
}
catch (IOException e)
- { // do nothing
+ {
+ logger.warn(LocalizableMessage.raw(
+ "Unable to create the license approval file %s: %s", approvalFile, e.getLocalizedMessage()));
}
}
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
index 2b3596e..c7f43fe 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
@@ -24,6 +24,7 @@
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
+import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -682,17 +683,14 @@
{
logger.debug(LocalizableMessage.raw("Parent file of %s doesn't exist", destination.getPath()));
- parentDirectory.mkdirs();
+ Files.createDirectories(parentDirectory.toPath());
logger.debug(LocalizableMessage.raw("Parent directory %s created.", parentDirectory.getPath()));
}
- if (!destination.exists())
- {
- destination.createNewFile();
- }
logger.debug(LocalizableMessage.raw("Writing entries in %s.", destination.getAbsolutePath()));
+ // The destination file is created by the output stream below if it does not exist yet.
try (LDIFEntryWriter writer = new LDIFEntryWriter(new FileOutputStream(destination)))
{
writer.writeEntry(theNewSchemaEntry);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java b/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
index 5c505e3..189e2b6 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.types;
@@ -22,6 +23,7 @@
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
+import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
@@ -289,7 +291,7 @@
// Rename the new descriptor file to match the previous one.
try
{
- newDescriptorFile.renameTo(descriptorFile);
+ renameFile(newDescriptorFile, descriptorFile);
}
catch (Exception e)
{
@@ -307,7 +309,7 @@
{
try
{
- dir.mkdirs();
+ Files.createDirectories(dir.toPath());
}
catch (Exception e)
{
@@ -328,24 +330,11 @@
{
String savedDescriptorFilePath = descriptorFilePath + ".save";
File savedDescriptorFile = new File(savedDescriptorFilePath);
- if (savedDescriptorFile.exists())
- {
- try
- {
- savedDescriptorFile.delete();
- }
- catch (Exception e)
- {
- logger.traceException(e);
- LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.get(
- savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath);
- throw new IOException(message.toString());
- }
- }
try
{
- descriptorFile.renameTo(savedDescriptorFile);
+ // renameFile() deletes an existing target and reports a failure to do so.
+ renameFile(descriptorFile, savedDescriptorFile);
}
catch (Exception e)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java b/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
index f7e4a61..5a383d7 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
@@ -13,6 +13,7 @@
*
* Copyright 2006-2009 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.types;
@@ -195,31 +196,24 @@
case APPEND:
// Create new file if it doesn't exist ensuring that we can
// set its permissions.
- if (!f.exists())
- {
- f.createNewFile();
- mustSetPermissions = true;
- }
+ mustSetPermissions = f.createNewFile();
ldifOutputStream = new FileOutputStream(ldifFile, true);
break;
case OVERWRITE:
// Create new file if it doesn't exist ensuring that we can
// set its permissions.
- if (!f.exists())
- {
- f.createNewFile();
- mustSetPermissions = true;
- }
+ mustSetPermissions = f.createNewFile();
ldifOutputStream = new FileOutputStream(ldifFile, false);
break;
case FAIL:
- if (f.exists())
+ // Create new file ensuring that we can set its permissions. The creation is
+ // atomic, hence it also fails if the file was created by someone else in
+ // the mean time.
+ if (!f.createNewFile())
{
LocalizableMessage message = ERR_LDIF_FILE_EXISTS.get(ldifFile);
throw new IOException(message.toString());
}
- // Create new file ensuring that we can set its permissions.
- f.createNewFile();
mustSetPermissions = true;
ldifOutputStream = new FileOutputStream(ldifFile);
break;
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/core.properties b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
index ee4a3e0..7be7a7d 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/core.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/core.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
#
@@ -1076,6 +1077,9 @@
provided object class set because it used an undefined token %s
ERR_COMPRESSEDSCHEMA_CANNOT_WRITE_UPDATED_DATA_622=Unable to write the \
updated compressed schema token data: %s
+WARN_COMPRESSEDSCHEMA_CANNOT_SAVE_PREVIOUS_DATA_756=Unable to keep a copy of \
+ the previous compressed schema token data by renaming %s to %s: %s. The \
+ updated token data has still been written
ERR_ENTRYENCODECFG_INVALID_LENGTH_623=Unable to decode the provided \
entry encode configuration element because it has an invalid length
INFO_RESULT_NO_OPERATION_624=No Operation
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/logger.properties b/opendj-server-legacy/src/messages/org/opends/messages/logger.properties
index de90779..8b0486c 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/logger.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/logger.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2008 Sun Microsystems, Inc.
# Portions Copyright 2015-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
#
# Global directives
@@ -100,3 +101,10 @@
common audit log publisher %s, the keystore file %s could not be read: %s
ERR_COMMON_AUDIT_KEYSTORE_FILE_IS_EMPTY_33=Error while processing \
common audit log publisher %s, the keystore file %s is empty
+ERR_LOGGER_ERROR_ROTATING_FILE_34=Error occurred while rotating log file %s to \
+ %s. The current log file will be kept and appended to, and the rotation will be \
+ retried on the next interval. Any further rotation errors will be ignored until \
+ a rotation succeeds
+WARN_LOGGER_ERROR_DELETING_FILE_35=Error occurred while deleting log file %s \
+ while enforcing retention policy %s. Any further deletion errors will be ignored \
+ until a log file is deleted successfully
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
index 3046004..9913479 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2014-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
@@ -235,6 +236,10 @@
Referential Integrity plugin update log file: %s
ERR_PLUGIN_REFERENT_REPLACE_LOGFILE_84=An error occurred replacing the \
Referential Integrity plugin update log file: %s
+ERR_PLUGIN_REFERENT_CANNOT_REPLACE_LOGFILE_130=The Referential Integrity \
+ plugin update log file %s could not be deleted and created again after its \
+ records were processed. Those records will be processed again the next time \
+ the log file is read
INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART_85=The file name that \
the Referential Integrity plugin logs changes to during background \
processing has been changed from %s to %s, but this change will not take \
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java
new file mode 100644
index 0000000..adbc1c5
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java
@@ -0,0 +1,316 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.loggers;
+
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.server.config.server.SizeLimitLogRotationPolicyCfg;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.types.FilePermission;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** Tests the rotation of {@link MultifileTextWriter}, in particular when the rename fails. */
+@SuppressWarnings("javadoc")
+public class MultifileTextWriterTestCase extends DirectoryServerTestCase
+{
+ private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+ private static final DN PUBLISHER_DN = DN.valueOf("cn=Test Logger,cn=Loggers,cn=config");
+ /** Small enough for two records to overflow it, large enough for one not to. */
+ private static final long SIZE_LIMIT = 64;
+ private static final String RECORD_A = "a".repeat(32);
+ private static final String RECORD_B = "b".repeat(32);
+ /** Long enough to leave the re-opened stream over the size limit together with the first record. */
+ private static final String STAND_IN_WARNING =
+ "stand-in for the permission warnings logged while the writer is re-opened";
+
+ /** Naming policy with fixed names, so that the test does not depend on the current second. */
+ private static final class FixedNamingPolicy implements FileNamingPolicy
+ {
+ private final File initialFile;
+ private final File nextFile;
+
+ private FixedNamingPolicy(File initialFile, File nextFile)
+ {
+ this.initialFile = initialFile;
+ this.nextFile = nextFile;
+ }
+
+ @Override
+ public File getInitialName()
+ {
+ return initialFile;
+ }
+
+ @Override
+ public File getNextName()
+ {
+ return nextFile;
+ }
+
+ @Override
+ public FilenameFilter getFilenameFilter()
+ {
+ return new FilenameFilter()
+ {
+ @Override
+ public boolean accept(File dir, String name)
+ {
+ return name.equals(nextFile.getName());
+ }
+ };
+ }
+
+ @Override
+ public File[] listFiles()
+ {
+ return new File[0];
+ }
+ }
+
+ /**
+ * A file which can run a hook from within constructWriter(): after a failed rotation the writer
+ * is re-opened, and {@code FilePermission.setPermissions} checks {@code exists()} at the exact
+ * point where the permission warnings are logged.
+ */
+ private static final class HookedFile extends File
+ {
+ private static final long serialVersionUID = 1L;
+ private transient Runnable onExists;
+
+ private HookedFile(File parent, String child)
+ {
+ super(parent, child);
+ }
+
+ @Override
+ public boolean exists()
+ {
+ if (onExists != null)
+ {
+ onExists.run();
+ }
+ return super.exists();
+ }
+ }
+
+ private File tempDir;
+ private File logFile;
+ private File rotatedFile;
+ private MultifileTextWriter writer;
+
+ @BeforeClass
+ public void startServer() throws Exception
+ {
+ // The writer registers itself as a shutdown listener, which requires a bootstrapped server.
+ TestCaseUtils.startServer();
+ }
+
+ @BeforeMethod
+ public void setUp() throws Exception
+ {
+ tempDir = Files.createTempDirectory("MultifileTextWriterTestCase").toFile();
+ logFile = new File(tempDir, "test.log");
+ rotatedFile = new File(tempDir, "test.log.rotated");
+ }
+
+ @AfterMethod
+ public void tearDown() throws Exception
+ {
+ if (writer != null)
+ {
+ writer.shutdown();
+ writer = null;
+ }
+ deleteRecursively(tempDir);
+ }
+
+ /**
+ * Makes the rename attempted by the rotation fail on every platform: renaming a file onto a
+ * non-empty directory is refused both on POSIX systems and on Windows.
+ */
+ private void breakRotation() throws Exception
+ {
+ assertTrue(rotatedFile.mkdir());
+ assertTrue(new File(rotatedFile, "blocker").createNewFile());
+ }
+
+ private MultifileTextWriter newWriter() throws Exception
+ {
+ MultifileTextWriter newWriter = new MultifileTextWriter("Multifile Text Writer for " + PUBLISHER_DN,
+ Long.MAX_VALUE, new FixedNamingPolicy(logFile, rotatedFile), FilePermission.decodeUNIXMode("600"),
+ new LogPublisherErrorHandler(PUBLISHER_DN), "UTF-8", true, true, 0);
+
+ SizeLimitLogRotationPolicyCfg config = mock(SizeLimitLogRotationPolicyCfg.class);
+ when(config.getFileSizeLimit()).thenReturn(SIZE_LIMIT);
+ SizeBasedRotationPolicy policy = new SizeBasedRotationPolicy();
+ policy.initializeLogRotationPolicy(config);
+ newWriter.addRotationPolicy(policy);
+
+ // The rotater thread is deliberately not started: the rotations under test are the ones
+ // triggered inline by writeRecord().
+ return newWriter;
+ }
+
+ private List<String> linesOf(File file) throws Exception
+ {
+ return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
+ }
+
+ private static void deleteRecursively(File file)
+ {
+ if (file.isDirectory())
+ {
+ for (File child : file.listFiles())
+ {
+ deleteRecursively(child);
+ }
+ }
+ file.delete();
+ }
+
+ @Test
+ public void testSuccessfulRotationRenamesTheFile() throws Exception
+ {
+ writer = newWriter();
+
+ writer.writeRecord(RECORD_A);
+ writer.writeRecord(RECORD_B);
+ writer.flush();
+
+ assertEquals(writer.getTotalFilesRotated(), 1);
+ assertEquals(linesOf(rotatedFile), List.of(RECORD_A));
+ assertEquals(linesOf(logFile), List.of(RECORD_B));
+ }
+
+ @Test
+ public void testFailedRotationAppendsInsteadOfTruncating() throws Exception
+ {
+ breakRotation();
+ writer = newWriter();
+
+ writer.writeRecord(RECORD_A);
+ assertEquals(writer.getTotalFilesRotated(), 0);
+
+ // Overflows the size limit, so a rotation is attempted and fails.
+ writer.writeRecord(RECORD_B);
+ writer.flush();
+
+ assertEquals(writer.getTotalFilesRotated(), 0, "a failed rotation must not be counted");
+ assertTrue(rotatedFile.isDirectory(), "the rotation target must have been left alone");
+ assertEquals(linesOf(logFile), List.of(RECORD_A, RECORD_B),
+ "the log file must have been appended to rather than truncated");
+ }
+
+ /**
+ * A failed rotation of the file backing the error log used to come back into writeRecord() on the
+ * same thread, from a writer which had just been closed and was still over the size limit, and to
+ * recurse until the stack blew up.
+ */
+ @Test
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public void testFailedRotationOfTheErrorLogDoesNotRecurse() throws Exception
+ {
+ breakRotation();
+ writer = newWriter();
+
+ MultifileTextWriter publishing = writer;
+ ErrorLogPublisher publisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(publishing);
+ ErrorLogger.getInstance().addLogPublisher(publisher);
+ try
+ {
+ publishing.writeRecord(RECORD_A);
+ // Overflows the size limit: the rotation fails and reports the failure through the very
+ // logger this writer is backing.
+ publishing.writeRecord(RECORD_B);
+ publishing.flush();
+ }
+ finally
+ {
+ // Removing the publisher closes it, which already shuts this writer down.
+ ErrorLogger.getInstance().removeLogPublisher(publisher);
+ writer = null;
+ }
+
+ assertEquals(publishing.getTotalFilesRotated(), 0);
+
+ List<String> lines = linesOf(logFile);
+ assertEquals(lines.get(0), RECORD_A, "the log file must have been appended to");
+ assertTrue(lines.contains(RECORD_B), "the record which triggered the rotation must not be lost");
+ assertTrue(lines.stream().anyMatch(line -> line.contains("rotating log file")),
+ "the rotation failure must be reported in the log file, but it contained: " + lines);
+ }
+
+ /**
+ * constructWriter() logs permission warnings of its own, between re-seeding the stream with the
+ * over-limit file length and the point where the failed rotation used to set its latch. When the
+ * writer backs the error log, such a warning used to re-enter writeRecord() and to recurse until
+ * the stack blew up. The hook stands in for the permission warning, firing at the same point of
+ * the re-open.
+ */
+ @Test
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public void testWarningDuringReopenAfterFailedRotationDoesNotRecurse() throws Exception
+ {
+ breakRotation();
+ HookedFile hookedLogFile = new HookedFile(tempDir, logFile.getName());
+ logFile = hookedLogFile;
+ writer = newWriter();
+
+ MultifileTextWriter publishing = writer;
+ ErrorLogPublisher publisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(publishing);
+ ErrorLogger.getInstance().addLogPublisher(publisher);
+ try
+ {
+ publishing.writeRecord(RECORD_A);
+ hookedLogFile.onExists = () -> logger.warn(LocalizableMessage.raw(STAND_IN_WARNING));
+ // Overflows the size limit: the rotation fails, and while the writer is re-opened the hook
+ // logs through the very logger this writer is backing, like the permission warnings do.
+ publishing.writeRecord(RECORD_B);
+ publishing.flush();
+ }
+ finally
+ {
+ // Removing the publisher closes it, which already shuts this writer down.
+ ErrorLogger.getInstance().removeLogPublisher(publisher);
+ writer = null;
+ }
+
+ assertEquals(publishing.getTotalFilesRotated(), 0);
+
+ List<String> lines = linesOf(logFile);
+ assertEquals(lines.get(0), RECORD_A, "the log file must have been appended to");
+ assertTrue(lines.stream().anyMatch(line -> line.contains(STAND_IN_WARNING)),
+ "the warning logged during the re-open must not be lost, but the log file contained: " + lines);
+ assertTrue(lines.contains(RECORD_B), "the record which triggered the rotation must not be lost");
+ assertEquals(lines.stream().filter(line -> line.contains("rotating log file")).count(), 1L,
+ "the rotation failure must be reported exactly once, but the log file contained: " + lines);
+ }
+}
--
Gitblit v1.10.0