/*
* Pentaho Data Integration
*
* Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com
*
* **************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pentaho.di.core.extension;
import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtNewMethod;
import javassist.NotFoundException;
import org.junit.BeforeClass;
import org.junit.Test;
import org.pentaho.di.core.KettleClientEnvironment;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.exception.KettlePluginException;
import org.pentaho.di.core.logging.LogChannelInterface;
import org.pentaho.di.core.plugins.PluginInterface;
import org.pentaho.di.core.plugins.PluginRegistry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
public class ExtensionPointIntegrationTest {
public static final String EXECUTED_FIELD_NAME = "executed";
private static final int TOTAL_THREADS_TO_RUN = 2000;
private static final int MAX_TIMEOUT_SECONDS = 60;
private static ClassPool pool;
@BeforeClass
public static void setupBeforeClass() throws Exception {
pool = ClassPool.getDefault();
pool.insertClassPath( new ClassClassPath( ExtensionPointIntegrationTest.class ) );
for ( KettleExtensionPoint ep : KettleExtensionPoint.values() ) {
ExtensionPointPluginType.getInstance().registerCustom( createClassRuntime( ep ),
"custom", "id" + ep.id, ep.id, "no description", null );
}
KettleClientEnvironment.init();
}
@Test
public void test() throws Exception {
// check that all extension points are added to the map
assertEquals( KettleExtensionPoint.values().length, ExtensionPointMap.getInstance().getNumberOfRows() );
// check that all extension points are executed
final LogChannelInterface log = mock( LogChannelInterface.class );
for ( KettleExtensionPoint ep : KettleExtensionPoint.values() ) {
final ExtensionPointInterface currentEP = ExtensionPointMap.getInstance().getTableValue( ep.id, "id" + ep.id );
assertFalse( currentEP.getClass().getField( EXECUTED_FIELD_NAME ).getBoolean( currentEP ) );
ExtensionPointHandler.callExtensionPoint( log, ep.id, null );
assertTrue( currentEP.getClass().getField( EXECUTED_FIELD_NAME ).getBoolean( currentEP ) );
}
// check modification of extension point
final KettleExtensionPoint jobAfterOpen = KettleExtensionPoint.JobAfterOpen;
final ExtensionPointInterface int1 = ExtensionPointMap.getInstance().getTableValue( jobAfterOpen.id, "id" + jobAfterOpen.id );
ExtensionPointPluginType.getInstance().registerCustom( createClassRuntime( jobAfterOpen, "Edited" ), "custom", "id"
+ jobAfterOpen.id, jobAfterOpen.id,
"no description", null );
assertNotSame( int1, ExtensionPointMap.getInstance().getTableValue( jobAfterOpen.id, "id" + jobAfterOpen.id ) );
assertEquals( KettleExtensionPoint.values().length, ExtensionPointMap.getInstance().getNumberOfRows() );
// check removal of extension point
PluginRegistry.getInstance().removePlugin( ExtensionPointPluginType.class, PluginRegistry.getInstance().getPlugin(
ExtensionPointPluginType.class, "id" + jobAfterOpen.id ) );
assertTrue( ExtensionPointMap.getInstance().getTableValue( jobAfterOpen.id, "id" + jobAfterOpen.id ) == null );
assertEquals( KettleExtensionPoint.values().length - 1, ExtensionPointMap.getInstance().getNumberOfRows() );
}
private static Class createClassRuntime( KettleExtensionPoint ep ) throws NotFoundException, CannotCompileException {
return createClassRuntime( ep, "" );
}
/**
* Create ExtensionPointInterface subclass in runtime
*
* @param ep extension point id
* @param addition addition to class name to avoid duplicate classes
* @return class
* @throws NotFoundException
* @throws CannotCompileException
*/
private static Class createClassRuntime( KettleExtensionPoint ep, String addition )
throws NotFoundException, CannotCompileException {
final CtClass ctClass = pool.makeClass( "Plugin" + ep.id + addition );
ctClass.addInterface( pool.get( ExtensionPointInterface.class.getCanonicalName() ) );
ctClass.addField( CtField.make( "public boolean " + EXECUTED_FIELD_NAME + ";", ctClass ) );
ctClass.addMethod( CtNewMethod.make(
"public void callExtensionPoint( org.pentaho.di.core.logging.LogChannelInterface log, Object object ) "
+ "throws org.pentaho.di.core.exception.KettleException { " + EXECUTED_FIELD_NAME + " = true; }",
ctClass ) );
return ctClass.toClass();
}
@Test
public void testExtensionPointMapConcurrency() throws InterruptedException {
final LogChannelInterface log = mock( LogChannelInterface.class );
List<Runnable> parallelTasksList = new ArrayList<>( TOTAL_THREADS_TO_RUN );
for ( int i = 0; i < TOTAL_THREADS_TO_RUN; i++ ) {
parallelTasksList.add( () -> {
KettleExtensionPoint kettleExtensionPoint = getRandomKettleExtensionPoint();
PluginInterface pluginInterface = PluginRegistry.getInstance().getPlugin( ExtensionPointPluginType.class,
"id" + kettleExtensionPoint.id );
try {
PluginRegistry.getInstance().removePlugin( ExtensionPointPluginType.class, pluginInterface );
PluginRegistry.getInstance().registerPlugin( ExtensionPointPluginType.class, pluginInterface );
} catch ( KettlePluginException e ) {
e.printStackTrace();
} catch ( NullPointerException e ) {
//NullPointerException can be thrown if trying to remove a plugin that doesn't exit, discarding occurence
}
ExtensionPointMap.getInstance().reInitialize();
try {
ExtensionPointMap.getInstance().callExtensionPoint( log, kettleExtensionPoint.id, null );
} catch ( KettleException e ) {
e.printStackTrace();
}
} );
}
assertConcurrent( parallelTasksList );
}
private static KettleExtensionPoint getRandomKettleExtensionPoint() {
KettleExtensionPoint[] kettleExtensionPoints = KettleExtensionPoint.values();
int randomInd = ThreadLocalRandom.current().nextInt( 0, kettleExtensionPoints.length );
return kettleExtensionPoints[randomInd];
}
private static void assertConcurrent( final List<? extends Runnable> runnables ) throws InterruptedException {
final int numThreads = runnables.size();
final List<Throwable> exceptions = Collections.synchronizedList( new ArrayList<>() );
final ExecutorService threadPool = Executors.newFixedThreadPool( numThreads );
try {
final CountDownLatch allExecutorThreadsReady = new CountDownLatch( numThreads );
final CountDownLatch afterInitBlocker = new CountDownLatch( 1 );
final CountDownLatch allDone = new CountDownLatch( numThreads );
for ( final Runnable submittedTestRunnable : runnables ) {
threadPool.submit( () -> {
allExecutorThreadsReady.countDown();
try {
afterInitBlocker.await();
submittedTestRunnable.run();
} catch ( final Throwable e ) {
exceptions.add( e );
} finally {
allDone.countDown();
}
} );
}
// wait until all threads are ready
assertTrue(
"Timeout initializing threads! Perform long lasting initializations before passing runnables to assertConcurrent",
allExecutorThreadsReady.await( 10L * runnables.size(), TimeUnit.MILLISECONDS ) );
// start all test runners
afterInitBlocker.countDown();
assertTrue( String.format( "Timeout! Run took more than %s seconds", MAX_TIMEOUT_SECONDS ),
allDone.await( MAX_TIMEOUT_SECONDS, TimeUnit.SECONDS ) );
} finally {
threadPool.shutdownNow();
}
assertTrue( String.format( " Run failed with exception(s): %s", exceptions ), exceptions.isEmpty() );
}
}