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 */
017
018package org.apache.xbean.finder;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URL;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import org.apache.xbean.osgi.bundle.util.BundleClassFinder;
028import org.apache.xbean.osgi.bundle.util.BundleDescription;
029import org.apache.xbean.osgi.bundle.util.ClassDiscoveryFilter;
030import org.objectweb.asm.ClassReader;
031import org.objectweb.asm.Opcodes;
032import org.osgi.framework.Bundle;
033import org.osgi.service.packageadmin.ExportedPackage;
034import org.osgi.service.packageadmin.PackageAdmin;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * @version $Rev: 941557 $ $Date: 2010-05-06 03:16:59 +0200 (jeu. 06 mai 2010) $
040 */
041public class BundleAssignableClassFinder extends BundleClassFinder {
042
043    private static final Logger logger = LoggerFactory.getLogger(BundleAssignableClassFinder.class);
044
045    private Class<?>[] clses;
046
047    private Set<String> targetClassNames = new HashSet<String>();
048
049    private Set<String> targetInterfaceNames = new HashSet<String>();
050
051    private Set<String> wiredImportedPackageNames = new HashSet<String>();
052
053    /**
054     * Create a new BundleClassFinder, it will search all the classes based the rule defined by the parameters via ASM tool
055     * @param packageAdmin
056     * @param bundle
057     * @param clses
058     * @param discoveryFilter
059     */
060    public BundleAssignableClassFinder(PackageAdmin packageAdmin, Bundle bundle, Class<?>[] clses, ClassDiscoveryFilter discoveryFilter) {
061        super(packageAdmin, bundle, discoveryFilter);
062        if (clses == null || clses.length == 0) {
063            throw new IllegalArgumentException("At least one class or interface should be specified");
064        }
065        this.clses = clses;
066        for (Class<?> cls : clses) {
067            String asmStyleName = cls.getName().replace('.', '/');
068            if (cls.isInterface()) {
069                targetInterfaceNames.add(asmStyleName);
070            } else {
071                targetClassNames.add(asmStyleName);
072            }
073        }
074        initialize();
075    }
076
077    public BundleAssignableClassFinder(PackageAdmin packageAdmin, Class<?>[] clses, Bundle bundle) {
078        this(packageAdmin, bundle, clses, FULL_CLASS_DISCOVERY_FILTER);
079    }
080
081    @Override
082    protected BundleClassFinder createSubBundleClassFinder(PackageAdmin packageAdmin, Bundle bundle, ClassDiscoveryFilter classDiscoveryFilter) {
083        return new BundleAssignableClassFinder(packageAdmin, bundle, clses, classDiscoveryFilter);
084    }
085
086    @Override
087    protected boolean isClassAcceptable(String name, InputStream in) throws IOException {
088        ClassReader classReader = new ClassReader(in);
089        String className = classReader.getClassName();
090        if ((classReader.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
091            if (targetClassNames.contains(className)) {
092                return true;
093            }
094        } else {
095            if (targetInterfaceNames.contains(className)) {
096                return true;
097            }
098        }
099        String[] interfaceNames = classReader.getInterfaces();
100        try {
101            for (String interfaceName : interfaceNames) {
102                if (wiredImportedPackageNames.contains(toASMStylePackageName(interfaceName))) {
103                    return isClassAssignable(bundle.loadClass(toJavaStyleClassName(interfaceName)));
104                } else {
105                    if (isInterfaceAssignable(interfaceName)) {
106                        return true;
107                    }
108                }
109            }
110            String superClassName = classReader.getSuperName();
111            if (wiredImportedPackageNames.contains(toASMStylePackageName(superClassName))) {
112                return isClassAssignable(bundle.loadClass(toJavaStyleClassName(superClassName)));
113            }
114            return isSuperClassAssignable(superClassName);
115        } catch (ClassNotFoundException e) {
116            return false;
117        }
118    }
119
120    @Override
121    protected boolean isClassAcceptable(URL url) {
122        InputStream in = null;
123        try {
124            in = url.openStream();
125            return isClassAcceptable("", in);
126        } catch (IOException e) {
127            logger.warn("Unable to check the class of url " + url, e);
128            return false;
129        } finally {
130            if (in != null)
131                try {
132                    in.close();
133                } catch (Exception e) {
134                }
135        }
136    }
137
138    private void initialize() {
139        BundleDescription description = new BundleDescription(bundle.getHeaders());
140        List<BundleDescription.ImportPackage> imports = description.getExternalImports();
141        for (BundleDescription.ImportPackage packageImport : imports) {
142            String packageName = packageImport.getName();
143            ExportedPackage[] exports = packageAdmin.getExportedPackages(packageName);
144            Bundle wiredBundle = isWired(bundle, exports);
145            if (wiredBundle != null) {
146                wiredImportedPackageNames.add(packageName.replace('.', '/'));
147                break;
148            }
149        }
150    }
151
152    private boolean isClassAssignable(Class<?> cls) {
153        for (Class<?> targetClass : clses) {
154            if (targetClass.isAssignableFrom(cls)) {
155                return true;
156            }
157        }
158        return false;
159    }
160
161    /**
162     *
163     * @param interfaceName The interface name should be in the format of org/test/SimpleInterface
164     * @return return true if the method parameter interfaceName is assignable to any interface in the expected interfaces
165     */
166    private boolean isInterfaceAssignable(String interfaceName) {
167        //Check each interface in interfaceNames set
168        if (targetInterfaceNames.contains(interfaceName)) {
169            return true;
170        }
171        //Check ancestor intefaces
172        URL url = bundle.getResource("/" + interfaceName + ".class");
173        if (url == null) {
174            //TODO what should we do if we do not find the interface ?
175            return false;
176        }
177        InputStream in = null;
178        try {
179            in = url.openStream();
180            ClassReader classReader = new ClassReader(in);
181            String[] superInterfaceNames = classReader.getInterfaces();
182            for (String superInterfaceName : superInterfaceNames) {
183                if (isInterfaceAssignable(superInterfaceName)) {
184                    return true;
185                }
186            }
187            return false;
188        } catch (IOException e) {
189            logger.warn("Unable to check the interface " + interfaceName, e);
190            return false;
191        } finally {
192            if (in != null)
193                try {
194                    in.close();
195                } catch (Exception e) {
196                }
197        }
198    }
199
200    /**
201     *
202     * @param superClassName The super class name should be in the format of org/test/SimpleClass
203     * @return return true if the method parameter superClassName  is assignable to any interface in the expected interfaces or any class in the expected classes
204     */
205    private boolean isSuperClassAssignable(String superClassName) {
206        if (targetClassNames.contains(superClassName)) {
207            return true;
208        }
209        //Check parent class
210        URL url = bundle.getResource("/" + superClassName + ".class");
211        if (url == null) {
212            //TODO what should we do if we do not find the super class ?
213            return false;
214        }
215        InputStream in = null;
216        try {
217            in = url.openStream();
218            ClassReader classReader = new ClassReader(in);
219            String[] superInterfaceNames = classReader.getInterfaces();
220            //Check interfaces
221            for (String superInterfaceName : superInterfaceNames) {
222                if (isInterfaceAssignable(superInterfaceName)) {
223                    return true;
224                }
225            }
226            //Check className
227            if (classReader.getSuperName().equals("java/lang/Object")) {
228                return targetClassNames.contains("java/lang/Object");
229            } else {
230                return isSuperClassAssignable(classReader.getSuperName());
231            }
232        } catch (IOException e) {
233            logger.warn("Unable to check the super class  " + superClassName, e);
234            return false;
235        } finally {
236            if (in != null)
237                try {
238                    in.close();
239                } catch (Exception e) {
240                }
241        }
242    }
243
244    /**
245     * Get the ASM style package name from the parameter className.
246     * If the className is ended with .class extension, e.g.  /org/apache/geronimo/TestCass.class or org.apache.geronimo.TestClass.class,
247     *      then org/apache/geronimo is returned
248     * If the className is not ended with .class extension, e.g.  /org/apache/geronimo/TestCass or org.apache.geronimo.TestClass,
249     *      then org/apache/geronimo is returned
250     * @param className
251     * @return ASM style package name, should be in the format of  "org/apache/geronimo"
252     */
253    protected String toASMStylePackageName(String className) {
254        if (className.endsWith(EXT)) {
255            className = className.substring(0, className.length() - EXT.length());
256        }
257        className = className.replace('.', '/');
258        int iLastDotIndex = className.lastIndexOf('/');
259        if (iLastDotIndex != -1) {
260            return className.substring(0, iLastDotIndex);
261        } else {
262            return "";
263        }
264    }
265}