001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.activemq.util.osgi; 018 019import static org.osgi.framework.wiring.BundleRevision.PACKAGE_NAMESPACE; 020 021import java.io.BufferedReader; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.InputStreamReader; 025import java.lang.reflect.InvocationTargetException; 026import java.net.URL; 027import java.util.ArrayList; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Properties; 031import java.util.Set; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentMap; 034 035import org.apache.activemq.Service; 036import org.apache.activemq.store.PersistenceAdapter; 037import org.apache.activemq.transport.Transport; 038import org.apache.activemq.transport.discovery.DiscoveryAgent; 039import org.apache.activemq.util.FactoryFinder; 040import org.apache.activemq.util.FactoryFinder.ObjectFactory; 041import org.osgi.framework.Bundle; 042import org.osgi.framework.BundleActivator; 043import org.osgi.framework.BundleContext; 044import org.osgi.framework.BundleEvent; 045import org.osgi.framework.SynchronousBundleListener; 046import org.osgi.framework.wiring.BundleCapability; 047import org.osgi.framework.wiring.BundleWire; 048import org.osgi.framework.wiring.BundleWiring; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052/** 053 * An OSGi bundle activator for ActiveMQ which adapts the {@link org.apache.activemq.util.FactoryFinder} 054 * to the OSGi environment. 055 * 056 */ 057public class Activator implements BundleActivator, SynchronousBundleListener, ObjectFactory { 058 059 private static final Logger LOG = LoggerFactory.getLogger(Activator.class); 060 061 private final ConcurrentMap<String, Class<?>> serviceCache = new ConcurrentHashMap<String, Class<?>>(); 062 private final ConcurrentMap<Long, BundleWrapper> bundleWrappers = new ConcurrentHashMap<Long, BundleWrapper>(); 063 private BundleContext bundleContext; 064 private Set<BundleCapability> packageCapabilities = new HashSet<BundleCapability>(); 065 066 // ================================================================ 067 // BundleActivator interface impl 068 // ================================================================ 069 070 @Override 071 public synchronized void start(BundleContext bundleContext) throws Exception { 072 073 // This is how we replace the default FactoryFinder strategy 074 // with one that is more compatible in an OSGi env. 075 FactoryFinder.setObjectFactory(this); 076 077 debug("activating"); 078 this.bundleContext = bundleContext; 079 080 cachePackageCapabilities(Service.class, Transport.class, DiscoveryAgent.class, PersistenceAdapter.class); 081 082 debug("checking existing bundles"); 083 bundleContext.addBundleListener(this); 084 for (Bundle bundle : bundleContext.getBundles()) { 085 if (bundle.getState() == Bundle.RESOLVED || bundle.getState() == Bundle.STARTING || 086 bundle.getState() == Bundle.ACTIVE || bundle.getState() == Bundle.STOPPING) { 087 register(bundle); 088 } 089 } 090 debug("activated"); 091 } 092 093 /** 094 * Caches the package capabilities that are needed for a set of interface classes 095 * 096 * @param classes interfaces we want to track 097 */ 098 private void cachePackageCapabilities(Class<?> ... classes) { 099 BundleWiring ourWiring = bundleContext.getBundle().adapt(BundleWiring.class); 100 Set<String> packageNames = new HashSet<String>(); 101 for (Class<?> clazz: classes) { 102 packageNames.add(clazz.getPackage().getName()); 103 } 104 105 List<BundleCapability> ourExports = ourWiring.getCapabilities(PACKAGE_NAMESPACE); 106 for (BundleCapability ourExport : ourExports) { 107 String ourPkgName = (String) ourExport.getAttributes().get(PACKAGE_NAMESPACE); 108 if (packageNames.contains(ourPkgName)) { 109 packageCapabilities.add(ourExport); 110 } 111 } 112 } 113 114 115 @Override 116 public synchronized void stop(BundleContext bundleContext) throws Exception { 117 debug("deactivating"); 118 bundleContext.removeBundleListener(this); 119 while (!bundleWrappers.isEmpty()) { 120 unregister(bundleWrappers.keySet().iterator().next()); 121 } 122 debug("deactivated"); 123 this.bundleContext = null; 124 } 125 126 // ================================================================ 127 // SynchronousBundleListener interface impl 128 // ================================================================ 129 130 @Override 131 public void bundleChanged(BundleEvent event) { 132 if (event.getType() == BundleEvent.RESOLVED) { 133 register(event.getBundle()); 134 } else if (event.getType() == BundleEvent.UNRESOLVED || event.getType() == BundleEvent.UNINSTALLED) { 135 unregister(event.getBundle().getBundleId()); 136 } 137 } 138 139 protected void register(final Bundle bundle) { 140 debug("checking bundle " + bundle.getBundleId()); 141 if (isOurBundle(bundle) || isImportingUs(bundle) ) { 142 debug("Registering bundle for extension resolution: "+ bundle.getBundleId()); 143 bundleWrappers.put(bundle.getBundleId(), new BundleWrapper(bundle)); 144 } 145 } 146 147 private boolean isOurBundle(final Bundle bundle) { 148 return bundle.getBundleId() == bundleContext.getBundle().getBundleId(); 149 } 150 151 /** 152 * When bundles unload.. we remove them thier cached Class entries from the 153 * serviceCache. Future service lookups for the service will fail. 154 * 155 * TODO: consider a way to get the Broker release any references to 156 * instances of the service. 157 * 158 * @param bundleId 159 */ 160 protected void unregister(long bundleId) { 161 BundleWrapper bundle = bundleWrappers.remove(bundleId); 162 if (bundle != null) { 163 for (String path : bundle.cachedServices) { 164 debug("unregistering service for key: " +path ); 165 serviceCache.remove(path); 166 } 167 } 168 } 169 170 // ================================================================ 171 // ObjectFactory interface impl 172 // ================================================================ 173 174 @Override 175 public Object create(String path) throws IllegalAccessException, InstantiationException, IOException, ClassNotFoundException { 176 Class<?> clazz = serviceCache.get(path); 177 if (clazz == null) { 178 StringBuffer warnings = new StringBuffer(); 179 // We need to look for a bundle that has that class. 180 int wrrningCounter=1; 181 for (BundleWrapper wrapper : bundleWrappers.values()) { 182 URL resource = wrapper.bundle.getResource(path); 183 if( resource == null ) { 184 continue; 185 } 186 187 Properties properties = loadProperties(resource); 188 189 String className = properties.getProperty("class"); 190 if (className == null) { 191 warnings.append("("+(wrrningCounter++)+") Invalid service file in bundle "+wrapper+": 'class' property not defined."); 192 continue; 193 } 194 195 try { 196 clazz = wrapper.bundle.loadClass(className); 197 } catch (ClassNotFoundException e) { 198 warnings.append("("+(wrrningCounter++)+") Bundle "+wrapper+" could not load "+className+": "+e); 199 continue; 200 } 201 202 // Yay.. the class was found. Now cache it. 203 serviceCache.put(path, clazz); 204 wrapper.cachedServices.add(path); 205 break; 206 } 207 208 if( clazz == null ) { 209 // Since OSGi is such a tricky environment to work in.. lets give folks the 210 // most information we can in the error message. 211 String msg = "Service not found: '" + path + "'"; 212 if (warnings.length()!= 0) { 213 msg += ", "+warnings; 214 } 215 throw new IOException(msg); 216 } 217 } 218 219 try { 220 return clazz.getConstructor().newInstance(); 221 } catch (InvocationTargetException | NoSuchMethodException e) { 222 throw new InstantiationException(e.getMessage()); 223 } 224 } 225 226 // ================================================================ 227 // Internal Helper Methods 228 // ================================================================ 229 230 private void debug(Object msg) { 231 LOG.debug(msg.toString()); 232 } 233 234 private Properties loadProperties(URL resource) throws IOException { 235 InputStream in = resource.openStream(); 236 try { 237 BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8")); 238 Properties properties = new Properties(); 239 properties.load(in); 240 return properties; 241 } finally { 242 try { 243 in.close(); 244 } catch (Exception e) { 245 } 246 } 247 } 248 249 /** 250 * We consider a bundle to be a candidate for objects if it imports at least 251 * one of the packages of our interfaces 252 * 253 * @param bundle 254 * @return true if the bundle is improting. 255 */ 256 private boolean isImportingUs(Bundle bundle) { 257 BundleWiring wiring = bundle.adapt(BundleWiring.class); 258 List<BundleWire> imports = wiring.getRequiredWires(PACKAGE_NAMESPACE); 259 for (BundleWire importWire : imports) { 260 if (packageCapabilities.contains(importWire.getCapability())) { 261 return true; 262 } 263 } 264 return false; 265 } 266 267 private static class BundleWrapper { 268 private final Bundle bundle; 269 private final List<String> cachedServices = new ArrayList<String>(); 270 271 public BundleWrapper(Bundle bundle) { 272 this.bundle = bundle; 273 } 274 } 275}