| /* |
| * 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.felix.ipojo.extender.internal.processor; |
| |
| import org.apache.felix.ipojo.configuration.Instance; |
| import org.apache.felix.ipojo.extender.internal.BundleProcessor; |
| import org.apache.felix.ipojo.extender.internal.Extender; |
| import org.apache.felix.ipojo.extender.internal.declaration.DefaultInstanceDeclaration; |
| import org.apache.felix.ipojo.extender.internal.declaration.DefaultTypeDeclaration; |
| import org.apache.felix.ipojo.util.InvocationResult; |
| import org.apache.felix.ipojo.util.Log; |
| import org.objectweb.asm.ClassReader; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.wiring.BundleWiring; |
| |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.net.URL; |
| import java.util.*; |
| |
| import static org.apache.felix.ipojo.util.Reflection.fields; |
| import static org.apache.felix.ipojo.util.Reflection.methods; |
| import static org.apache.felix.ipojo.util.StreamUtils.closeQuietly; |
| |
| /** |
| * Processor looking for classes annotated with @Configuration and creating the corresponding instance declaration. |
| */ |
| public class ConfigurationProcessor implements BundleProcessor { |
| |
| /** |
| * The logger. |
| */ |
| private final Log m_logger; |
| |
| /** |
| * Registry storing the bundle to components and instances declared within this bundle. |
| * Only instances are expected. |
| */ |
| private final Map<Bundle, ComponentsAndInstances> m_registry = new HashMap<Bundle, ComponentsAndInstances>(); |
| |
| /** |
| * Set to false to disable this processor. |
| * When an OSGi framework does not provide the wiring API, this processor is disabled. |
| */ |
| private final boolean m_enabled; |
| |
| /** |
| * Creates the configuration processor. |
| * |
| * @param logger the logger. |
| */ |
| public ConfigurationProcessor(Log logger) { |
| |
| m_logger = logger; |
| |
| // org.osgi.framework.wiring may not be available, in this case, disable us. |
| try { |
| this.getClass().getClassLoader().loadClass("org.osgi.framework.wiring.BundleWiring"); |
| } catch (ClassNotFoundException e) { |
| m_logger.log(Log.ERROR, "The org.osgi.framework.wiring.BundleWiring class is not provided by the " + |
| "framework, configuration processor disabled."); |
| m_enabled = false; |
| return; |
| } |
| // Check ok. |
| m_enabled = true; |
| } |
| |
| public static String getClassNameFromResource(String resource) { |
| String res = resource; |
| if (resource.startsWith("/")) { |
| res = resource.substring(1); // Remove the first / |
| } |
| res = res.substring(0, res.length() - ".class".length()); // Remove the .class |
| return res.replace("/", "."); |
| } |
| |
| /** |
| * A bundle is starting. |
| * |
| * @param bundle the bundle |
| */ |
| public void activate(Bundle bundle) { |
| if (! m_enabled && Extender.getIPOJOBundleContext().getBundle().getBundleId() == bundle.getBundleId()) { |
| // Fast return if the configuration tracking is disabled, or if we are iPOJO |
| return; |
| } |
| |
| // It's not required to process bundle not importing the configuration package. |
| final String imports = bundle.getHeaders().get(Constants.IMPORT_PACKAGE); |
| if (imports == null || ! imports.contains("org.apache.felix.ipojo.configuration")) { |
| // TODO Check dynamic imports to verify if the package is not imported lazily. |
| return; |
| } |
| |
| BundleWiring wiring = bundle.adapt(BundleWiring.class); |
| if (wiring == null) { |
| // Invalid state. |
| m_logger.log(Log.ERROR, "The bundle " + bundle.getBundleId() + " (" + bundle.getSymbolicName() + ") " + |
| "cannot be adapted to BundleWiring, state: " + bundle.getState()); |
| return; |
| } |
| |
| // Only lookup for local classes, parent classes will be analyzed on demand. |
| Collection<String> resources = wiring.listResources("/", "*.class", |
| BundleWiring.FINDENTRIES_RECURSE + BundleWiring.LISTRESOURCES_LOCAL); |
| if (resources == null) { |
| m_logger.log(Log.ERROR, "The bundle " + bundle.getBundleId() + " (" + bundle.getSymbolicName() + ") " + |
| " does not have any classes to be analyzed"); |
| return; |
| } |
| m_logger.log(Log.DEBUG, resources.size() + " classes found"); |
| handleResources(bundle, resources, wiring.getClassLoader()); |
| } |
| |
| /** |
| * A bundle is stopping. |
| * |
| * @param bundle the bundle |
| */ |
| public void deactivate(Bundle bundle) { |
| if (! m_enabled) { return; } |
| |
| ComponentsAndInstances cai = m_registry.remove(bundle); |
| if (cai != null) { |
| cai.stop(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc BundleProcessor#start} |
| */ |
| public void start() { |
| // Nothing to do |
| } |
| |
| /** |
| * {@inheritDoc BundleProcessor#stop} |
| * <p/> |
| * This method cleans up all created factories and instances. |
| */ |
| public void stop() { |
| // Ignored, for a simple ordered shutdown, use ReverseBundleProcessor |
| } |
| |
| private void handleResources(Bundle bundle, Collection<String> resources, ClassLoader classLoader) { |
| for (String resource : resources) { |
| handleResource(bundle, resource, classLoader); |
| } |
| } |
| |
| private void handleResource(Bundle bundle, String resource, ClassLoader classLoader) { |
| URL url = classLoader.getResource(resource); |
| if (url == null) { |
| m_logger.log(Log.ERROR, "The resource " + resource + " cannot be loaded by " + bundle.getBundleId() + " " + |
| "(" + bundle.getSymbolicName() + ")"); |
| return; |
| } |
| |
| try { |
| if (hasConfigurationAnnotation(bundle, url, classLoader)) { |
| instantiateAndDeclareInstances(bundle, resource, classLoader); |
| } |
| } catch (IOException e) { |
| m_logger.log(Log.ERROR, "The resource " + resource + " cannot be loaded by " + bundle.getBundleId() + " " + |
| "(" + bundle.getSymbolicName() + ")", e); |
| } |
| |
| } |
| |
| private void instantiateAndDeclareInstances(Bundle bundle, String resource, ClassLoader classLoader) { |
| String classname = getClassNameFromResource(resource); |
| List<Instance> instances = new ArrayList<Instance>(); |
| try { |
| Class clazz = classLoader.loadClass(classname); |
| Object configuration = clazz.newInstance(); |
| |
| // Collect fields |
| Map<Field, Instance> fields = |
| fields().ofType(Instance.class).in(configuration).map(); |
| for (Map.Entry<Field, Instance> entry : fields.entrySet()) { |
| Instance instance = entry.getValue(); |
| instance.nameIfUnnamed(entry.getKey().getName()) |
| .with("instance.bundle.context").setto(bundle.getBundleContext()); |
| instances.add(instance); |
| } |
| |
| // Collect methods with Bundle Context as argument |
| Map<Method, InvocationResult<Instance>> methods = |
| methods().in(configuration).ofReturnType(Instance.class).withParameter(BundleContext.class) |
| .map(bundle.getBundleContext()); |
| |
| // Collect methods without arguments |
| methods.putAll(methods().in(configuration).ofReturnType(Instance.class).map()); |
| |
| for (Map.Entry<Method, InvocationResult<Instance>> entry : methods.entrySet()) { |
| Instance instance = entry.getValue().get(); |
| if (instance == null) { |
| m_logger.log(Log.ERROR, "The Instance declaration creation failed because the method " + entry |
| .getKey().getName() + " of class " + entry.getKey().getDeclaringClass() + " threw an " + |
| "exception", entry.getValue().error()); |
| } else { |
| instance |
| .nameIfUnnamed(entry.getKey().getName()) |
| .with("instance.bundle.context").setto(bundle.getBundleContext()); |
| instances.add(instance); |
| } |
| } |
| |
| } catch (ClassNotFoundException e) { |
| m_logger.log(Log.ERROR, "Cannot load class " + classname + " despite it was considered as a " + |
| "configuration", e); |
| return; |
| } catch (InstantiationException e) { |
| m_logger.log(Log.ERROR, "Cannot instantiate class " + classname + " despite it was considered as a " + |
| "configuration", e); |
| return; |
| } catch (IllegalAccessException e) { |
| m_logger.log(Log.ERROR, "Cannot instantiate class " + classname + " despite it was considered as a " + |
| "configuration", e); |
| return; |
| } |
| |
| m_logger.log(Log.WARNING, instances.size() + " instances found in class " + classname); |
| //Build and enqueue declaration |
| for (Instance instance : instances) { |
| DefaultInstanceDeclaration declaration = new DefaultInstanceDeclaration(bundle.getBundleContext(), |
| instance.factory(), instance.configuration()); |
| declaration.start(); |
| getComponentsAndInstances(bundle).m_instances.add(declaration); |
| } |
| } |
| |
| private boolean hasConfigurationAnnotation(Bundle bundle, URL url, ClassLoader classLoader) throws IOException { |
| InputStream is = url.openStream(); |
| |
| try { |
| // Exclude inner classes and classes containing $ |
| if (url.toExternalForm().contains("$")) { |
| return false; |
| } |
| |
| ClassReader reader = new ClassReader(is); |
| ConfigurationAnnotationScanner scanner = new ConfigurationAnnotationScanner(); |
| reader.accept(scanner, 0); |
| |
| // Only class with the @Configuration are considered, parent classes are not analyzed. |
| // Indeed, we have to detect when the parent must be considered independently, |
| // or when only the daughter needs too (to avoid creating the instances twice). |
| |
| return scanner.isConfiguration(); |
| |
| // The following code would traverse the whole class hierarchy. |
| // if (scanner.isConfiguration()) { |
| // return true; |
| // } else if (scanner.getParent() != null) { |
| // URL parentUrl = classLoader.getResource("/" + scanner.getParent().replace(".", "/") + ".class"); |
| // if (parentUrl == null) { |
| // m_logger.log(Log.DEBUG, "Cannot load the parent class " + scanner.getParent() + " - stopping " + |
| // "scan"); |
| // return false; |
| // } |
| // return hasConfigurationAnnotation(bundle, parentUrl, classLoader); |
| // } |
| } finally { |
| closeQuietly(is); |
| } |
| } |
| |
| /** |
| * Gets the {@link ComponentsAndInstances} declared by the given bundle. |
| * |
| * @param bundle the bundle |
| * @return the set of component and instances declared by the bundle, <code>null</code> otherwise |
| */ |
| private ComponentsAndInstances getComponentsAndInstances(Bundle bundle) { |
| ComponentsAndInstances cai = m_registry.get(bundle); |
| if (cai == null) { |
| cai = new ComponentsAndInstances(); |
| m_registry.put(bundle, cai); |
| } |
| return cai; |
| } |
| |
| /** |
| * Container storing the components and instances declared by a bundle. |
| * This class is not intended to be used outside from the current processor. |
| */ |
| private static class ComponentsAndInstances { |
| List<DefaultTypeDeclaration> m_types = new ArrayList<DefaultTypeDeclaration>(); |
| List<DefaultInstanceDeclaration> m_instances = new ArrayList<DefaultInstanceDeclaration>(); |
| |
| /** |
| * Stops all declarations. |
| */ |
| void stop() { |
| for (DefaultInstanceDeclaration instance : m_instances) { |
| instance.stop(); |
| } |
| for (DefaultTypeDeclaration declaration : m_types) { |
| declaration.stop(); |
| } |
| m_instances.clear(); |
| m_types.clear(); |
| } |
| } |
| |
| |
| } |