| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.openejb.resource.jdbc; |
| |
| import org.apache.openejb.cipher.PasswordCipherException; |
| import org.apache.openejb.cipher.PasswordCipherFactory; |
| import org.apache.openejb.loader.IO; |
| import org.apache.openejb.loader.SystemInstance; |
| import org.apache.openejb.resource.XAResourceWrapper; |
| import org.apache.openejb.resource.jdbc.dbcp.DbcpDataSourceCreator; |
| import org.apache.openejb.resource.jdbc.driver.AlternativeDriver; |
| import org.apache.openejb.resource.jdbc.logging.LoggingSqlDataSource; |
| import org.apache.openejb.resource.jdbc.plugin.AbstractDataSourcePlugin; |
| import org.apache.openejb.resource.jdbc.plugin.DataSourcePlugin; |
| import org.apache.openejb.resource.jdbc.pool.DataSourceCreator; |
| import org.apache.openejb.resource.jdbc.pool.DefaultDataSourceCreator; |
| import org.apache.openejb.util.Duration; |
| import org.apache.openejb.util.LogCategory; |
| import org.apache.openejb.util.Logger; |
| import org.apache.openejb.util.SuperProperties; |
| import org.apache.xbean.recipe.ExecutionContext; |
| import org.apache.xbean.recipe.ObjectRecipe; |
| import org.apache.xbean.recipe.Option; |
| import org.apache.xbean.recipe.Recipe; |
| |
| import javax.sql.CommonDataSource; |
| import javax.sql.ConnectionPoolDataSource; |
| import javax.sql.DataSource; |
| import javax.sql.XADataSource; |
| import java.io.Flushable; |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.lang.reflect.InvocationHandler; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Proxy; |
| import java.sql.Driver; |
| import java.sql.SQLException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.concurrent.TimeUnit; |
| |
| import static java.util.Arrays.asList; |
| |
| /** |
| * @version $Rev$ $Date$ |
| */ |
| public class DataSourceFactory { |
| |
| private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB, DataSourceFactory.class); |
| |
| public static final String LOG_SQL_PROPERTY = "LogSql"; |
| public static final String LOG_SQL_PACKAGE_PROPERTY = "LogSqlPackages"; |
| public static final String FLUSHABLE_PROPERTY = "Flushable"; |
| public static final String RESET_PROPERTY = "ResetOnError"; |
| public static final String RESET_METHODS_PROPERTY = "ResetOnErrorMethods"; |
| public static final String GLOBAL_LOG_SQL_PROPERTY = "openejb.jdbc.log"; |
| public static final String GLOBAL_LOG_SQL_PACKAGE_PROPERTY = "openejb.jdbc.log.packages"; |
| public static final String GLOBAL_FLUSH_PROPERTY = "openejb.jdbc.flushable"; |
| public static final String POOL_PROPERTY = "openejb.datasource.pool"; |
| public static final String DATA_SOURCE_CREATOR_PROP = "DataSourceCreator"; |
| public static final String XA_GLOBAL_FORCE_DIFFERENT = "openejb.datasource.xa.force-different-xaresource"; |
| public static final String XA_FORCE_DIFFERENT = "XAForceDifferent"; |
| public static final String HANDLER_PROPERTY = "TomEEProxyHandler"; |
| public static final String GLOBAL_HANDLER_PROPERTY = "openejb.jdbc.handler"; |
| |
| private static final Map<CommonDataSource, AlternativeDriver> driverByDataSource = new HashMap<>(); |
| |
| private static final Map<CommonDataSource, DataSourceCreator> creatorByDataSource = new HashMap<>(); |
| private static final Map<String, String> KNOWN_CREATORS = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER) {{ |
| put("simple", "org.apache.openejb.resource.jdbc.SimpleDataSourceCreator"); // use user provided DS, pooling not supported |
| put("dbcp", "org.apache.openejb.resource.jdbc.pool.DefaultDataSourceCreator"); // the original one |
| put("dbcp-alternative", "org.apache.openejb.resource.jdbc.dbcp.DbcpDataSourceCreator"); // dbcp for the ds pool only |
| put("tomcat", "org.apache.tomee.jdbc.TomEEDataSourceCreator"); // tomee |
| put("bonecp", "org.apache.openejb.bonecp.BoneCPDataSourceCreator"); // bonecp |
| }}; |
| |
| public static CommonDataSource create(final String name, |
| final boolean configuredManaged, |
| final Class impl, |
| final String definition, |
| final Duration maxWaitTime, |
| final Duration timeBetweenEvictionRuns, |
| final Duration minEvictableIdleTime, |
| final boolean useAlternativeDriver) throws IllegalAccessException, InstantiationException, IOException { |
| final Properties properties = asProperties(definition); |
| final Set<String> originalKeys = properties.stringPropertyNames(); |
| |
| final String handler = SystemInstance.get().getOptions().get(GLOBAL_HANDLER_PROPERTY, (String) properties.remove(HANDLER_PROPERTY)); |
| boolean flushable = SystemInstance.get().getOptions().get(GLOBAL_FLUSH_PROPERTY, |
| "true".equalsIgnoreCase((String) properties.remove(FLUSHABLE_PROPERTY))); |
| final String forceDifferent = SystemInstance.get().getOptions().get(XA_GLOBAL_FORCE_DIFFERENT, String.class.cast(properties.remove(XA_FORCE_DIFFERENT))); |
| |
| convert(properties, maxWaitTime, "maxWaitTime", "maxWait"); |
| convert(properties, timeBetweenEvictionRuns, "timeBetweenEvictionRuns", "timeBetweenEvictionRunsMillis"); |
| convert(properties, minEvictableIdleTime, "minEvictableIdleTime", "minEvictableIdleTimeMillis"); |
| |
| // these can be added and are managed by OpenEJB and not the DataSource itself |
| properties.remove("Definition"); |
| properties.remove("JtaManaged"); |
| properties.remove("ServiceId"); |
| |
| boolean managed = configuredManaged; |
| if (properties.containsKey("transactional")) { |
| managed = Boolean.parseBoolean((String) properties.remove("transactional")) || managed; |
| } |
| |
| normalizeJdbcUrl(properties); |
| |
| final String jdbcUrl = properties.getProperty("JdbcUrl"); |
| |
| final AlternativeDriver driver; |
| if (Driver.class.isAssignableFrom(impl) && jdbcUrl != null && useAlternativeDriver) { |
| try { |
| driver = new AlternativeDriver((Driver) impl.newInstance(), jdbcUrl); |
| driver.register(); |
| } catch (final SQLException e) { |
| throw new IllegalStateException(e); |
| } |
| } else { |
| driver = null; |
| } |
| |
| final boolean logSql = SystemInstance.get().getOptions().get(GLOBAL_LOG_SQL_PROPERTY, |
| "true".equalsIgnoreCase((String) properties.remove(LOG_SQL_PROPERTY))); |
| final String logPackages = SystemInstance.get().getProperty(GLOBAL_LOG_SQL_PACKAGE_PROPERTY, (String) properties.remove(LOG_SQL_PACKAGE_PROPERTY)); |
| final DataSourceCreator creator = creator(properties.remove(DATA_SOURCE_CREATOR_PROP), logSql); |
| final String resetOnError = (String) properties.remove(RESET_PROPERTY); |
| final String resetMethods = (String) properties.remove(RESET_METHODS_PROPERTY); // before setProperties() |
| |
| boolean useContainerLoader = "true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.resources.use-container-loader", "true")) && impl.getClassLoader() == DataSourceFactory.class.getClassLoader(); |
| final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); |
| if (useContainerLoader) { |
| final ClassLoader containerLoader = DataSourceFactory.class.getClassLoader(); |
| Thread.currentThread().setContextClassLoader(containerLoader); |
| try { |
| useContainerLoader = basicChecksThatDataSourceCanBeCreatedFromContainerLoader(properties, containerLoader); |
| } finally { |
| Thread.currentThread().setContextClassLoader(oldLoader); |
| } |
| if (useContainerLoader) { |
| Thread.currentThread().setContextClassLoader(containerLoader); |
| } else { |
| LOGGER.info("Can't use container loader to create datasource " + name + " so using application one"); |
| } |
| } |
| |
| try { |
| CommonDataSource ds; |
| if (createDataSourceFromClass(impl)) { // opposed to "by driver" |
| trimNotSupportedDataSourceProperties(properties); |
| |
| final ObjectRecipe recipe = new ObjectRecipe(impl); |
| recipe.allow(Option.CASE_INSENSITIVE_PROPERTIES); |
| recipe.allow(Option.IGNORE_MISSING_PROPERTIES); |
| recipe.allow(Option.NAMED_PARAMETERS); |
| recipe.allow(Option.PRIVATE_PROPERTIES); |
| recipe.setAllProperties(properties); |
| if (!properties.containsKey("url") && properties.containsKey("JdbcUrl")) { // depend on the datasource class so add all well known keys |
| recipe.setProperty("url", properties.getProperty("JdbcUrl")); |
| } |
| |
| CommonDataSource dataSource = (CommonDataSource) recipe.create(); |
| final boolean isDs = DataSource.class.isInstance(dataSource); |
| if (!isDs && XADataSource.class.isInstance(dataSource) && forceDifferent != null) { |
| try { |
| dataSource = CommonDataSource.class.cast(Thread.currentThread().getContextClassLoader() |
| .loadClass("true".equals(forceDifferent) ? "org.apache.openejb.resource.jdbc.xa.IsDifferentXaDataSourceWrapper" : forceDifferent) |
| .getConstructor(XADataSource.class) |
| .newInstance(dataSource)); |
| } catch (InvocationTargetException | ClassNotFoundException | NoSuchMethodException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| |
| if (managed) { |
| if (isDs && usePool(properties)) { |
| ds = creator.poolManaged(name, DataSource.class.cast(dataSource), properties); |
| } else { |
| ds = creator.managed(name, dataSource); |
| } |
| } else { |
| if (isDs && usePool(properties)) { |
| ds = creator.pool(name, DataSource.class.cast(dataSource), properties); |
| } else { |
| ds = dataSource; |
| } |
| } |
| } else { // by driver |
| if (managed) { |
| final XAResourceWrapper xaResourceWrapper = SystemInstance.get().getComponent(XAResourceWrapper.class); |
| if (xaResourceWrapper != null) { |
| ds = creator.poolManagedWithRecovery(name, xaResourceWrapper, impl.getName(), properties); |
| } else { |
| ds = creator.poolManaged(name, impl.getName(), properties); |
| } |
| } else { |
| ds = creator.pool(name, impl.getName(), properties); |
| } |
| } |
| |
| // ds and creator are associated here, not after the proxying of the next if if active |
| setCreatedWith(creator, ds); |
| if (driver != null) { |
| driverByDataSource.put(ds, driver); |
| } |
| |
| final boolean doResetOnError = resetOnError != null && !"false".equals(resetOnError); |
| if (doResetOnError || logSql || flushable) { // will get proxied |
| ObjectRecipe objectRecipe = null; |
| ResettableDataSourceHandler existingResettableHandler = null; |
| FlushableDataSourceHandler flushableDataSourceHandler = null; |
| if (ExecutionContext.isContextSet()) { |
| final ExecutionContext context = ExecutionContext.getContext(); |
| final List<Recipe> stack = context.getStack(); |
| if (stack.size() > 0) { |
| objectRecipe = ObjectRecipe.class.cast(stack.get(0)); |
| existingResettableHandler = ResettableDataSourceHandler.class.cast(objectRecipe.getProperty("resettableHandler")); |
| flushableDataSourceHandler = FlushableDataSourceHandler.class.cast(objectRecipe.getProperty("flushableHandler")); |
| |
| final Map<String, Object> props = objectRecipe.getProperties(); |
| for (final String key : originalKeys) { |
| props.remove(key); |
| } |
| |
| // meta properties, not needed here so gain few cycles removing them |
| props.remove("properties"); |
| props.remove("Definition"); |
| props.remove("ServiceId"); |
| props.remove("resettableHandler"); |
| props.remove("flushableHandler"); |
| |
| //we create a proxy so we cant get txmgr etc in another manner or we cant extend (= break) this method |
| new ObjectRecipe(ds.getClass()) {{ |
| allow(Option.CASE_INSENSITIVE_PROPERTIES); |
| allow(Option.IGNORE_MISSING_PROPERTIES); |
| allow(Option.NAMED_PARAMETERS); |
| allow(Option.PRIVATE_PROPERTIES); |
| setAllProperties(props); |
| }}.setProperties(ds); |
| } |
| } |
| |
| ds = wrapIfNeeded(handler, ds); |
| |
| if (logSql) { |
| ds = makeItLogging(ds, logPackages); |
| } |
| |
| final ResettableDataSourceHandler resettableDataSourceHandler; |
| if (doResetOnError) { // needs to be done after flushable |
| // ensure we reuse the same handle instance otherwise we loose state |
| resettableDataSourceHandler = existingResettableHandler != null ? |
| existingResettableHandler : |
| new ResettableDataSourceHandler(ds, resetOnError, resetMethods); |
| } else { |
| resettableDataSourceHandler = null; |
| } |
| |
| if (flushable || doResetOnError) { |
| if (flushableDataSourceHandler == null) { |
| final FlushableDataSourceHandler.FlushConfig flushConfig; |
| properties.remove("flushable"); // don't let it wrap the delegate again |
| |
| final Map<String, Object> recipeProps = new HashMap<>(objectRecipe == null ? new HashMap<>() : objectRecipe.getProperties()); |
| recipeProps.remove("properties"); |
| recipeProps.put("OpenEJBResourceClasspath", String.valueOf(useAlternativeDriver)); |
| |
| flushConfig = new FlushableDataSourceHandler.FlushConfig(recipeProps); |
| flushableDataSourceHandler = new FlushableDataSourceHandler(ds, flushConfig, resettableDataSourceHandler); |
| } else { |
| flushableDataSourceHandler.updateDataSource(ds); |
| } |
| ds = makeSerializableFlushableDataSourceProxy(ds, flushableDataSourceHandler); |
| } |
| if (doResetOnError) { // needs to be done after flushable |
| // ensure we reuse the same handle instance otherwise we loose state |
| resettableDataSourceHandler.updateDelegate(ds); |
| ds = makeSerializableFlushableDataSourceProxy(ds, resettableDataSourceHandler); |
| } |
| } else { |
| ds = wrapIfNeeded(handler, ds); |
| } |
| |
| return ds; |
| } finally { |
| if (useContainerLoader) { |
| Thread.currentThread().setContextClassLoader(oldLoader); |
| } |
| } |
| } |
| |
| private static CommonDataSource wrapIfNeeded(final String handler, final CommonDataSource ds) throws InstantiationException, IllegalAccessException { |
| if (handler != null) { |
| try { |
| final Class<?> handlerClass = Thread.currentThread().getContextClassLoader().loadClass(handler); |
| InvocationHandler instance; |
| try { |
| instance = InvocationHandler.class.cast(handlerClass.getConstructor(DataSource.class).newInstance(ds)); |
| } catch (final InvocationTargetException e) { |
| throw new IllegalStateException(e.getCause()); |
| } catch (final NoSuchMethodException e) { |
| try { |
| instance = InvocationHandler.class.cast(handlerClass.getConstructor(CommonDataSource.class).newInstance(ds)); |
| } catch (final InvocationTargetException e2) { |
| throw new IllegalStateException(e.getCause()); |
| } catch (final NoSuchMethodException e2) { |
| instance = InvocationHandler.class.cast(handlerClass.newInstance()); |
| } |
| } |
| return wrapWithHandler(ds, instance); |
| } catch (ClassNotFoundException e) { |
| throw new IllegalArgumentException("Can't find handler: " + handler, e); |
| } |
| } |
| return ds; |
| } |
| |
| public static CommonDataSource makeSerializableFlushableDataSourceProxy(final CommonDataSource ds, final InvocationHandler handler) { |
| return (CommonDataSource) Proxy.newProxyInstance( |
| Thread.currentThread().getContextClassLoader(), |
| new Class<?>[]{DataSource.class.isInstance(ds) ? DataSource.class : XADataSource.class, Serializable.class, Flushable.class}, |
| handler); |
| } |
| |
| private static boolean basicChecksThatDataSourceCanBeCreatedFromContainerLoader(final Properties properties, final ClassLoader containerLoader) { |
| // check basic some classes can be loaded from container otherwise don't force it |
| try { |
| for (final String property : asList("JdbcDriver", "driverClassName")) { |
| final String value = properties.getProperty(property); |
| if (value != null) { |
| Class.forName(value, false, containerLoader); |
| } |
| } |
| } catch (final ClassNotFoundException | NoClassDefFoundError cnfe) { |
| return false; |
| } |
| |
| // also password cipher can be loaded from apps |
| try { |
| final String cipher = properties.getProperty("PasswordCipher"); |
| if (cipher != null && !"PlainText".equals(cipher) && !"Static3DES".equals(cipher)) { |
| PasswordCipherFactory.getPasswordCipher(cipher); |
| } |
| } catch (final PasswordCipherException cnfe) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| public static void setCreatedWith(final DataSourceCreator creator, final CommonDataSource ds) { |
| creatorByDataSource.put(ds, creator); |
| } |
| |
| private static CommonDataSource wrapWithHandler(final CommonDataSource ds, final InvocationHandler instance) { |
| return (CommonDataSource) Proxy.newProxyInstance( |
| Thread.currentThread().getContextClassLoader(), |
| new Class<?>[]{DataSource.class.isInstance(ds) ? DataSource.class : XADataSource.class, Serializable.class}, |
| instance); |
| } |
| |
| public static DataSource makeItLogging(final CommonDataSource ds, final String packagesStr) { |
| final String[] pck = packagesStr == null ? null : packagesStr.split(" *, *"); |
| return (DataSource) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), |
| new Class<?>[]{DataSource.class, Serializable.class}, new LoggingSqlDataSource(ds, pck)); |
| } |
| |
| private static void normalizeJdbcUrl(final Properties properties) { |
| final String key = "JdbcUrl"; |
| final String jdbcUrl = properties.getProperty(key); |
| |
| if (jdbcUrl == null) { |
| return; |
| } |
| |
| try { |
| // get the plugin |
| final DataSourcePlugin helper = BasicDataSourceUtil.getDataSourcePlugin(jdbcUrl); |
| |
| // configure this |
| if (AbstractDataSourcePlugin.isActive(helper)) { |
| final String newUrl = helper.updatedUrl(jdbcUrl); |
| properties.setProperty(key, newUrl); |
| } |
| } catch (final SQLException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| |
| private static void convert(final Properties properties, final Duration duration, final String key, final String oldKey) { |
| properties.remove(key); |
| |
| // If someone is using the legacy property, use it |
| if (properties.containsKey(oldKey)) { |
| return; |
| } |
| properties.remove(oldKey); |
| |
| if (duration == null) { |
| return; |
| } |
| if (duration.getUnit() == null) { |
| duration.setUnit(TimeUnit.MILLISECONDS); |
| } |
| |
| final long milliseconds = TimeUnit.MILLISECONDS.convert(duration.getTime(), duration.getUnit()); |
| properties.put(oldKey, String.valueOf(milliseconds)); |
| } |
| |
| public static DataSourceCreator creator(final Object creatorName, final boolean willBeProxied) { |
| final DataSourceCreator defaultCreator = SystemInstance.get().getComponent(DataSourceCreator.class); |
| final ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| if (creatorName != null && creatorName instanceof String |
| && (defaultCreator == null || !creatorName.equals(defaultCreator.getClass().getName()))) { |
| String clazz = KNOWN_CREATORS.get(creatorName); |
| if (clazz == null) { |
| clazz = (String) creatorName; |
| } |
| if (willBeProxied && clazz.equals(DefaultDataSourceCreator.class.getName())) { |
| clazz = DbcpDataSourceCreator.class.getName(); |
| } |
| try { |
| return (DataSourceCreator) loader.loadClass(clazz).newInstance(); |
| } catch (final Throwable e) { |
| LOGGER.error("can't create '" + creatorName + "', the default one will be used: " + defaultCreator, e); |
| } |
| } |
| if (defaultCreator instanceof DefaultDataSourceCreator && willBeProxied) { |
| // this one is proxiable, not the default one (legacy) |
| return new DbcpDataSourceCreator(); |
| } |
| return defaultCreator; |
| } |
| |
| private static boolean createDataSourceFromClass(final Class<?> impl) { |
| return isDataSource(impl) && !SystemInstance.get().getOptions().get("org.apache.openejb.resource.jdbc.hot.deploy", false); |
| } |
| |
| private static boolean isDataSource(final Class<?> impl) { |
| return DataSource.class.isAssignableFrom(impl) || XADataSource.class.isAssignableFrom(impl) || |
| ConnectionPoolDataSource.class.isAssignableFrom(impl); |
| } |
| |
| private static boolean usePool(final Properties properties) { |
| String property = properties.getProperty(POOL_PROPERTY, SystemInstance.get().getProperty(POOL_PROPERTY)); |
| if (property != null) { |
| properties.remove(POOL_PROPERTY); |
| } else { // defined from @DataSourceDefinition and doesn't need pooling |
| final String initialPoolSize = properties.getProperty("initialPoolSize"); |
| final String maxPoolSize = properties.getProperty("maxPoolSize"); |
| if ((null == initialPoolSize || "-1".equals(initialPoolSize)) |
| && ("-1".equals(maxPoolSize) || maxPoolSize == null)) { |
| property = "false"; |
| } |
| } |
| return "true".equalsIgnoreCase(property) || null == property; |
| } |
| |
| private static Properties asProperties(final String definition) throws IOException { |
| final SuperProperties properties = new SuperProperties(); |
| properties.caseInsensitive(true); |
| properties.putAll(IO.readProperties(IO.read(definition), new Properties())); |
| return properties; |
| } |
| |
| public static void trimNotSupportedDataSourceProperties(final Properties properties) { |
| properties.remove("LoginTimeout"); |
| } |
| |
| public static boolean knows(final Object object) { |
| return object instanceof CommonDataSource && creatorByDataSource.containsKey(CommonDataSource.class.cast(realInstance(object))); |
| } |
| |
| // TODO: should we get a get and a clear method instead of a single one? |
| @SuppressWarnings("SuspiciousMethodCalls") |
| public static ObjectRecipe forgetRecipe(final Object rawObject, final ObjectRecipe defaultValue) { |
| final Object object = realInstance(rawObject); |
| final DataSourceCreator creator = creatorByDataSource.get(object); |
| ObjectRecipe recipe = null; |
| if (creator != null) { |
| recipe = creator.clearRecipe(object); |
| } |
| if (recipe == null) { |
| return defaultValue; |
| } |
| return recipe; |
| } |
| |
| @SuppressWarnings("SuspiciousMethodCalls") |
| public static void destroy(final Object o) throws Throwable { |
| final Object instance = realInstance(o); |
| if (instance == null) { |
| return; |
| } |
| final DataSourceCreator remove = creatorByDataSource.remove(instance); |
| remove.destroy(instance); |
| |
| final AlternativeDriver driver = driverByDataSource.remove(instance); |
| if (driver != null) { |
| driver.deregister(); |
| } |
| } |
| |
| // remove proxy added by us in front of the datasource returned by the creator |
| private static Object realInstance(final Object o) { |
| if (o == null || !(o instanceof DataSource)) { |
| return o; |
| } |
| |
| Object ds = o; |
| while (Proxy.isProxyClass(ds.getClass())) { |
| final InvocationHandler handler = Proxy.getInvocationHandler(ds); |
| if (DelegatableHandler.class.isInstance(handler)) { |
| ds = DelegatableHandler.class.cast(handler).getDelegate(); |
| } else { |
| break; |
| } |
| } |
| |
| return ds; |
| } |
| } |