mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
16 hours ago ebb19c5816399b83c42b8f9194dbf3d9296975a3
Fix concurrency and equals-contract CodeQL alerts (#785)
8 files modified
2 files added
466 ■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/admin/ads/SuffixDescriptor.java 26 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/datamodel/ScheduleType.java 3 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java 30 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java 74 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/loggers/DebugLogPublisher.java 37 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/plugins/profiler/ProfileStack.java 28 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/plugins/profiler/ProfileStackFrame.java 28 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/util/cli/PointAdder.java 31 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/admin/ads/SuffixDescriptorTest.java 132 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/datamodel/ScheduleTypeTest.java 77 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/admin/ads/SuffixDescriptor.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2015-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.admin.ads;
@@ -107,6 +108,13 @@
  }
  @Override
  public boolean equals(Object o)
  {
    return this == o
        || (o instanceof SuffixDescriptor && getId().equals(((SuffixDescriptor) o).getId()));
  }
  @Override
  public int hashCode()
  {
    return getId().hashCode();
@@ -114,16 +122,28 @@
  /**
   * Returns an Id that is unique for this suffix.
   * <p>
   * The id is built from the suffix DN and the ids of the servers holding a replica of it.
   * The server ids are sorted because {@link #getReplicas()} hands out a {@link HashSet} of
   * {@link ReplicaDescriptor}s, which do not override {@code hashCode()}: without the sort,
   * two descriptors describing the same suffix on the same servers could produce different
   * ids depending on the iteration order.
   *
   * @return an Id that is unique for this suffix.
   */
  public String getId()
  {
    StringBuilder buf = new StringBuilder();
    buf.append(getDN());
    Set<String> serverIds = new TreeSet<>();
    for (ReplicaDescriptor replica : getReplicas())
    {
      buf.append("-").append(replica.getServer().getId());
      serverIds.add(replica.getServer().getId());
    }
    StringBuilder buf = new StringBuilder();
    buf.append(getDN());
    for (String serverId : serverIds)
    {
      buf.append("-").append(serverId);
    }
    return buf.toString();
  }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/datamodel/ScheduleType.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2009 Sun Microsystems, Inc.
 * Portions Copyright 2015-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.datamodel;
@@ -119,7 +120,7 @@
    {
      return true;
    }
    return o != null
    return o instanceof ScheduleType
        && toString().equals(o.toString());
  }
opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2012-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.quicksetup;
@@ -799,9 +800,12 @@
  /** Class used to add points periodically to the end of the logs. */
  protected class PointAdder implements Runnable
  {
    private Thread t;
    private boolean stopPointAdder;
    private boolean pointAdderStopped;
    /** Written by start() and read by stop(), hence volatile. */
    private volatile Thread t;
    /** Written by stop() and read by the point adder thread, hence volatile. */
    private volatile boolean stopPointAdder;
    /** Written by the point adder thread and read by stop(), hence volatile. */
    private volatile boolean pointAdderStopped;
    /** Default constructor. */
    public PointAdder()
@@ -825,21 +829,31 @@
      t.start();
    }
    /** Stops the PointAdder: points are no longer added at the end of the logs periodically. */
    public synchronized void stop()
    /**
     * Stops the PointAdder: points are no longer added at the end of the logs periodically.
     * <p>
     * Calling this method on a PointAdder that was never started is a no-op.
     */
    public void stop()
    {
      stopPointAdder = true;
      final Thread thread = t;
      if (thread == null)
      {
        return;
      }
      while (!pointAdderStopped)
      {
        thread.interrupt();
        try
        {
          t.interrupt();
          // To allow the thread to set the boolean.
          Thread.sleep(100);
        }
        catch (Throwable t)
        catch (InterruptedException e)
        {
          // do nothing
          Thread.currentThread().interrupt();
          break;
        }
      }
    }
opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2010-2016 ForgeRock AS.
 * Portions Copyright 2022-2025 3A Systems, LLC.
 * Portions Copyright 2022-2026 3A Systems, LLC.
 * Portions Copyright 2025 Wren Security.
 */
package org.opends.server.core;
@@ -177,11 +177,27 @@
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /** The singleton Directory Server instance. */
  private static DirectoryServer directoryServer = new DirectoryServer();
  /**
   * Guards the life cycle of the {@link #directoryServer} singleton: bootstrapping,
   * starting, shutting down, and replacing the instance during an in-core restart.
   * A dedicated lock is used because the {@code directoryServer} field is reassigned
   * by {@link #getNewInstance(DirectoryEnvironmentConfig)}: synchronizing on the field
   * itself would let threads acquire different monitors and provide no exclusion at all.
   * <p>
   * The lock makes the guarded transitions atomic; it does not make the life cycle flags
   * visible to the code that reads them without holding it (see {@link #isRunning()} and
   * {@link #isShuttingDown()}), which is why those flags are {@code volatile}.
   */
  private static final Object LIFECYCLE_LOCK = new Object();
  /** Indicates whether the server currently holds an exclusive lock on the server lock file. */
  private static boolean serverLocked;
  /** The singleton Directory Server instance. */
  private static volatile DirectoryServer directoryServer = new DirectoryServer();
  /**
   * Indicates whether the server currently holds an exclusive lock on the server lock file.
   * Written outside {@link #LIFECYCLE_LOCK} on shutdown, hence volatile.
   */
  private static volatile boolean serverLocked;
  /** The message to be displayed on the command-line when the user asks for the usage. */
  private static final LocalizableMessage toolDescription = INFO_DSCORE_TOOL_DESCRIPTION.get();
@@ -228,15 +244,18 @@
  /** The configuration manager that will handle the server backends. */
  private BackendConfigManager backendConfigManager;
  /** Indicates whether the server has been bootstrapped. */
  private boolean isBootstrapped;
  /** Indicates whether the server is currently online. */
  private boolean isRunning;
  /** Indicates whether the server has been bootstrapped. Read without holding {@link #LIFECYCLE_LOCK}. */
  private volatile boolean isBootstrapped;
  /** Indicates whether the server is currently online. Read without holding {@link #LIFECYCLE_LOCK}. */
  private volatile boolean isRunning;
  /** Indicates whether the server is currently in "lockdown mode". */
  private boolean lockdownMode;
  /** Indicates whether the server is currently in the process of shutting down. */
  private boolean shuttingDown;
  /**
   * Indicates whether the server is currently in the process of shutting down.
   * Read without holding {@link #LIFECYCLE_LOCK}.
   */
  private volatile boolean shuttingDown;
  /** The configuration manager that will handle the certificate mapper. */
  private CertificateMapperConfigManager certificateMapperConfigManager;
@@ -284,7 +303,7 @@
   * a mapping between the DN of the associated configuration entry and the
   * policy implementation.
   */
  private ConcurrentMap<DN, AuthenticationPolicy> authenticationPolicies;
  private final ConcurrentMap<DN, AuthenticationPolicy> authenticationPolicies = new ConcurrentHashMap<>();
  /**
   * The set of password validators registered with the Directory Server, as a
@@ -356,7 +375,7 @@
  /** The set of alert handlers registered with the Directory Server. */
  private List<AlertHandler<?>> alertHandlers;
  /** The set of connection handlers registered with the Directory Server. */
  private List<ConnectionHandler<?>> connectionHandlers;
  private final List<ConnectionHandler<?>> connectionHandlers = new CopyOnWriteArrayList<>();
  /** The set of backup task listeners registered with the Directory Server. */
  private CopyOnWriteArrayList<BackupTaskListener> backupTaskListeners;
@@ -430,7 +449,7 @@
  private KeyManagerProviderConfigManager keyManagerProviderConfigManager;
  /** The set of connections that are currently established. */
  private Set<ClientConnection> establishedConnections;
  private final Set<ClientConnection> establishedConnections = new LinkedHashSet<>(1000);
  /** The log rotation policy config manager for the Directory Server. */
  private LogRotationPolicyConfigManager rotationPolicyConfigManager;
@@ -1059,7 +1078,7 @@
  private static DirectoryServer
                      getNewInstance(DirectoryEnvironmentConfig config)
  {
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      return directoryServer = new DirectoryServer(config);
    }
@@ -1122,7 +1141,7 @@
   */
  public static void bootstrapClient()
  {
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      // Schema handler contains a default schema to start with
      directoryServer.schemaHandler = new SchemaHandler();
@@ -1143,14 +1162,14 @@
      directoryServer.rotationPolicies = new ConcurrentHashMap<>();
      directoryServer.retentionPolicies = new ConcurrentHashMap<>();
      directoryServer.certificateMappers = new ConcurrentHashMap<>();
      directoryServer.authenticationPolicies = new ConcurrentHashMap<>();
      directoryServer.authenticationPolicies.clear();
      directoryServer.defaultPasswordPolicy = null;
      directoryServer.monitorProviders = new ConcurrentHashMap<>();
      directoryServer.initializationCompletedListeners = new CopyOnWriteArrayList<>();
      directoryServer.shutdownListeners = new CopyOnWriteArrayList<>();
      directoryServer.synchronizationProviders = new CopyOnWriteArrayList<>();
      directoryServer.supportedLDAPVersions = new ConcurrentHashMap<>();
      directoryServer.connectionHandlers = new CopyOnWriteArrayList<>();
      directoryServer.connectionHandlers.clear();
      directoryServer.identityMappers = new ConcurrentHashMap<>();
      directoryServer.extendedOperationHandlers = new ConcurrentHashMap<>();
      directoryServer.saslMechanismHandlers = new ConcurrentHashMap<>();
@@ -1180,7 +1199,7 @@
    // First, make sure that the server isn't currently running.  If it isn't,
    // then make sure that no other thread will try to start or bootstrap the
    // server before this thread is done.
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      if (isRunning)
      {
@@ -1205,7 +1224,10 @@
    bootstrapClient();
    // Initialize the variables that will be used for connection tracking.
    establishedConnections = new LinkedHashSet<>(1000);
    synchronized (establishedConnections)
    {
      establishedConnections.clear();
    }
    currentConnections     = 0;
    maxConnections         = 0;
    totalConnections       = 0;
@@ -1216,7 +1238,7 @@
    pluginConfigManager = new PluginConfigManager(serverContext);
    // If we have gotten here, then the configuration should be properly bootstrapped.
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      isBootstrapped = true;
    }
@@ -1331,7 +1353,7 @@
      throw new InitializationException(e.getMessageObject());
    }
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      if (! isBootstrapped)
      {
@@ -2758,7 +2780,7 @@
   */
  public static void setEntryCache(EntryCache<?> entryCache)
  {
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      directoryServer.entryCache = entryCache;
    }
@@ -4084,7 +4106,7 @@
   */
  public static void shutDown(String className, LocalizableMessage reason)
  {
    synchronized (directoryServer)
    synchronized (LIFECYCLE_LOCK)
    {
      if (directoryServer.shuttingDown)
      {
@@ -4319,7 +4341,9 @@
    // doing that, destroy the previous instance.
    DirectoryEnvironmentConfig envConfig = directoryServer.environmentConfig;
    directoryServer.destroy();
    directoryServer = getNewInstance(envConfig);
    // getNewInstance() publishes the new instance under LIFECYCLE_LOCK: assigning its
    // result here again would repeat that write outside the lock.
    getNewInstance(envConfig);
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/loggers/DebugLogPublisher.java
@@ -13,13 +13,13 @@
 *
 * Copyright 2009 Sun Microsystems, Inc.
 * Portions Copyright 2012-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.loggers;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.server.config.server.DebugLogPublisherCfg;
@@ -42,11 +42,24 @@
  /** The default global settings key. */
  private static final String GLOBAL= "_global";
  /** The map of class names to their trace settings. */
  private Map<String,TraceSettings> classTraceSettings;
  /**
   * The map of class names to their trace settings.
   * <p>
   * The getters below read this map without any lock, so the field is volatile
   * (the map is created lazily and must be published safely) and the map itself
   * is concurrent; all writes go through the synchronized mutators below.
   */
  private volatile Map<String,TraceSettings> classTraceSettings;
  /** The map of class names to their method trace settings. */
  private Map<String,Map<String,TraceSettings>> methodTraceSettings;
  /**
   * The map of class names to their method trace settings.
   * <p>
   * Concurrent for the same reason as {@link #classTraceSettings}. The nested maps
   * are concurrent as well: {@link DebugTracer} keeps the one returned by
   * {@link #getMethodSettings(String)} and iterates it while another thread may be
   * reconfiguring the publisher.
   */
  private volatile Map<String,Map<String,TraceSettings>> methodTraceSettings;
@@ -146,6 +159,10 @@
   *                   {@code null} to set the trace settings for the
   *                   global scope.
   * @param  settings  The trace settings for the specified scope.
   *                   Must not be {@code null}.
   * @throws NullPointerException  If {@code settings} is {@code null}: the trace
   *                   settings are held in concurrent maps, which do not accept
   *                   null values.
   */
  public final void addTraceSettings(String scope, TraceSettings settings)
  {
@@ -213,7 +230,7 @@
   *          {@code null} if no trace setting is defined for that
   *          scope.
   */
  final TraceSettings removeTraceSettings(String scope)
  final synchronized TraceSettings removeTraceSettings(String scope)
  {
    TraceSettings removedSettings = null;
    if (scope == null) {
@@ -262,7 +279,7 @@
  {
    if (classTraceSettings == null)
    {
      classTraceSettings = new HashMap<>();
      classTraceSettings = new ConcurrentHashMap<>();
    }
    classTraceSettings.put(className, settings);
  }
@@ -280,12 +297,12 @@
      String methodName, TraceSettings settings)
  {
    if (methodTraceSettings == null) {
      methodTraceSettings = new HashMap<>();
      methodTraceSettings = new ConcurrentHashMap<>();
    }
    Map<String, TraceSettings> methodLevels = methodTraceSettings.get(className);
    if (methodLevels == null)
    {
      methodLevels = new TreeMap<>();
      methodLevels = new ConcurrentHashMap<>();
      methodTraceSettings.put(className, methodLevels);
    }
    methodLevels.put(methodName, settings);
opendj-server-legacy/src/main/java/org/opends/server/plugins/profiler/ProfileStack.java
@@ -13,12 +13,12 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.plugins.profiler;
import java.io.IOException;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.io.ASN1Writer;
@@ -28,11 +28,6 @@
 */
public class ProfileStack
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /**
   * The line number that will be used for stack frames in which the line number
   * is unknown but it is not a native method.
@@ -240,20 +235,16 @@
  @Override
  public boolean equals(Object o)
  {
    if (o == null)
    {
      return false;
    }
    else if (this == o)
    if (this == o)
    {
      return true;
    }
    try
    if (!(o instanceof ProfileStack))
    {
      ProfileStack s = (ProfileStack) o;
      return false;
    }
    ProfileStack s = (ProfileStack) o;
      if (numFrames != s.numFrames)
      {
        return false;
@@ -271,13 +262,6 @@
      return true;
    }
    catch (Exception e)
    {
      logger.traceException(e);
      return false;
    }
  }
opendj-server-legacy/src/main/java/org/opends/server/plugins/profiler/ProfileStackFrame.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.plugins.profiler;
@@ -21,9 +22,6 @@
import java.util.Arrays;
import java.util.HashMap;
import org.forgerock.i18n.slf4j.LocalizedLogger;
/**
 * This class defines a data structure for holding information about a stack
 * frame captured by the Directory Server profiler.  It will contain the class
@@ -34,11 +32,6 @@
public class ProfileStackFrame
       implements Comparable
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /**
   * The mapping between the line numbers for this stack frame and the
   * number of times that they were encountered.
@@ -302,26 +295,17 @@
  @Override
  public boolean equals(Object o)
  {
    if (o == null)
    {
      return false;
    }
    else if (this == o)
    if (this == o)
    {
      return true;
    }
    try
    if (!(o instanceof ProfileStackFrame))
    {
      ProfileStackFrame f = (ProfileStackFrame) o;
      return className.equals(f.className) && methodName.equals(f.methodName);
    }
    catch (Exception e)
    {
      logger.traceException(e);
      return false;
    }
    ProfileStackFrame f = (ProfileStackFrame) o;
    return className.equals(f.className) && methodName.equals(f.methodName);
  }
opendj-server-legacy/src/main/java/org/opends/server/util/cli/PointAdder.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2010 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.util.cli;
@@ -26,9 +27,12 @@
public class PointAdder implements Runnable
{
  private final ConsoleApplication app;
  private Thread t;
  private boolean stopPointAdder;
  private boolean pointAdderStopped;
  /** Written by start() and read by stop(), hence volatile. */
  private volatile Thread t;
  /** Written by stop() and read by the point adder thread, hence volatile. */
  private volatile boolean stopPointAdder;
  /** Written by the point adder thread and read by stop(), hence volatile. */
  private volatile boolean pointAdderStopped;
  private final long periodTime;
  private final ProgressMessageFormatter formatter;
@@ -78,20 +82,33 @@
    t.start();
  }
  /** Stops the PointAdder: points are no longer added at the end of the logs periodically. */
  public synchronized void stop()
  /**
   * Stops the PointAdder: points are no longer added at the end of the logs periodically.
   * <p>
   * Calling this method on a PointAdder that was never started is a no-op: callers commonly
   * create the PointAdder before the code path that starts it and stop it from a
   * {@code finally} block.
   */
  public void stop()
  {
    stopPointAdder = true;
    final Thread thread = t;
    if (thread == null)
    {
      return;
    }
    while (!pointAdderStopped)
    {
      thread.interrupt();
      try
      {
        t.interrupt();
        // To allow the thread to set the boolean.
        Thread.sleep(100);
      }
      catch (Throwable t)
      catch (InterruptedException e)
      {
        Thread.currentThread().interrupt();
        break;
      }
    }
  }
opendj-server-legacy/src/test/java/org/opends/admin/ads/SuffixDescriptorTest.java
New file
@@ -0,0 +1,132 @@
/*
 * 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.admin.ads;
import static org.assertj.core.api.Assertions.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.forgerock.opendj.ldap.DN;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.Test;
/** Tests the id-based {@code equals()}/{@code hashCode()} contract of {@link SuffixDescriptor}. */
@Test(groups = { "precommit", "admin" }, sequential = true)
@SuppressWarnings("javadoc")
public class SuffixDescriptorTest extends DirectoryServerTestCase
{
  private static final DN SUFFIX_DN = DN.valueOf("dc=example,dc=com");
  private static final DN OTHER_SUFFIX_DN = DN.valueOf("dc=example,dc=org");
  @Test
  public void suffixesWithTheSameIdAreEqual()
  {
    ServerDescriptor server1 = server("server1.example.com");
    ServerDescriptor server2 = server("server2.example.com");
    SuffixDescriptor suffix1 = suffix(SUFFIX_DN, server1, server2);
    SuffixDescriptor suffix2 = suffix(SUFFIX_DN, server1, server2);
    assertThat(suffix1.getId()).isEqualTo(suffix2.getId());
    assertThat(suffix1).isEqualTo(suffix2);
    assertThat(suffix1.hashCode()).isEqualTo(suffix2.hashCode());
    assertThat(suffix1.compareTo(suffix2)).isZero();
  }
  @Test
  public void suffixesWithTheSameIdCollapseInASet()
  {
    ServerDescriptor server = server("server1.example.com");
    Set<SuffixDescriptor> suffixes = new HashSet<>();
    suffixes.add(suffix(SUFFIX_DN, server));
    suffixes.add(suffix(SUFFIX_DN, server));
    assertThat(suffixes).hasSize(1);
  }
  @Test
  public void suffixesOnDifferentDnsAreNotEqual()
  {
    ServerDescriptor server = server("server1.example.com");
    assertThat(suffix(SUFFIX_DN, server)).isNotEqualTo(suffix(OTHER_SUFFIX_DN, server));
  }
  @Test
  public void suffixesOnDifferentServersAreNotEqual()
  {
    SuffixDescriptor suffix1 = suffix(SUFFIX_DN, server("server1.example.com"));
    SuffixDescriptor suffix2 = suffix(SUFFIX_DN, server("server2.example.com"));
    assertThat(suffix1).isNotEqualTo(suffix2);
  }
  @Test
  public void suffixIsNotEqualToItsId()
  {
    SuffixDescriptor suffix = suffix(SUFFIX_DN, server("server1.example.com"));
    assertThat(suffix.equals(suffix.getId())).isFalse();
    assertThat(suffix.equals(null)).isFalse();
  }
  /**
   * The id must not depend on the iteration order of the replica set: {@code getReplicas()}
   * hands out a {@code HashSet} of {@code ReplicaDescriptor}s, which do not override
   * {@code hashCode()}.
   */
  @Test
  public void idIsIndependentOfTheReplicaOrder()
  {
    ServerDescriptor server1 = server("server1.example.com");
    ServerDescriptor server2 = server("server2.example.com");
    assertThat(server1.getId()).isLessThan(server2.getId());
    String expectedId = SUFFIX_DN + "-" + server1.getId() + "-" + server2.getId();
    assertThat(suffix(SUFFIX_DN, server1, server2).getId()).isEqualTo(expectedId);
    assertThat(suffix(SUFFIX_DN, server2, server1).getId()).isEqualTo(expectedId);
  }
  private ServerDescriptor server(String hostName)
  {
    Map<ADSContext.ServerProperty, Object> adsProperties = new HashMap<>();
    adsProperties.put(ADSContext.ServerProperty.HOST_NAME, hostName);
    adsProperties.put(ADSContext.ServerProperty.LDAP_PORT, 1389);
    return ServerDescriptor.createStandalone(adsProperties);
  }
  private SuffixDescriptor suffix(DN suffixDN, ServerDescriptor... servers)
  {
    SuffixDescriptor suffix = new SuffixDescriptor(suffixDN, replica(servers[0]));
    for (int i = 1; i < servers.length; i++)
    {
      suffix.addReplica(replica(servers[i]));
    }
    return suffix;
  }
  private ReplicaDescriptor replica(ServerDescriptor server)
  {
    ReplicaDescriptor replica = new ReplicaDescriptor();
    replica.setServer(server);
    return replica;
  }
}
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/datamodel/ScheduleTypeTest.java
New file
@@ -0,0 +1,77 @@
/*
 * 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.guitools.controlpanel.datamodel;
import static org.assertj.core.api.Assertions.*;
import java.util.Date;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.Test;
/** Tests the {@code equals()} contract of {@link ScheduleType}. */
@Test(groups = { "precommit", "controlpanel" }, sequential = true)
@SuppressWarnings("javadoc")
public class ScheduleTypeTest extends DirectoryServerTestCase
{
  @Test
  public void schedulesOfTheSameTypeAreEqual()
  {
    ScheduleType schedule1 = ScheduleType.createCron("0 0 * * *");
    ScheduleType schedule2 = ScheduleType.createCron("0 0 * * *");
    assertThat(schedule1).isEqualTo(schedule2);
    assertThat(schedule1.hashCode()).isEqualTo(schedule2.hashCode());
  }
  @Test
  public void schedulesOfDifferentTypesAreNotEqual()
  {
    ScheduleType launchNow = ScheduleType.createLaunchNow();
    ScheduleType launchLater = ScheduleType.createLaunchLater(new Date(0));
    assertThat(launchNow).isNotEqualTo(launchLater);
    assertThat(launchNow).isNotEqualTo(ScheduleType.createCron("0 0 * * *"));
  }
  /**
   * A schedule must not compare equal to a plain object sharing its textual form: such a
   * comparison could never be symmetric.
   */
  @Test
  public void scheduleIsNotEqualToAStringWithTheSameTextualForm()
  {
    ScheduleType schedule = ScheduleType.createLaunchNow();
    String sameText = schedule.toString();
    assertThat(schedule.equals(sameText)).isFalse();
    assertThat(sameText.equals(schedule)).isFalse();
  }
  @Test
  public void scheduleIsNotEqualToNull()
  {
    assertThat(ScheduleType.createLaunchNow().equals(null)).isFalse();
  }
  @Test
  public void scheduleIsEqualToItself()
  {
    ScheduleType schedule = ScheduleType.createLaunchNow();
    assertThat(schedule).isEqualTo(schedule);
  }
}