/** * Copyright 2011-2017 Asakusa Framework Team. * * 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 com.asakusafw.testdriver.windgate; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Properties; import java.util.WeakHashMap; import org.apache.hadoop.conf.Configurable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.asakusafw.runtime.stage.launcher.ApplicationLauncher; import com.asakusafw.testdriver.core.TestContext; import com.asakusafw.testdriver.hadoop.ConfigurationFactory; import com.asakusafw.vocabulary.windgate.WindGateExporterDescription; import com.asakusafw.vocabulary.windgate.WindGateImporterDescription; import com.asakusafw.vocabulary.windgate.WindGateProcessDescription; import com.asakusafw.windgate.core.DriverScript; import com.asakusafw.windgate.core.GateProfile; import com.asakusafw.windgate.core.ParameterList; import com.asakusafw.windgate.core.ProcessScript; import com.asakusafw.windgate.core.ProfileContext; import com.asakusafw.windgate.core.resource.ResourceManipulator; import com.asakusafw.windgate.core.resource.ResourceMirror; import com.asakusafw.windgate.core.resource.ResourceProfile; import com.asakusafw.windgate.core.resource.ResourceProvider; import com.asakusafw.windgate.file.resource.Preparable; /** * Utilities for this package. * @since 0.2.2 * @version 0.7.2 */ public final class WindGateTestHelper { static final Logger LOG = LoggerFactory.getLogger(WindGateTestHelper.class); /** * The environment variable name / parameter name in profile context for the framework home path. * @since 0.7.2 */ public static final String ENV_FRAMEWORK_HOME = "ASAKUSA_HOME"; //$NON-NLS-1$ /** * For testing, WindGate profile path pattern in form of {@link MessageFormat}. * <code>{0}</code> will be replaced as the its profile name. * This module will load these files from the class path. */ public static final String TESTING_PROFILE_PATH = "windgate-{0}.properties"; //$NON-NLS-1$ /** * WindGate plugin directory path from Asakusa installation path. */ public static final String PRODUCTION_PLUGIN_DIRECTORY = "windgate/plugin"; //$NON-NLS-1$ /** * For normal use, WindGate profile path pattern in form of {@link MessageFormat}. * <code>{0}</code> will be replaced as the its profile name. * This module will load these files in {@code ASAKUSA_HOME} * if there are not in {@link #TESTING_PROFILE_PATH}. * @see #ENV_FRAMEWORK_HOME */ public static final String PRODUCTION_PROFILE_PATH = "windgate/profile/{0}.properties"; //$NON-NLS-1$ private static final String PLUGIN_EXTENSION = ".jar"; //$NON-NLS-1$ private static final String DUMMY_RESOURCE_NAME = "__DUMMY__"; //$NON-NLS-1$ private static final String DUMMY_PROCESS_NAME = "test-moderator"; //$NON-NLS-1$ private static final WeakHashMap<TestContext, Holder> CACHE_TEMPORARY_LOADER = new WeakHashMap<>(); /** * Creates a new {@link ProcessScript} for testing. * The description is used for a source driver, and a dummy driver is set as its drain. * @param <T> the type of target data model * @param modelType the type of target data model * @param description target description * @return the created script * @throws IllegalArgumentException if some parameters were {@code null} */ public static <T> ProcessScript<T> createProcessScript( Class<T> modelType, WindGateImporterDescription description) { if (modelType == null) { throw new IllegalArgumentException("modelType must not be null"); //$NON-NLS-1$ } if (description == null) { throw new IllegalArgumentException("description must not be null"); //$NON-NLS-1$ } Holder.clean(); LOG.debug("Create process script: {}", description.getClass().getName()); //$NON-NLS-1$ return new ProcessScript<>( DUMMY_PROCESS_NAME, DUMMY_PROCESS_NAME, modelType, description.getDriverScript(), createDummyDriverScript()); } /** * Creates a new {@link ProcessScript} for testing. * The description is used for a drain driver, and a dummy driver is set as its source. * @param <T> the type of target data model * @param modelType the type of target data model * @param description target description * @return the created script * @throws IllegalArgumentException if some parameters were {@code null} */ public static <T> ProcessScript<T> createProcessScript( Class<T> modelType, WindGateExporterDescription description) { if (modelType == null) { throw new IllegalArgumentException("modelType must not be null"); //$NON-NLS-1$ } if (description == null) { throw new IllegalArgumentException("description must not be null"); //$NON-NLS-1$ } Holder.clean(); LOG.debug("Create process script: {}", description.getClass().getName()); //$NON-NLS-1$ return new ProcessScript<>( DUMMY_PROCESS_NAME, DUMMY_PROCESS_NAME, modelType, createDummyDriverScript(), description.getDriverScript()); } private static DriverScript createDummyDriverScript() { return new DriverScript( DUMMY_RESOURCE_NAME, Collections.emptyMap()); } /** * Creates a WindGate {@link ProfileContext} for the test context. * @param testContext the current test context * @return the created {@link ProfileContext} * @since 0.7.2 */ public static ProfileContext createProfileContext(TestContext testContext) { if (testContext == null) { throw new IllegalArgumentException("testContext must not be null"); //$NON-NLS-1$ } Holder.clean(); ClassLoader loader = findClassLoader(testContext); return new ProfileContext(loader, new ParameterList(testContext.getEnvironmentVariables())); } /** * Creates a {@link ResourceMirror} for the description. * @param testContext current testing context * @param description the target description * @param arguments the arguments * @return the corresponded {@link ResourceManipulator} * @throws IOException if failed to create a manipulator * @throws IllegalArgumentException if some parameters were {@code null} */ public static ResourceManipulator createResourceManipulator( TestContext testContext, WindGateProcessDescription description, ParameterList arguments) throws IOException { if (testContext == null) { throw new IllegalArgumentException("testContext must not be null"); //$NON-NLS-1$ } if (description == null) { throw new IllegalArgumentException("description must not be null"); //$NON-NLS-1$ } if (arguments == null) { throw new IllegalArgumentException("arguments must not be null"); //$NON-NLS-1$ } Holder.clean(); LOG.debug("Create resource manipulator: {}", description.getClass().getName()); //$NON-NLS-1$ GateProfile profile = loadProfile(testContext, description); String resourceName = description.getDriverScript().getResourceName(); for (ResourceProfile resource : profile.getResources()) { if (resource.getName().equals(resourceName)) { return createManipulator(description, resource, arguments); } } throw new IOException(MessageFormat.format( Messages.getString("WindGateTestHelper.errorFailedToCreateResourceManipulator"), //$NON-NLS-1$ description.getClass().getName(), description.getProfileName(), resourceName)); } private static ResourceManipulator createManipulator( WindGateProcessDescription description, ResourceProfile resource, ParameterList arguments) throws IOException { assert description != null; assert resource != null; assert arguments != null; ResourceProvider provider = resource.createProvider(); ResourceManipulator manipulator = provider.createManipulator(arguments); if (manipulator instanceof Configurable) { LOG.debug("Configuring resource manipulator: {}", manipulator); //$NON-NLS-1$ ConfigurationFactory configuration = ConfigurationFactory.getDefault(); ((Configurable) manipulator).setConf(configuration.newInstance()); } return manipulator; } private static GateProfile loadProfile( TestContext testContext, WindGateProcessDescription description) throws IOException { assert testContext != null; assert description != null; String profileName = description.getProfileName(); LOG.debug("Searching for a WindGate profile: {}", profileName); //$NON-NLS-1$ ProfileContext profileContext = createProfileContext(testContext); URL url = profileContext.getClassLoader().getResource(MessageFormat.format( TESTING_PROFILE_PATH, profileName)); if (url == null) { url = findResourceOnHomePath( testContext, MessageFormat.format( PRODUCTION_PROFILE_PATH, profileName)); } if (url == null) { throw new IOException(MessageFormat.format( Messages.getString("WindGateTestHelper.errorMissingProfile"), //$NON-NLS-1$ description.getClass().getName(), description.getProfileName())); } LOG.debug("Loading a WindGate profile: {}", url); //$NON-NLS-1$ try { Properties p = new Properties(); try (InputStream input = url.openStream()) { p.load(input); } LOG.debug("Resolving a WindGate profile: {}", url); //$NON-NLS-1$ GateProfile profile = GateProfile.loadFrom(profileName, p, profileContext); return profile; } catch (Exception e) { throw new IOException(MessageFormat.format( Messages.getString("WindGateTestHelper.errorFailedToLoadProfile"), //$NON-NLS-1$ description.getClass().getName(), description.getProfileName(), url), e); } } private static URL findResourceOnHomePath(TestContext testContext, String path) { assert testContext != null; assert path != null; File file = findFileOnHomePath(testContext, path); if (file != null && file.isFile() != false) { try { return file.toURI().toURL(); } catch (IOException e) { LOG.warn(MessageFormat.format( Messages.getString("WindGateTestHelper.errorInvalidFilePath"), //$NON-NLS-1$ file), e); return null; } } return null; } private static File findFileOnHomePath(TestContext testContext, String path) { assert testContext != null; assert path != null; String home = testContext.getEnvironmentVariables().get(ENV_FRAMEWORK_HOME); if (home != null) { File file = new File(home, path); if (file.exists()) { return file; } } else { LOG.warn(MessageFormat.format( Messages.getString("WindGateTestHelper.warnUndefinedEnvironmentVariable"), //$NON-NLS-1$ ENV_FRAMEWORK_HOME)); } return null; } private static ClassLoader findClassLoader(TestContext testContext) { assert testContext != null; ClassLoader current = testContext.getClassLoader(); File pluginDirectory = findFileOnHomePath(testContext, PRODUCTION_PLUGIN_DIRECTORY); if (pluginDirectory == null || pluginDirectory.isDirectory() == false) { return current; } synchronized (CACHE_TEMPORARY_LOADER) { Holder holder = CACHE_TEMPORARY_LOADER.get(testContext); if (holder != null) { assert holder.get() != null; assert holder.get() == testContext; if (holder.directory.equals(pluginDirectory)) { return holder.loader; } } List<URL> pluginLibraries = new ArrayList<>(); for (File file : list(pluginDirectory)) { if (file.isFile() && file.getName().endsWith(PLUGIN_EXTENSION)) { try { URL url = file.toURI().toURL(); pluginLibraries.add(url); } catch (Exception e) { LOG.warn(MessageFormat.format( Messages.getString("WindGateTestHelper.errorInvalidPluginPath"), //$NON-NLS-1$ file), e); } } } if (pluginLibraries.isEmpty()) { return current; } PluginClassLoader pluginClassLoader = PluginClassLoader.newInstance(current, pluginLibraries); CACHE_TEMPORARY_LOADER.put(testContext, new Holder(testContext, pluginClassLoader, pluginDirectory)); return pluginClassLoader; } } private static List<File> list(File file) { return Optional.ofNullable(file.listFiles()) .map(Arrays::asList) .orElse(Collections.emptyList()); } /** * Invoke {@link Preparable#prepare() object.prepare()}, or {@link Closeable#close()} is failed. * @param <T> the target object type * @param object target object * @return the object passed to the parameter * @throws IOException if failed to prepare the object * @throws IllegalArgumentException if some parameters were {@code null} */ public static <T extends Preparable & Closeable> T prepare(T object) throws IOException { if (object == null) { throw new IllegalArgumentException("object must not be null"); //$NON-NLS-1$ } Holder.clean(); LOG.debug("Preparing object: {}", object); //$NON-NLS-1$ boolean succeed = false; try { object.prepare(); succeed = true; return object; } finally { if (succeed == false) { LOG.warn(MessageFormat.format( Messages.getString("WindGateTestHelper.errorFailedToPrepareObject"), //$NON-NLS-1$ object)); try { object.close(); } catch (IOException e) { LOG.warn(Messages.getString("WindGateTestHelper.errorFailedToCloseObject"), e); //$NON-NLS-1$ } } } } /** * Disposes WindGate plug-in class loader. * @param loader the target plug-in class loader * @since 0.7.2 */ public static void disposePluginClassLoader(PluginClassLoader loader) { if (loader == null) { throw new IllegalArgumentException("loader must not be null"); //$NON-NLS-1$ } disposePluginClassLoader0(loader); Holder.clean(); } static void disposePluginClassLoader0(PluginClassLoader loader) { assert loader != null; JdbcDriverCleaner.runIn(loader); ApplicationLauncher.disposeClassLoader(loader); } private static final class Holder extends WeakReference<TestContext> { private static final ReferenceQueue<TestContext> QUEUE = new ReferenceQueue<>(); final PluginClassLoader loader; final File directory; Holder(TestContext referent, PluginClassLoader loader, File directory) { super(referent, QUEUE); this.loader = loader; this.directory = directory; } public static void clean() { while (true) { Holder next = (Holder) QUEUE.poll(); if (next == null) { break; } disposePluginClassLoader0(next.loader); } } } private WindGateTestHelper() { return; } }