001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *  http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020package org.apache.xbean.osgi.bundle.util;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Dictionary;
031import java.util.Enumeration;
032import java.util.HashMap;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.concurrent.CopyOnWriteArrayList;
038
039import org.osgi.framework.Bundle;
040import org.osgi.framework.BundleContext;
041import org.osgi.framework.BundleException;
042import org.osgi.framework.Constants;
043import org.osgi.framework.ServiceReference;
044import org.osgi.framework.Version;
045import org.osgi.framework.wiring.BundleCapability;
046import org.osgi.framework.wiring.BundleRevision;
047import org.osgi.framework.wiring.BundleWiring;
048
049/**
050 * Bundle that delegates ClassLoader operations to a collection of {@link Bundle} objects.
051 *
052 * @version $Rev: 1371482 $ $Date: 2012-08-09 22:58:28 +0200 (Thu, 09 Aug 2012) $
053 */
054public class DelegatingBundle implements Bundle {
055
056    private static final String PACKAGE_CACHE = DelegatingBundle.class.getName() + ".packageCache";    
057    private static final String RESOURCE_CACHE_SIZE = DelegatingBundle.class.getName() + ".resourceCacheSize";
058    
059    private static final URL NOT_FOUND_RESOURCE;
060    
061    static {
062        try {
063            NOT_FOUND_RESOURCE = new URL("file://foo");
064        } catch (MalformedURLException e) {
065            throw new Error(e);
066        }        
067    }
068    
069    private CopyOnWriteArrayList<Bundle> bundles;
070    private Bundle bundle;
071    private BundleContext bundleContext;
072
073    private final boolean hasDynamicImports;
074    private final Map<String, URL> resourceCache;
075    private final boolean packageCacheEnabled;
076    private Map<String, Bundle> packageCache;
077    
078    public DelegatingBundle(Collection<Bundle> bundles) {
079        if (bundles.isEmpty()) {
080            throw new IllegalArgumentException("At least one bundle is required");
081        }
082        this.bundles = new CopyOnWriteArrayList<Bundle>(bundles);
083        Iterator<Bundle> iterator = bundles.iterator();
084        // assume first Bundle is the main bundle
085        this.bundle = iterator.next();
086        this.bundleContext = new DelegatingBundleContext(this, bundle.getBundleContext());
087        this.hasDynamicImports = hasDynamicImports(iterator);
088        this.resourceCache = initResourceCache();
089        this.packageCacheEnabled = initPackageCacheEnabled();
090    }
091
092    public DelegatingBundle(Bundle bundle) {
093        this(Collections.singletonList(bundle));
094    }
095    
096    private static Map<String, URL> initResourceCache() {
097        String value = System.getProperty(RESOURCE_CACHE_SIZE, "250");
098        int size = Integer.parseInt(value);
099        if (size > 0) {
100            return Collections.synchronizedMap(new Cache<String, URL>(size));
101        } else {
102            return null;
103        }
104    }
105    
106    private static boolean initPackageCacheEnabled() {
107        String value = System.getProperty(PACKAGE_CACHE, "true");
108        boolean enabled = Boolean.parseBoolean(value);
109        return enabled;
110    }
111    
112    /*
113     * Returns true if a single bundle has Dynamic-ImportPackage: *. False, otherwise.       
114     */
115    private boolean hasDynamicImports(Iterator<Bundle> iterator) {
116        while (iterator.hasNext()) {
117            Bundle delegate = iterator.next();
118            if (hasWildcardDynamicImport(delegate)) {
119                return true;
120            }
121        }
122        return false;
123    }
124    
125    private synchronized Map<String, Bundle> getPackageBundleMap() {
126        if (packageCache == null) {
127            packageCache = buildPackageBundleMap();
128        }
129        return packageCache;
130    }
131    
132    private synchronized void reset() {
133        resourceCache.clear();
134        packageCache = null;
135    }
136
137    private Map<String, Bundle> buildPackageBundleMap() {
138        Map<String, Bundle> map = new HashMap<String, Bundle>();
139        Iterator<Bundle> iterator = bundles.iterator();
140        // skip first bundle
141        iterator.next();
142        // attempt to load the class from the remaining bundles
143        while (iterator.hasNext()) {
144            Bundle bundle = iterator.next();
145            BundleWiring wiring = bundle.adapt(BundleWiring.class);
146            if (wiring != null) {
147                List<BundleCapability> capabilities = wiring.getCapabilities(BundleRevision.PACKAGE_NAMESPACE);
148                if (capabilities != null && !capabilities.isEmpty()) {
149                    for (BundleCapability capability : capabilities) {
150                        Map<String, Object> attributes = capability.getAttributes();
151                        if (attributes != null) {
152                            String packageName = String.valueOf(attributes.get(BundleRevision.PACKAGE_NAMESPACE));
153                            if (!map.containsKey(packageName)) {
154                                map.put(packageName, bundle);
155                            }
156                        }
157                    }
158                }
159            }
160        }
161        return map;
162    }
163    
164    public Bundle getMainBundle() {
165        return bundle;
166    }
167        
168    public Class<?> loadClass(String name) throws ClassNotFoundException {
169        try {
170            Class<?> clazz = bundle.loadClass(name);
171            return clazz;
172        } catch (ClassNotFoundException cnfe) {
173            if (name.startsWith("java.")) {
174                throw cnfe;
175            }
176            
177            int index = name.lastIndexOf('.');
178            if (index > 0 && bundles.size() > 1) {
179                String packageName = name.substring(0, index);
180                if (packageCacheEnabled) {
181                    return findCachedClass(name, packageName, cnfe);
182                } else {
183                    return findClass(name, packageName, cnfe);
184                }
185            }
186            
187            throw cnfe;
188        }
189    }
190    
191    private Class<?> findCachedClass(String className, String packageName, ClassNotFoundException cnfe) throws ClassNotFoundException {
192        Map<String, Bundle> map = getPackageBundleMap();
193        Bundle bundle = map.get(packageName);
194        if (bundle == null) {
195            // Work-around for Introspector always looking for classes in sun.beans.infos
196            if (packageName.equals("sun.beans.infos") && className.endsWith("BeanInfo")) {
197                throw cnfe;
198            }
199            return findClass(className, packageName, cnfe);
200        } else {
201            return bundle.loadClass(className);
202        }
203    }
204        
205    private Class<?> findClass(String className, String packageName, ClassNotFoundException cnfe) throws ClassNotFoundException {
206        Iterator<Bundle> iterator = bundles.iterator();
207        // skip first bundle
208        iterator.next();
209        while (iterator.hasNext()) {
210            Bundle delegate = iterator.next();
211            if (hasDynamicImports && hasWildcardDynamicImport(delegate)) {
212                // skip any bundles with Dynamic-ImportPackage: * to avoid unnecessary wires
213                continue;
214            }
215            try {
216                return delegate.loadClass(className);
217            } catch (ClassNotFoundException e) {
218                // ignore
219            }
220        }
221        throw cnfe;
222    }
223      
224    private static boolean hasWildcardDynamicImport(Bundle bundle) {
225        Dictionary<String, String> headers = bundle.getHeaders();
226        if (headers != null) {
227            String value = headers.get(Constants.DYNAMICIMPORT_PACKAGE);
228            if (value == null) {
229                return false;
230            } else {
231                return "*".equals(value.trim());
232            }
233        } else {
234            return false;
235        }
236    }
237    
238    public void addBundle(Bundle b) {
239        bundles.add(b);
240        reset();
241    }
242
243    public void removeBundle(Bundle b) {
244        bundles.remove(b);
245        reset();
246    }
247
248    public URL getResource(String name) {
249        URL resource = null;
250        if (resourceCache == null) {
251            resource = findResource(name);
252        } else {
253            resource = findCachedResource(name);
254        }
255        return resource;
256    }
257    
258    private URL findCachedResource(String name) {
259        URL resource = bundle.getResource(name);
260        if (resource == null) {
261            resource = resourceCache.get(name);
262            if (resource == null) {
263                Iterator<Bundle> iterator = bundles.iterator();
264                // skip first bundle
265                iterator.next();
266                // look for resource in the remaining bundles
267                resource = findResource(name, iterator);                
268                resourceCache.put(name, (resource == null) ? NOT_FOUND_RESOURCE : resource);
269            } else if (resource == NOT_FOUND_RESOURCE) {
270                resource = null;
271            }
272        }
273        return resource;
274    }
275    
276    private URL findResource(String name) {
277        Iterator<Bundle> iterator = bundles.iterator();
278        return findResource(name, iterator);
279    }
280    
281    private URL findResource(String name, Iterator<Bundle> iterator) {
282        URL resource = null;
283        while (iterator.hasNext() && resource == null) {
284            Bundle delegate = iterator.next();
285            resource = delegate.getResource(name);
286        }
287        return resource;
288    }
289
290    public Enumeration<URL> getResources(String name) throws IOException {
291        ArrayList<URL> allResources = new ArrayList<URL>();
292        for (Bundle bundle : bundles) {
293            Enumeration<URL> e = bundle.getResources(name);
294            addToList(allResources, e);
295        }
296        return Collections.enumeration(allResources);
297    }
298
299    private static void addToList(List<URL> list, Enumeration<URL> enumeration) {
300        if (enumeration != null) {
301            while (enumeration.hasMoreElements()) {
302                list.add(enumeration.nextElement());
303            }
304        }
305    }
306
307    public BundleContext getBundleContext() {
308        return bundleContext;
309    }
310
311    public Enumeration findEntries(String arg0, String arg1, boolean arg2) {
312        return bundle.findEntries(arg0, arg1, arg2);
313    }
314
315    public long getBundleId() {
316        return bundle.getBundleId();
317    }
318
319    public URL getEntry(String arg0) {
320        return bundle.getEntry(arg0);
321    }
322
323    public Enumeration getEntryPaths(String arg0) {
324        return bundle.getEntryPaths(arg0);
325    }
326
327    public Dictionary getHeaders() {
328        return bundle.getHeaders();
329    }
330
331    public Dictionary getHeaders(String arg0) {
332        return bundle.getHeaders(arg0);
333    }
334
335    public long getLastModified() {
336        return bundle.getLastModified();
337    }
338
339    public String getLocation() {
340        return bundle.getLocation();
341    }
342
343    public ServiceReference[] getRegisteredServices() {
344        return bundle.getRegisteredServices();
345    }
346
347    public ServiceReference[] getServicesInUse() {
348        return bundle.getServicesInUse();
349    }
350
351    public Map getSignerCertificates(int arg0) {
352        return bundle.getSignerCertificates(arg0);
353    }
354
355    public int getState() {
356        return bundle.getState();
357    }
358
359    public String getSymbolicName() {
360        return bundle.getSymbolicName();
361    }
362
363    public Version getVersion() {
364        return bundle.getVersion();
365    }
366
367    public boolean hasPermission(Object arg0) {
368        return bundle.hasPermission(arg0);
369    }
370
371    public void start() throws BundleException {
372        bundle.start();
373    }
374
375    public void start(int arg0) throws BundleException {
376        bundle.start(arg0);
377    }
378
379    public void stop() throws BundleException {
380        bundle.stop();
381    }
382
383    public void stop(int arg0) throws BundleException {
384        bundle.stop(arg0);
385    }
386
387    public void uninstall() throws BundleException {
388        bundle.uninstall();
389    }
390
391    public void update() throws BundleException {
392        bundle.update();
393    }
394
395    public void update(InputStream arg0) throws BundleException {
396        bundle.update(arg0);
397    }
398
399    public int compareTo(Bundle other) {
400        return bundle.compareTo(other);
401    }
402
403    public <A> A adapt(Class<A> type) {
404        return bundle.adapt(type);
405    }
406
407    public File getDataFile(String filename) {
408        return bundle.getDataFile(filename);
409    }
410    
411    public String toString() {
412        return "[DelegatingBundle: " + bundles + "]";
413    }
414    
415    private static class Cache<K, V> extends LinkedHashMap<K, V> {
416        
417        private final int maxSize;
418        
419        public Cache(int maxSize) {
420            this(16, maxSize, 0.75f);
421        }
422        
423        public Cache(int initialSize, int maxSize, float loadFactor) {
424            super(initialSize, loadFactor, true);
425            this.maxSize = maxSize;
426        }
427        
428        @Override   
429        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
430            if (size() > maxSize) {
431                return true;
432            } else {
433                return false;
434            }
435        }
436    }
437
438}