Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -822,8 +822,6 @@ public static void setSQLParam(final PreparedStatement stmt, final int idx, SQLP
stmt.setLong(idx, ((BigInteger) val).longValue());
} else if (val instanceof Byte) {
stmt.setByte(idx, (Byte) val);
} else if (val instanceof Integer) {
stmt.setInt(idx, (Integer) val);
} else if (val instanceof InputStream) {
stmt.setBinaryStream(idx, (InputStream) val, 10000);
} else if (val instanceof Blob) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Portions Copyright 2026 3A Systems, LLC.
*/

package org.forgerock.openicf.connectors.scriptedcrest
Expand Down Expand Up @@ -198,7 +200,6 @@ class ScriptedCRESTConfiguration extends ScriptedConfiguration {
}

private Closure init = null;
private Closure release = null;
private Closure beforeRequest = null;
private Closure onComplete = null;
private Closure onFail = null;
Expand All @@ -223,7 +224,7 @@ class ScriptedCRESTConfiguration extends ScriptedConfiguration {

customizerClass.metaClass.customize << { Closure cl ->
init = null
release = null
setReleaseClosure(null)
beforeRequest = null
onComplete = null
onFail = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,15 @@ protected Log getLogger(final Class<?> clazz) {
return logger;
}

/** The engine, published only once its customizer has run. Guarded by {@code this}. */
private GroovyScriptEngine groovyScriptEngine = null;

/**
* The engine whose customizer is running, for the calls the customizer makes back into
* {@link #getGroovyScriptEngine()} on the same thread. Guarded by {@code this}.
*/
private GroovyScriptEngine customizingGroovyScriptEngine = null;

/**
* Synchronised for the whole initialisation, not double-checked: the
* customizer script runs against the half-initialised engine (it may call
Expand All @@ -792,35 +799,54 @@ protected Log getLogger(final Class<?> clazz) {
*/
protected synchronized GroovyScriptEngine getGroovyScriptEngine() {
if (null == groovyScriptEngine) {
if (null != customizingGroovyScriptEngine) {
return customizingGroovyScriptEngine;
}

final CompilerConfiguration compilerConfiguration =
new CompilerConfiguration(config);
compilerConfiguration.addCompilationCustomizers(getImportCustomizer(null));

final GroovyClassLoader loader =
new GroovyClassLoader(getParentLoader(), compilerConfiguration, true);

groovyScriptEngine =
final GroovyScriptEngine engine =
new GroovyScriptEngine(getRoots(compilerConfiguration, loader), loader);

initializeCustomizer();
// If the customizer fails, the engine is dropped and the next call retries.
customizingGroovyScriptEngine = engine;
try {
initializeCustomizer();
} finally {
customizingGroovyScriptEngine = null;
}
groovyScriptEngine = engine;
}
return groovyScriptEngine;
}

/*
* This must be called once from thread-safe location and inside the
* synchronized to avoid deadlock.
* synchronized to avoid deadlock. The customizer runs while this
* configuration's monitor is held: it must not wait for another thread
* that calls getGroovyScriptEngine(), evaluate() or loadScript().
*/
private void initializeCustomizer() {
Class customizerClass = null;
try {
Class customizerClass = getCustomizerClass();
customizerClass = getCustomizerClass();

if (null != customizerClass) {
Binding binding = new Binding();
binding.setVariable(LOGGER, getLogger(customizerClass));
createCustomizerScript(customizerClass, binding).run();
}
} catch (Throwable t) {
if (null != customizerClass) {
// The retry compiles a new class; unregister this one, together with any
// metaclass createCustomizerScript() put on it, so its loader can be collected.
InvokerHelper.removeClass(customizerClass);
}
logger.error(t, "Failed to customize the connector");
throw ConnectorException.wrap(t);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* 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.openicf.misc.scriptedcommon;

import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.codehaus.groovy.reflection.ClassInfo;
import org.forgerock.openicf.connectors.scriptedcrest.ScriptedCRESTConfiguration;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import groovy.lang.Binding;
import groovy.lang.ExpandoMetaClass;
import groovy.lang.GroovySystem;
import groovy.lang.Script;
import groovy.util.GroovyScriptEngine;

/**
* Tests that {@link ScriptedConfiguration#getGroovyScriptEngine()} only hands out an engine whose
* customizer script has run.
*/
public class ScriptedConfigurationTest {

/** What the customizer script does; set by each test. */
private static volatile Runnable customizer;

/** Called from the customizer script written by {@link #setUp()}. */
public static void customize() {
customizer.run();
}

/** How often {@link #isFirstAttempt()} was called; reset by {@link #setUp()}. */
private static final AtomicInteger attempts = new AtomicInteger();

/** Whether the release closure of a customizer script ran; reset by {@link #setUp()}. */
private static volatile boolean releaseClosureRan;

/** Called from a customizer script to behave differently on its first attempt. */
public static boolean isFirstAttempt() {
return attempts.incrementAndGet() == 1;
}

/** Called from the release closure of a customizer script. */
public static void releaseClosureRan() {
releaseClosureRan = true;
}

private File scriptRoot;

private ScriptedConfiguration configuration;

private ExecutorService executor;

@BeforeMethod
public void setUp() throws Exception {
attempts.set(0);
releaseClosureRan = false;
scriptRoot = Files.createTempDirectory("scripted-configuration").toFile();
Files.write(new File(scriptRoot, "Customizer.groovy").toPath(),
(ScriptedConfigurationTest.class.getName() + ".customize()\n")
.getBytes(StandardCharsets.UTF_8));
configuration = new ScriptedConfiguration();
configuration.setScriptRoots(new String[] { scriptRoot.getAbsolutePath() });
configuration.setCustomizerScriptFileName("Customizer.groovy");
executor = Executors.newCachedThreadPool();
}

@AfterMethod
public void tearDown() throws Exception {
customizer = null;
executor.shutdownNow();
new File(scriptRoot, "Customizer.groovy").delete();
scriptRoot.delete();
}

@Test
public void testFailedCustomizerIsRetriedOnTheNextCall() {
final AtomicInteger calls = new AtomicInteger();
customizer = () -> {
if (calls.incrementAndGet() == 1) {
throw new IllegalStateException("customizer failed");
}
};

try {
configuration.getGroovyScriptEngine();
fail("The customizer failure must reach the caller");
} catch (IllegalStateException e) {
assertThat(e).hasMessage("customizer failed");
}

assertThat(configuration.getGroovyScriptEngine()).isNotNull();
assertThat(calls.get()).isEqualTo(2);
}

@Test
public void testFailedCustomizerClassIsUnregistered() {
customizer = () -> {
throw new IllegalStateException("customizer failed");
};
final AtomicReference<Class> customizerClass = new AtomicReference<>();
// Registers a metaclass on the customizer class, as the REST, CREST and SSH
// configurations do; a registered metaclass keeps the class's loader alive.
configuration = new ScriptedConfiguration() {
@Override
protected Script createCustomizerScript(Class clazz, Binding binding) {
customizerClass.set(clazz);
ExpandoMetaClass metaClass = new ExpandoMetaClass(clazz, false, true);
metaClass.initialize();
GroovySystem.getMetaClassRegistry().setMetaClass(clazz, metaClass);
return super.createCustomizerScript(clazz, binding);
}
};
configuration.setScriptRoots(new String[] { scriptRoot.getAbsolutePath() });
configuration.setCustomizerScriptFileName("Customizer.groovy");

try {
configuration.getGroovyScriptEngine();
fail("The customizer failure must reach the caller");
} catch (IllegalStateException e) {
assertThat(e).hasMessage("customizer failed");
}

assertThat(customizerClass.get()).isNotNull();
assertThat(ClassInfo.getClassInfo(customizerClass.get()).getStrongMetaClass()).isNull();
}

@Test
public void testRetriedCRESTCustomizerDropsTheReleaseClosureOfTheFailedAttempt() throws Exception {
final String test = ScriptedConfigurationTest.class.getName();
Files.write(new File(scriptRoot, "Customizer.groovy").toPath(),
("if (" + test + ".isFirstAttempt()) {\n"
+ " customize {\n"
+ " release { " + test + ".releaseClosureRan() }\n"
+ " }\n"
+ " throw new IllegalStateException('customizer failed')\n"
+ "}\n"
+ "customize {\n"
+ "}\n").getBytes(StandardCharsets.UTF_8));
configuration = new ScriptedCRESTConfiguration();
configuration.setScriptRoots(new String[] { scriptRoot.getAbsolutePath() });
configuration.setCustomizerScriptFileName("Customizer.groovy");

try {
configuration.getGroovyScriptEngine();
fail("The customizer failure must reach the caller");
} catch (IllegalStateException e) {
assertThat(e).hasMessage("customizer failed");
}
assertThat(configuration.getGroovyScriptEngine()).isNotNull();

configuration.release();
assertThat(releaseClosureRan).as("release closure of the failed attempt ran").isFalse();
}

@Test(timeOut = 30000)
public void testEngineIsHiddenFromOtherThreadsUntilCustomized() throws Exception {
final CountDownLatch customizing = new CountDownLatch(1);
final CountDownLatch finishCustomizing = new CountDownLatch(1);
customizer = () -> {
customizing.countDown();
try {
finishCustomizing.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};

Future<GroovyScriptEngine> first = executor.submit(configuration::getGroovyScriptEngine);
customizing.await();
final AtomicReference<Thread> secondThread = new AtomicReference<>();
Future<GroovyScriptEngine> second = executor.submit(() -> {
secondThread.set(Thread.currentThread());
return configuration.getGroovyScriptEngine();
});
// The second caller either returns early or blocks on the configuration's monitor.
while (!second.isDone() && (secondThread.get() == null
|| secondThread.get().getState() != Thread.State.BLOCKED)) {
Thread.sleep(10);
}
assertThat(second.isDone()).as("second caller returned while the customizer ran").isFalse();

finishCustomizing.countDown();
assertThat(first.get()).isNotNull();
assertThat(second.get()).isSameAs(first.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,20 @@
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* Portions Copyrighted 2026 3A Systems, LLC
*/

package org.identityconnectors.contract.test;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.identityconnectors.common.StringUtil;
import org.identityconnectors.contract.data.DataProvider;
import org.identityconnectors.framework.api.ConnectorFacade;
import org.identityconnectors.framework.common.objects.Schema;
import org.testng.IObjectFactory;
import org.testng.ITestContext;
import org.testng.annotations.Factory;
import org.testng.internal.ObjectFactoryImpl;

import com.google.inject.Guice;
import com.google.inject.Injector;
Expand Down Expand Up @@ -70,17 +66,16 @@ public Object[] createInstances(ITestContext context) {

Injector injector = getInjector(context);
List<Object> result = new ArrayList<Object>();
IObjectFactory objectFactory = null;

for (Class<?> testClass: getContractTestClasses(context)) {
Constructor constructor = null;
try {
constructor = testClass.getConstructor(String.class);
Object test = objectFactory.newInstance(constructor, "");
Object test = testClass.getConstructor(String.class).newInstance("");
injector.injectMembers(test);
result.add(test);
} catch (NoSuchMethodException e) {
result.add(injector.getInstance(testClass));
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Cannot instantiate " + testClass.getName(), e);
}
}
return result.toArray();
Expand Down Expand Up @@ -111,14 +106,4 @@ public Injector getInjector(ITestContext context) {
public DataProvider getDataProvider(ITestContext context) {
return ConnectorHelper.createDataProvider();
}

private static class ContractTestFactory {

private ConnectorFacade connectorFacade = null;

private Schema schema = null;

private IObjectFactory objectFactory = new ObjectFactoryImpl();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public class WebSocketConnectionGroup

private Encryptor encryptor = null;

private RemoteOperationContext operationContext = null;
private volatile RemoteOperationContext operationContext = null;

private final AtomicBoolean isRunning = new AtomicBoolean(Boolean.TRUE);
private final Set<String> principals = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
Expand Down
Loading
Loading