/** * 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.aries.spifly.dynamic; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.aries.spifly.BaseActivator; import org.apache.aries.spifly.SpiFlyConstants; import org.apache.aries.spifly.Streams; import org.easymock.EasyMock; import org.easymock.IAnswer; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleReference; import org.osgi.framework.Version; import org.osgi.framework.hooks.weaving.WeavingHook; import org.osgi.framework.hooks.weaving.WovenClass; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWiring; public class ClientWeavingHookOSGi43Test { DynamicWeavingActivator activator; @Before public void setUp() { activator = new DynamicWeavingActivator(); BaseActivator.activator = activator; } @After public void tearDown() { BaseActivator.activator = null; activator = null; } @Test public void testBasicServiceLoaderUsageWithClassLoaderFromBundleRevision() throws Exception { Dictionary<String, String> consumerHeaders = new Hashtable<String, String>(); consumerHeaders.put(SpiFlyConstants.SPI_CONSUMER_HEADER, "*"); // Register the bundle that provides the SPI implementation. Bundle providerBundle = mockProviderBundle("impl1", 1); activator.registerProviderBundle("org.apache.aries.mytest.MySPI", providerBundle, new HashMap<String, Object>()); Bundle consumerBundle = mockConsumerBundle(consumerHeaders, providerBundle); activator.addConsumerWeavingData(consumerBundle, SpiFlyConstants.SPI_CONSUMER_HEADER); Bundle spiFlyBundle = mockSpiFlyBundle("spifly", Version.parseVersion("1.9.4"), consumerBundle, providerBundle); WeavingHook wh = new ClientWeavingHook(spiFlyBundle.getBundleContext(), activator); // Weave the TestClient class. URL clsUrl = getClass().getResource("TestClient.class"); Assert.assertNotNull("Precondition", clsUrl); String clientClassName = "org.apache.aries.spifly.dynamic.TestClient"; WovenClass wc = new MyWovenClass(clsUrl, clientClassName, consumerBundle); Assert.assertEquals("Precondition", 0, wc.getDynamicImports().size()); wh.weave(wc); Assert.assertEquals(1, wc.getDynamicImports().size()); String di1 = "org.apache.aries.spifly;bundle-symbolic-name=spifly;bundle-version=1.9.4"; String di2 = "org.apache.aries.spifly;bundle-version=1.9.4;bundle-symbolic-name=spifly"; String di = wc.getDynamicImports().get(0); Assert.assertTrue("Weaving should have added a dynamic import", di1.equals(di) || di2.equals(di)); // Invoke the woven class and check that it properly sets the TCCL so that the // META-INF/services/org.apache.aries.mytest.MySPI file from impl1 is visible. Class<?> cls = wc.getDefinedClass(); Method method = cls.getMethod("test", new Class [] {String.class}); Object result = method.invoke(cls.newInstance(), "hello"); Assert.assertEquals(Collections.singleton("olleh"), result); } private Bundle mockSpiFlyBundle(String bsn, Version version, Bundle ... bundles) throws Exception { Bundle spiFlyBundle = EasyMock.createMock(Bundle.class); BundleContext spiFlyBundleContext = EasyMock.createMock(BundleContext.class); EasyMock.expect(spiFlyBundleContext.getBundle()).andReturn(spiFlyBundle).anyTimes(); List<Bundle> allBundles = new ArrayList<Bundle>(Arrays.asList(bundles)); allBundles.add(spiFlyBundle); EasyMock.expect(spiFlyBundleContext.getBundles()).andReturn(allBundles.toArray(new Bundle [] {})).anyTimes(); EasyMock.replay(spiFlyBundleContext); EasyMock.expect(spiFlyBundle.getSymbolicName()).andReturn(bsn).anyTimes(); EasyMock.expect(spiFlyBundle.getVersion()).andReturn(version).anyTimes(); EasyMock.expect(spiFlyBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); EasyMock.expect(spiFlyBundle.getBundleContext()).andReturn(spiFlyBundleContext).anyTimes(); EasyMock.replay(spiFlyBundle); // Set the bundle context for testing purposes Field bcField = BaseActivator.class.getDeclaredField("bundleContext"); bcField.setAccessible(true); bcField.set(activator, spiFlyBundle.getBundleContext()); return spiFlyBundle; } private Bundle mockProviderBundle(String subdir, long id) throws Exception { return mockProviderBundle(subdir, id, Version.emptyVersion); } private Bundle mockProviderBundle(String subdir, long id, Version version) throws Exception { URL url = getClass().getResource("/" + getClass().getName().replace('.', '/') + ".class"); File classFile = new File(url.getFile()); File baseDir = new File(classFile.getParentFile(), subdir); File directory = new File(baseDir, "/META-INF/services"); final List<String> classNames = new ArrayList<String>(); // Do a directory listing of the applicable META-INF/services directory List<String> resources = new ArrayList<String>(); for (File f : directory.listFiles()) { String fileName = f.getName(); if (fileName.startsWith(".") || fileName.endsWith(".")) continue; classNames.addAll(getClassNames(f)); // Needs to be something like: META-INF/services/org.apache.aries.mytest.MySPI String path = f.getAbsolutePath().substring(baseDir.getAbsolutePath().length()); path = path.replace('\\', '/'); if (path.startsWith("/")) { path = path.substring(1); } resources.add(path); } // Set up the classloader that will be used by the ASM-generated code as the TCCL. // It can load a META-INF/services file final ClassLoader cl = new TestProviderBundleClassLoader(subdir, resources.toArray(new String [] {})); final List<String> classResources = new ArrayList<String>(); for(String className : classNames) { classResources.add("/" + className.replace('.', '/') + ".class"); } Bundle systemBundle = EasyMock.createMock(Bundle.class); EasyMock.<Class<?>>expect(systemBundle.loadClass(EasyMock.anyObject(String.class))).andAnswer(new IAnswer<Class<?>>() { @Override public Class<?> answer() throws Throwable { String name = (String) EasyMock.getCurrentArguments()[0]; return ClientWeavingHookOSGi43Test.class.getClassLoader().loadClass(name); } }).anyTimes(); EasyMock.replay(systemBundle); BundleContext bc = EasyMock.createNiceMock(BundleContext.class); EasyMock.expect(bc.getBundle(0)).andReturn(systemBundle).anyTimes(); EasyMock.replay(bc); BundleWiring bundleWiring = EasyMock.createMock(BundleWiring.class); EasyMock.expect(bundleWiring.getClassLoader()).andReturn(cl).anyTimes(); EasyMock.replay(bundleWiring); BundleRevision bundleRevision = EasyMock.createMock(BundleRevision.class); EasyMock.expect(bundleRevision.getWiring()).andReturn(bundleWiring).anyTimes(); EasyMock.replay(bundleRevision); Bundle providerBundle = EasyMock.createMock(Bundle.class); String bsn = subdir; int idx = bsn.indexOf('_'); if (idx > 0) { bsn = bsn.substring(0, idx); } EasyMock.expect(providerBundle.getSymbolicName()).andReturn(bsn).anyTimes(); EasyMock.expect(providerBundle.getBundleId()).andReturn(id).anyTimes(); EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); EasyMock.expect(providerBundle.adapt(BundleRevision.class)).andReturn(bundleRevision).anyTimes(); EasyMock.<Class<?>>expect(providerBundle.loadClass(EasyMock.anyObject(String.class))).andAnswer(new IAnswer<Class<?>>() { @Override public Class<?> answer() throws Throwable { String name = (String) EasyMock.getCurrentArguments()[0]; if (!classNames.contains(name)) { throw new ClassCastException(name); } return cl.loadClass(name); } }).anyTimes(); EasyMock.replay(providerBundle); return providerBundle; } private Collection<String> getClassNames(File f) throws IOException { List<String> names = new ArrayList<String>(); BufferedReader br = new BufferedReader(new FileReader(f)); try { String line = null; while((line = br.readLine()) != null) { names.add(line.trim()); } } finally { br.close(); } return names; } private Bundle mockConsumerBundle(Dictionary<String, String> headers, Bundle ... otherBundles) { // Create a mock object for the client bundle which holds the code that uses ServiceLoader.load() // or another SPI invocation. BundleContext bc = EasyMock.createMock(BundleContext.class); Bundle consumerBundle = EasyMock.createMock(Bundle.class); EasyMock.expect(consumerBundle.getSymbolicName()).andReturn("testConsumer").anyTimes(); EasyMock.expect(consumerBundle.getHeaders()).andReturn(headers).anyTimes(); EasyMock.expect(consumerBundle.getBundleContext()).andReturn(bc).anyTimes(); EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(null).anyTimes(); EasyMock.replay(consumerBundle); List<Bundle> allBundles = new ArrayList<Bundle>(Arrays.asList(otherBundles)); allBundles.add(consumerBundle); EasyMock.expect(bc.getBundles()).andReturn(allBundles.toArray(new Bundle [] {})).anyTimes(); EasyMock.replay(bc); return consumerBundle; } // A classloader that loads anything starting with org.apache.aries.spifly.dynamic.impl1 from it // and the rest from the parent. This is to mimic a bundle that holds a specific SPI implementation. public static class TestProviderBundleClassLoader extends URLClassLoader { private final List<String> resources; private final String prefix; private final String classPrefix; private final Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<String, Class<?>>(); public TestProviderBundleClassLoader(String subdir, String ... resources) { super(new URL [] {}, TestProviderBundleClassLoader.class.getClassLoader()); this.prefix = TestProviderBundleClassLoader.class.getPackage().getName().replace('.', '/') + "/" + subdir + "/"; this.classPrefix = prefix.replace('/', '.'); this.resources = Arrays.asList(resources); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (name.startsWith(classPrefix)) return loadClassLocal(name); return super.loadClass(name); } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.startsWith(classPrefix)) { Class<?> cls = loadClassLocal(name); if (resolve) resolveClass(cls); return cls; } return super.loadClass(name, resolve); } protected Class<?> loadClassLocal(String name) throws ClassNotFoundException { Class<?> prevLoaded = loadedClasses.get(name); if (prevLoaded != null) return prevLoaded; URL res = TestProviderBundleClassLoader.class.getClassLoader().getResource(name.replace('.', '/') + ".class"); try { byte[] bytes = Streams.suck(res.openStream()); Class<?> cls = defineClass(name, bytes, 0, bytes.length); loadedClasses.put(name, cls); return cls; } catch (Exception e) { throw new ClassNotFoundException(name, e); } } @Override public URL findResource(String name) { if (resources.contains(name)) { return getClass().getClassLoader().getResource(prefix + name); } else { return super.findResource(name); } } @Override public Enumeration<URL> findResources(String name) throws IOException { if (resources.contains(name)) { return getClass().getClassLoader().getResources(prefix + name); } else { return super.findResources(name); } } } private static class MyWovenClass implements WovenClass { byte [] bytes; final String className; final Bundle bundleContainingOriginalClass; List<String> dynamicImports = new ArrayList<String>(); boolean weavingComplete = false; private MyWovenClass(URL clazz, String name, Bundle bundle) throws Exception { bytes = Streams.suck(clazz.openStream()); className = name; bundleContainingOriginalClass = bundle; } @Override public byte[] getBytes() { return bytes; } @Override public void setBytes(byte[] newBytes) { bytes = newBytes; } @Override public List<String> getDynamicImports() { return dynamicImports; } @Override public boolean isWeavingComplete() { return weavingComplete; } @Override public String getClassName() { return className; } @Override public ProtectionDomain getProtectionDomain() { return null; } @Override public Class<?> getDefinedClass() { try { weavingComplete = true; return new MyWovenClassClassLoader(className, getBytes(), getClass().getClassLoader(), bundleContainingOriginalClass).loadClass(className); } catch (ClassNotFoundException e) { e.printStackTrace(); return null; } } @Override public BundleWiring getBundleWiring() { BundleWiring bw = EasyMock.createMock(BundleWiring.class); EasyMock.expect(bw.getBundle()).andReturn(bundleContainingOriginalClass); EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()); EasyMock.replay(bw); return bw; } } private static class MyWovenClassClassLoader extends ClassLoader implements BundleReference { private final String className; private final Bundle bundle; private final byte [] bytes; private Class<?> wovenClass; public MyWovenClassClassLoader(String className, byte[] bytes, ClassLoader parent, Bundle bundle) { super(parent); this.className = className; this.bundle = bundle; this.bytes = bytes; } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(className)) { if (wovenClass == null) wovenClass = defineClass(className, bytes, 0, bytes.length); return wovenClass; } else { return super.loadClass(name, resolve); } } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } @Override public Bundle getBundle() { return bundle; } } }