diff --git a/openidm-cluster/src/main/java/org/forgerock/openidm/cluster/ClusterManager.java b/openidm-cluster/src/main/java/org/forgerock/openidm/cluster/ClusterManager.java index 060eccc0e0..c318e2b3c6 100644 --- a/openidm-cluster/src/main/java/org/forgerock/openidm/cluster/ClusterManager.java +++ b/openidm-cluster/src/main/java/org/forgerock/openidm/cluster/ClusterManager.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Portions copyright 2013-2016 ForgeRock AS. - * Portions Copyrighted 2024 3A Systems LLC. + * Portions Copyrighted 2024-2026 3A Systems LLC. */ package org.forgerock.openidm.cluster; @@ -530,7 +530,7 @@ private void checkOut() { // just update the timestamp state.setState(InstanceState.STATE_DOWN); updateInstanceState(instanceId, state); - logger.debug("Instance {} state updated successfully"); + logger.debug("Instance {} state updated successfully", instanceId); break; case InstanceState.STATE_DOWN: // Already down diff --git a/openidm-core/src/main/java/org/forgerock/openidm/sync/impl/ReconciliationService.java b/openidm-core/src/main/java/org/forgerock/openidm/sync/impl/ReconciliationService.java index 4a332839f1..dead1d8ba7 100644 --- a/openidm-core/src/main/java/org/forgerock/openidm/sync/impl/ReconciliationService.java +++ b/openidm-core/src/main/java/org/forgerock/openidm/sync/impl/ReconciliationService.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Portions copyright 2012-2015 ForgeRock AS. - * Portions Copyrighted 2024 3A Systems LLC. + * Portions Copyrighted 2024-2026 3A Systems LLC. */ package org.forgerock.openidm.sync.impl; @@ -31,7 +31,6 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import javax.management.MBeanServer; @@ -529,8 +528,6 @@ public ExecutorService getThreadPool() { public int getActiveThreads() throws ResourceException { if (fullReconExecutor instanceof ThreadPoolExecutor) { return ((ThreadPoolExecutor) fullReconExecutor).getActiveCount(); - } else if (fullReconExecutor instanceof ScheduledThreadPoolExecutor) { - return ((ScheduledThreadPoolExecutor) fullReconExecutor).getActiveCount(); } else { logger.error("Unable to get the number of active threads in recon thread pool"); throw new InternalServerErrorException("Unable to get the number of active threads in recon thread pool"); @@ -544,8 +541,6 @@ public int getActiveThreads() throws ResourceException { public int getCorePoolSize() throws ResourceException { if (fullReconExecutor instanceof ThreadPoolExecutor) { return ((ThreadPoolExecutor) fullReconExecutor).getCorePoolSize(); - } else if (fullReconExecutor instanceof ScheduledThreadPoolExecutor) { - return ((ScheduledThreadPoolExecutor) fullReconExecutor).getCorePoolSize(); } else { logger.error("Unable to get the core pool size in recon thread pool"); throw new InternalServerErrorException("Unable to get the core pool size in recon thread pool"); @@ -559,8 +554,6 @@ public int getCorePoolSize() throws ResourceException { public int getPoolSize() throws ResourceException { if (fullReconExecutor instanceof ThreadPoolExecutor) { return ((ThreadPoolExecutor) fullReconExecutor).getPoolSize(); - } else if (fullReconExecutor instanceof ScheduledThreadPoolExecutor) { - return ((ScheduledThreadPoolExecutor) fullReconExecutor).getPoolSize(); } else { logger.error("Unable to get the pool size in recon thread pool"); throw new InternalServerErrorException("Unable to get the pool size in recon thread pool"); @@ -574,8 +567,6 @@ public int getPoolSize() throws ResourceException { public int getLargestPoolSize() throws ResourceException { if (fullReconExecutor instanceof ThreadPoolExecutor) { return ((ThreadPoolExecutor) fullReconExecutor).getLargestPoolSize(); - } else if (fullReconExecutor instanceof ScheduledThreadPoolExecutor) { - return ((ScheduledThreadPoolExecutor) fullReconExecutor).getLargestPoolSize(); } else { logger.error("Unable to get the largest pool size in recon thread pool"); throw new InternalServerErrorException("Unable to get the largest pool size in recon thread pool"); @@ -589,8 +580,6 @@ public int getLargestPoolSize() throws ResourceException { public int getMaximumPoolSize() throws ResourceException { if (fullReconExecutor instanceof ThreadPoolExecutor) { return ((ThreadPoolExecutor) fullReconExecutor).getMaximumPoolSize(); - } else if (fullReconExecutor instanceof ScheduledThreadPoolExecutor) { - return ((ScheduledThreadPoolExecutor) fullReconExecutor).getMaximumPoolSize(); } else { logger.error("Unable to get the maximum pool size in recon thread pool"); throw new InternalServerErrorException("Unable to get the maximum pool size in recon thread pool"); diff --git a/openidm-maintenance/src/main/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFile.java b/openidm-maintenance/src/main/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFile.java index bac665dbab..5738ee1945 100644 --- a/openidm-maintenance/src/main/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFile.java +++ b/openidm-maintenance/src/main/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFile.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2015 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openidm.maintenance.upgrade; @@ -98,6 +99,15 @@ Path resolvePath(Path file) { return checksums.getParent().resolve(file); } + /** + * Shallow copy: the entries are copied, the checksum file path and the {@link MessageDigest} + * are shared with the original (the digest resets itself after every {@code digest()} call). + */ + @Override + public ChecksumFile clone() { + return (ChecksumFile) super.clone(); + } + private String computeDigest(byte[] data) { return hexAdapter.marshal(digest.digest(data)); } diff --git a/openidm-maintenance/src/test/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFileTest.java b/openidm-maintenance/src/test/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFileTest.java index 2652a3e736..777c528e72 100644 --- a/openidm-maintenance/src/test/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFileTest.java +++ b/openidm-maintenance/src/test/java/org/forgerock/openidm/maintenance/upgrade/ChecksumFileTest.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2015-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openidm.maintenance.upgrade; @@ -51,6 +52,17 @@ public void testChecksumFileNoSuchAlgorithm() throws IOException, NoSuchAlgorith new ChecksumFile(Paths.get(getClass().getResource("/unknownalgorithm.csv").toURI())); } + @Test + public void testCloneKeepsEntriesAndType() throws URISyntaxException, IOException, NoSuchAlgorithmException { + ChecksumFile original = new ChecksumFile(Paths.get(getClass().getResource("/checksums.csv").toURI())); + + ChecksumFile copy = original.clone(); + + assertThat(copy).isNotSameAs(original); + assertThat(copy).isEqualTo(original); + assertThat(copy.resolvePath(Paths.get("file1"))).isEqualTo(original.resolvePath(Paths.get("file1"))); + } + @Test public void testGetFiles() throws URISyntaxException, IOException, NoSuchAlgorithmException { Set files = new ChecksumFile(Paths.get(getClass().getResource("/checksums.csv").toURI())).getFilePaths(); diff --git a/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/ConnectorInfoProviderService.java b/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/ConnectorInfoProviderService.java index 0dc3ccecbd..b0fdfdc55f 100644 --- a/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/ConnectorInfoProviderService.java +++ b/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/ConnectorInfoProviderService.java @@ -738,8 +738,8 @@ public void handleResult(ConnectorInfo connectorInfo) { } properties = ci.createDefaultAPIConfiguration().getConfigurationProperties(); } catch (RuntimeException e) { - logger.error("Failed to parse the config of {}-{}: {}", new Object[] { - pidOrFactory, instanceAlias, e.getMessage()}, e); + logger.error("Failed to parse the config of {}-{}: {}", + pidOrFactory, instanceAlias, e.getMessage(), e); throw e; } diff --git a/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/OperationHelperImpl.java b/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/OperationHelperImpl.java index 778911cc3f..fc76babadc 100644 --- a/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/OperationHelperImpl.java +++ b/openidm-provisioner-openicf/src/main/java/org/forgerock/openidm/provisioner/openicf/impl/OperationHelperImpl.java @@ -42,7 +42,6 @@ import java.lang.reflect.UndeclaredThrowableException; import java.net.URI; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -54,7 +53,6 @@ public class OperationHelperImpl implements OperationHelper { private final ObjectClassInfoHelper objectClassInfoHelper; private final Map, OperationOptionInfoHelper> operations; - private final List> resultList = new ArrayList>(); private final Id systemObjectSetId; private final CryptoService cryptoService; diff --git a/openidm-provisioner/src/main/java/org/forgerock/openidm/provisioner/Id.java b/openidm-provisioner/src/main/java/org/forgerock/openidm/provisioner/Id.java index 578bfcac5c..30813e0ec1 100644 --- a/openidm-provisioner/src/main/java/org/forgerock/openidm/provisioner/Id.java +++ b/openidm-provisioner/src/main/java/org/forgerock/openidm/provisioner/Id.java @@ -170,7 +170,7 @@ public Id expectObjectId() throws ResourceException { if (StringUtils.isBlank(localId)) { ResourceException ex = new BadRequestException("This id instance does not qualified to identify a single unique object"); TRACE.error("Unqualified id: systemName={}, objectType={}, localId={}", - new Object[] { systemName, objectType, localId }, ex); + systemName, objectType, localId, ex); throw ex; } return this; diff --git a/openidm-quartz-fragment/src/main/java/org/forgerock/openidm/quartz/impl/RepoJobStore.java b/openidm-quartz-fragment/src/main/java/org/forgerock/openidm/quartz/impl/RepoJobStore.java index f4424c4d7a..ef8e668323 100644 --- a/openidm-quartz-fragment/src/main/java/org/forgerock/openidm/quartz/impl/RepoJobStore.java +++ b/openidm-quartz-fragment/src/main/java/org/forgerock/openidm/quartz/impl/RepoJobStore.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2012-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openidm.quartz.impl; @@ -134,11 +135,6 @@ public class RepoJobStore implements JobStore, ClusterEventListener { */ private int writeRetries = -1; - /** - * A list of all "blocked" jobs. - */ - private List blockedJobs = new ArrayList<>(); - /** * An AtomicLong used for creating record IDs */ @@ -1367,7 +1363,6 @@ public TriggerFiredBundle triggerFired(SchedulingContext context, Trigger trigge removeWaitingTrigger(t); } } - blockedJobs.add(getJobNameKey(job)); } else if (localTrigger.getNextFireTime() != null) { addWaitingTrigger(localTrigger); } @@ -1409,7 +1404,6 @@ public void triggeredJobComplete(SchedulingContext context, Trigger trigger, newData.clearDirtyFlag(); } jd.setJobDataMap(newData); - blockedJobs.remove(getJobNameKey(jd)); Trigger[] triggers = getTriggersForJob(context, jd.getName(), jd.getGroup()); for (Trigger t : triggers) { TriggerWrapper tmpTw = getTriggerWrapper(t.getGroup(), t.getName()); @@ -1428,8 +1422,6 @@ public void triggeredJobComplete(SchedulingContext context, Trigger trigger, } schedulerSignaler.signalSchedulingChange(0L); } - } else { - blockedJobs.remove(jobKey); } if (tw != null) { diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapper.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapper.java index 7b7a6d6571..64ab3fb0b5 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapper.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapper.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openidm.repo.jdbc.impl; @@ -116,7 +117,7 @@ public JsonValue mapToJsonValue(ResultSet rs, Set columnNames) throws SQ mappedResult.putPermissive(entry.objectColPointer, value); } } - if (columnNames.contains("total") && !columnMappings.contains("total")) { + if (columnNames.contains("total") && !isMappedDbColumn("total")) { mappedResult.putPermissive(pathToTotal, rs.getInt("total")); } logger.debug("Mapped rs {} to {}", rs, mappedResult); @@ -164,6 +165,15 @@ public String getDbColumnName(JsonPointer fieldName) { throw new IllegalArgumentException("Unknown object field: " + fieldName.toString()); } + private boolean isMappedDbColumn(String dbColName) { + for (ColumnMapping mapping : columnMappings) { + if (dbColName.equals(mapping.dbColName)) { + return true; + } + } + return false; + } + /** * @return the columnMappings */ diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java index f847e7e313..510c49a061 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java @@ -598,7 +598,7 @@ public void delete(String fullId, String type, String localId, String rev, Conne logger.debug("Delete statement: {}", deleteStatement); int deletedRows = deleteStatement.executeUpdate(); - logger.trace("Deleted {} rows for id : {} {}", deletedRows, localId); + logger.trace("Deleted {} rows for id : {}", deletedRows, localId); if (deletedRows < 1) { throw new InternalServerErrorException("Deleting object for " + fullId + " failed, DB reported " + deletedRows + " rows deleted"); } else { diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java index c31bffc4ab..902d3fc37c 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java @@ -170,7 +170,7 @@ protected void initializeQueries() { deleteQueryStr = "DELETE FROM " + mainTable + " WHERE objectid = ? AND rev = ?"; logger.debug("Unprepared query strings {} {} {} {} {}", - readQueryStr, createQueryStr, updateQueryStr, deleteQueryStr); + readQueryStr, readForUpdateQueryStr, createQueryStr, updateQueryStr, deleteQueryStr); } @@ -452,7 +452,7 @@ public void delete(String fullId, String type, String localId, String rev, Conne logger.debug("Delete statement: {}", deleteStatement); int deletedRows = deleteStatement.executeUpdate(); - logger.trace("Deleted {} rows for id : {} {}", deletedRows, localId); + logger.trace("Deleted {} rows for id : {}", deletedRows, localId); if (deletedRows < 1) { throw new InternalServerErrorException("Deleting object for " + fullId + " failed, DB reported " + deletedRows + " rows deleted"); diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/PostgreSQLMappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/PostgreSQLMappedTableHandler.java index 54e35c6112..d2e128109a 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/PostgreSQLMappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/PostgreSQLMappedTableHandler.java @@ -12,6 +12,7 @@  * information: "Portions copyright [year] [name of copyright owner]".  *  * Copyright 2015 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC.  */ package org.forgerock.openidm.repo.jdbc.impl; @@ -73,6 +74,6 @@ protected void initializeQueries() { deleteQueryStr = "DELETE FROM " + mainTable + " WHERE objectid = ? AND rev = ?"; logger.debug("Unprepared query strings {} {} {} {} {}", - readQueryStr, createQueryStr, updateQueryStr, deleteQueryStr); + readQueryStr, readForUpdateQueryStr, createQueryStr, updateQueryStr, deleteQueryStr); } } diff --git a/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapperTest.java b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapperTest.java new file mode 100644 index 0000000000..79c9e2d882 --- /dev/null +++ b/openidm-repo-jdbc/src/test/java/org/forgerock/openidm/repo/jdbc/impl/ExplicitResultSetMapperTest.java @@ -0,0 +1,79 @@ +/* + * 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.forgerock.openidm.repo.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.forgerock.json.JsonValue.array; +import static org.forgerock.json.JsonValue.field; +import static org.forgerock.json.JsonValue.json; +import static org.forgerock.json.JsonValue.object; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.ResultSet; +import java.util.Arrays; +import java.util.HashSet; + +import org.forgerock.json.JsonValue; +import org.forgerock.openidm.crypto.CryptoService; +import org.forgerock.openidm.util.Accessor; +import org.testng.annotations.Test; + +/** + * Tests for {@link ExplicitResultSetMapper#mapToJsonValue}. + */ +public class ExplicitResultSetMapperTest { + + /** STRING columns require a crypto service to be reachable, even when nothing is encrypted. */ + private static final Accessor CRYPTO = new Accessor() { + private final CryptoService cryptoService = mock(CryptoService.class); + + @Override + public CryptoService access() { + return cryptoService; + } + }; + + @Test + public void unmappedTotalColumnIsExposedAsRowCount() throws Exception { + ExplicitResultSetMapper mapper = new ExplicitResultSetMapper("t", + json(object(field("_id", "objectid"))), CRYPTO); + ResultSet rs = mock(ResultSet.class); + when(rs.getString("objectid")).thenReturn("1"); + when(rs.getInt("total")).thenReturn(42); + + JsonValue result = mapper.mapToJsonValue(rs, new HashSet<>(Arrays.asList("objectid", "total"))); + + assertThat(result.get("_id").asString()).isEqualTo("1"); + assertThat(result.get("total").asInteger()).isEqualTo(42); + } + + @Test + public void explicitlyMappedTotalColumnIsNotOverwrittenByRowCount() throws Exception { + ExplicitResultSetMapper mapper = new ExplicitResultSetMapper("t", + json(object(field("_id", "objectid"), field("total", array("total", "STRING")))), CRYPTO); + ResultSet rs = mock(ResultSet.class); + when(rs.getString("objectid")).thenReturn("1"); + when(rs.getString("total")).thenReturn("mapped"); + + JsonValue result = mapper.mapToJsonValue(rs, new HashSet<>(Arrays.asList("objectid", "total"))); + + assertThat(result.get("total").asString()).isEqualTo("mapped"); + verify(rs, never()).getInt("total"); + } +} diff --git a/openidm-repo-orientdb/src/main/java/org/forgerock/openidm/repo/orientdb/impl/OrientDBRepoService.java b/openidm-repo-orientdb/src/main/java/org/forgerock/openidm/repo/orientdb/impl/OrientDBRepoService.java index 0f008e4946..16d2b72ce8 100644 --- a/openidm-repo-orientdb/src/main/java/org/forgerock/openidm/repo/orientdb/impl/OrientDBRepoService.java +++ b/openidm-repo-orientdb/src/main/java/org/forgerock/openidm/repo/orientdb/impl/OrientDBRepoService.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2011-2016 ForgeRock AS. - * Portions Copyrighted 2024 3A Systems LLC. + * Portions Copyrighted 2024-2026 3A Systems LLC. * Portions copyright 2026 3A Systems, LLC. */ package org.forgerock.openidm.repo.orientdb.impl; @@ -679,30 +679,32 @@ ODatabaseDocumentTx getConnection() throws InternalServerErrorException { int maxRetry = 100; // give it up to approx 10 seconds to recover int retryCount = 0; - synchronized (dbLock) { - while (db == null && retryCount < maxRetry) { - retryCount++; - try { + while (db == null && retryCount < maxRetry) { + retryCount++; + try { + synchronized (dbLock) { db = pool.acquire(dbURL, user, password); - if (retryCount > 1) { - logger.info("Succeeded in acquiring connection from pool in retry attempt {}", retryCount); - } - retryCount = maxRetry; - } catch (com.orientechnologies.common.concur.lock.OLockException ex) { - // TODO: remove work-around once OrientDB resolves this condition - if (retryCount == maxRetry) { - logger.warn("Failure reported acquiring connection from pool, retried {} times before giving up.", retryCount, ex); - throw new InternalServerErrorException( - "Failure reported acquiring connection from pool, retried " + retryCount + " times before giving up: " - + ex.getMessage(), ex); - } else { - logger.info("Pool acquire reported failure, retrying - attempt {}", retryCount); - logger.trace("Pool acquire failure detail ", ex); - try { - Thread.sleep(100); // Give the DB time to complete what it's doing before retrying - } catch (InterruptedException iex) { - // ignore that sleep was interrupted - } + } + if (retryCount > 1) { + logger.info("Succeeded in acquiring connection from pool in retry attempt {}", retryCount); + } + retryCount = maxRetry; + } catch (com.orientechnologies.common.concur.lock.OLockException ex) { + // TODO: remove work-around once OrientDB resolves this condition + if (retryCount == maxRetry) { + logger.warn("Failure reported acquiring connection from pool, retried {} times before giving up.", retryCount, ex); + throw new InternalServerErrorException( + "Failure reported acquiring connection from pool, retried " + retryCount + " times before giving up: " + + ex.getMessage(), ex); + } else { + logger.info("Pool acquire reported failure, retrying - attempt {}", retryCount); + logger.trace("Pool acquire failure detail ", ex); + // Back off outside the lock so other callers (and re-init) are not held up while we wait + try { + Thread.sleep(100); // Give the DB time to complete what it's doing before retrying + } catch (InterruptedException iex) { + Thread.currentThread().interrupt(); + break; } } } diff --git a/openidm-script/src/main/java/org/forgerock/openidm/script/AbstractScriptedService.java b/openidm-script/src/main/java/org/forgerock/openidm/script/AbstractScriptedService.java index aa5c7d9c78..8f095a23aa 100644 --- a/openidm-script/src/main/java/org/forgerock/openidm/script/AbstractScriptedService.java +++ b/openidm-script/src/main/java/org/forgerock/openidm/script/AbstractScriptedService.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2013-2016 ForgeRock AS. - * Portions Copyrighted 2024 3A Systems LLC. + * Portions Copyrighted 2024-2026 3A Systems LLC. */ package org.forgerock.openidm.script; @@ -67,7 +67,10 @@ public abstract class AbstractScriptedService implements ScriptCustomizer, Scrip /** Script Registry service. */ private ScriptedRequestHandler embeddedHandler = null; - private ServiceRegistration selfRegistration = null; + private volatile ServiceRegistration selfRegistration = null; + + /** Guards {@link #selfRegistration}; the field itself may be null, so it cannot serve as the lock. */ + private final Object registrationLock = new Object(); private Dictionary properties = null; @@ -160,25 +163,22 @@ protected void unregisterService() { public void scriptChanged(ScriptEvent event) throws ScriptException { if (ScriptEvent.REGISTERED == event.getType()) { - if (null == selfRegistration) { - synchronized (selfRegistration) { - if (null == selfRegistration) { - final ScriptEntry scriptEntry = event.getScriptLibraryEntry(); - scriptEntry.setBindings(bindings); - selfRegistration = - getBundleContext().registerService( - RequestHandler.class, - new ScriptedRequestHandler(scriptEntry, - getScriptCustomizer()), getProperties()); - } + synchronized (registrationLock) { + if (null == selfRegistration) { + final ScriptEntry scriptEntry = event.getScriptLibraryEntry(); + scriptEntry.setBindings(bindings); + selfRegistration = + getBundleContext().registerService( + RequestHandler.class, + new ScriptedRequestHandler(scriptEntry, + getScriptCustomizer()), getProperties()); } } } else if (ScriptEvent.UNREGISTERING == event.getType()) { - if (null != selfRegistration) { - synchronized (selfRegistration) { - if (null != selfRegistration) { - selfRegistration.unregister(); - } + synchronized (registrationLock) { + if (null != selfRegistration) { + selfRegistration.unregister(); + selfRegistration = null; } } } else if (ScriptEvent.MODIFIED == event.getType()) { diff --git a/openidm-script/src/test/java/org/forgerock/openidm/script/AbstractScriptedServiceTest.java b/openidm-script/src/test/java/org/forgerock/openidm/script/AbstractScriptedServiceTest.java new file mode 100644 index 0000000000..5475d8a1c8 --- /dev/null +++ b/openidm-script/src/test/java/org/forgerock/openidm/script/AbstractScriptedServiceTest.java @@ -0,0 +1,101 @@ +/* + * 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.forgerock.openidm.script; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Dictionary; + +import org.forgerock.json.resource.RequestHandler; +import org.forgerock.script.ScriptEntry; +import org.forgerock.script.ScriptEvent; +import org.forgerock.script.ScriptRegistry; +import org.mockito.Matchers; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Tests for the script-listener side of {@link AbstractScriptedService}: registering the request + * handler when the script appears and unregistering it when the script goes away. + */ +public class AbstractScriptedServiceTest { + + private static class TestService extends AbstractScriptedService { + private final BundleContext bundleContext = mock(BundleContext.class); + + @Override + protected BundleContext getBundleContext() { + return bundleContext; + } + + @Override + protected ScriptRegistry getScriptRegistry() { + return mock(ScriptRegistry.class); + } + } + + private TestService service; + private ServiceRegistration registration; + + @BeforeMethod + @SuppressWarnings("unchecked") + public void setUp() { + service = new TestService(); + registration = mock(ServiceRegistration.class); + when(service.bundleContext.registerService(eq(RequestHandler.class), any(RequestHandler.class), + Matchers.>any())).thenReturn(registration); + } + + private static ScriptEvent event(int type) throws Exception { + ScriptEvent event = mock(ScriptEvent.class); + when(event.getType()).thenReturn(type); + when(event.getScriptLibraryEntry()).thenReturn(mock(ScriptEntry.class)); + return event; + } + + @Test + public void registeredEventRegistersTheHandlerExactlyOnce() throws Exception { + service.scriptChanged(event(ScriptEvent.REGISTERED)); + service.scriptChanged(event(ScriptEvent.REGISTERED)); + + verify(service.bundleContext, times(1)).registerService(eq(RequestHandler.class), + any(RequestHandler.class), Matchers.>any()); + } + + @Test + public void unregisteringEventUnregistersTheHandler() throws Exception { + service.scriptChanged(event(ScriptEvent.REGISTERED)); + + service.scriptChanged(event(ScriptEvent.UNREGISTERING)); + + verify(registration).unregister(); + } + + @Test + public void unregisteringEventBeforeRegistrationIsIgnored() throws Exception { + service.scriptChanged(event(ScriptEvent.UNREGISTERING)); + + verify(service.bundleContext, times(0)).registerService(eq(RequestHandler.class), + any(RequestHandler.class), Matchers.>any()); + } +} diff --git a/openidm-workflow-activiti/src/main/java/org/forgerock/openidm/workflow/activiti/impl/JsonUser.java b/openidm-workflow-activiti/src/main/java/org/forgerock/openidm/workflow/activiti/impl/JsonUser.java index c8e2629153..625337fab6 100644 --- a/openidm-workflow-activiti/src/main/java/org/forgerock/openidm/workflow/activiti/impl/JsonUser.java +++ b/openidm-workflow-activiti/src/main/java/org/forgerock/openidm/workflow/activiti/impl/JsonUser.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2012-2015 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openidm.workflow.activiti.impl; @@ -50,6 +51,17 @@ public JsonUser(JsonValue value) { super(value); } + /** + * {@link JsonValue#clone()} returns a plain {@code JsonValue}; keep the user type and the + * crypto service so the copy can still decrypt its password. + */ + @Override + public JsonUser clone() { + JsonUser copy = new JsonUser(super.clone()); + copy.cryptoService = cryptoService; + return copy; + } + public String getId() { return get(SCIM_USERNAME).required().asString(); } diff --git a/openidm-workflow-activiti/src/test/java/org/forgerock/openidm/workflow/activiti/impl/JsonUserTest.java b/openidm-workflow-activiti/src/test/java/org/forgerock/openidm/workflow/activiti/impl/JsonUserTest.java new file mode 100644 index 0000000000..2d32b3700a --- /dev/null +++ b/openidm-workflow-activiti/src/test/java/org/forgerock/openidm/workflow/activiti/impl/JsonUserTest.java @@ -0,0 +1,62 @@ +/* + * 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.forgerock.openidm.workflow.activiti.impl; + +import static org.forgerock.json.JsonValue.json; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; + +import org.forgerock.json.JsonValue; +import org.forgerock.openidm.crypto.CryptoService; +import org.testng.annotations.Test; + +/** + * Tests for {@link JsonUser#clone()}. + */ +public class JsonUserTest { + + @Test + public void cloneKeepsTypeStateAndCryptoService() throws Exception { + CryptoService cryptoService = mock(CryptoService.class); + when(cryptoService.decrypt(any(JsonValue.class))).thenReturn(json("secret")); + JsonUser user = new JsonUser(cryptoService, "bjensen"); + user.setFirstName("Barbara"); + user.setPassword("encrypted"); + + JsonUser copy = user.clone(); + + assertNotSame(copy, user); + assertEquals(copy.getId(), "bjensen"); + assertEquals(copy.getFirstName(), "Barbara"); + assertEquals(copy.getPassword(), "secret", "the crypto service must be carried over"); + } + + @Test + public void cloneIsIndependentAtTheTopLevel() { + JsonUser user = new JsonUser(mock(CryptoService.class), "bjensen"); + + JsonUser copy = user.clone(); + copy.setId("other"); + copy.remove(SharedIdentityService.SCIM_USERNAME); + + assertEquals(user.getId(), "bjensen"); + assertNull(copy.get(SharedIdentityService.SCIM_USERNAME).getObject()); + } +}