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}