/* * Copyright 2016 ThoughtWorks, Inc. * * Licensed 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 com.thoughtworks.go.plugin.activation; import com.thoughtworks.go.plugin.api.GoPluginApiMarker; import com.thoughtworks.go.plugin.api.annotation.Extension; import com.thoughtworks.go.plugin.api.annotation.Load; import com.thoughtworks.go.plugin.api.annotation.UnLoad; import com.thoughtworks.go.plugin.api.info.PluginContext; import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.internal.api.LoggingService; import com.thoughtworks.go.plugin.internal.api.PluginHealthService; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.util.*; /* Added as a part of the go-plugin-activator dependency JAR into each plugin. * Responsible for loading all classes in the plugin which are not in the dependency directory and registering services for the plugin extensions. */ public class DefaultGoPluginActivator implements GoPluginActivator { private List<String> errors = new ArrayList<>(); private List<UnloadMethodInvoker> unloadMethodInvokers = new ArrayList<>(); private PluginHealthService pluginHealthService; private static String pluginId; private static PluginContext DUMMY_PLUGIN_CONTEXT = new PluginContext() { }; @Override public void start(BundleContext bundleContext) throws Exception { Bundle bundle = bundleContext.getBundle(); pluginId = bundle.getSymbolicName(); pluginHealthService = bundleContext.getService(bundleContext.getServiceReference(PluginHealthService.class)); LoggingService loggingService = bundleContext.getService(bundleContext.getServiceReference(LoggingService.class)); Logger.initialize(loggingService); getImplementersAndRegister(bundleContext, bundle); reportErrorsToHealthService(); } private void reportErrorsToHealthService() { if (!errors.isEmpty()) { pluginHealthService.reportErrorAndInvalidate(pluginId, errors); } } private void reportWarningToHealthService(String message) { pluginHealthService.warning(pluginId, message); } //invoked using reflection boolean hasErrors() { return !errors.isEmpty(); } @Override public void stop(BundleContext bundleContext) throws Exception { for (UnloadMethodInvoker unloadMethodInvoker : unloadMethodInvokers) { try { unloadMethodInvoker.invokeUnloadMethod(); } catch (InvocationTargetException e) { errors.add(String.format("Invocation of unload method [%s]. Reason: %s.", unloadMethodInvoker.unloadMethod, e.getTargetException().toString())); } catch (Throwable e) { errors.add(String.format("Invocation of unload method [%s]. Reason: [%s].", unloadMethodInvoker.unloadMethod, e.toString())); } reportErrorsToHealthService(); } } void getImplementersAndRegister(BundleContext bundleContext, Bundle bundle) throws ClassNotFoundException { List<HashMap<Class, List<Object>>> toRegister = new ArrayList<>(); for (Class candidateGoExtensionClass : getCandidateGoExtensionClasses(bundle)) { HashMap<Class, List<Object>> interfaceToImplementations = getAllInterfaceToImplementationsMap(candidateGoExtensionClass); if (!interfaceToImplementations.isEmpty()) { toRegister.add(interfaceToImplementations); } } informIfNoExtensionFound(toRegister); registerAllServicesImplementedBy(bundleContext, toRegister); } private void informIfNoExtensionFound(List<HashMap<Class, List<Object>>> toRegister) { if(toRegister.isEmpty()){ errors.add("No extensions found in this plugin.Please check for @Extension annotations"); } } private void registerAllServicesImplementedBy(BundleContext bundleContext, List<HashMap<Class, List<Object>>> toRegister) { for (HashMap<Class, List<Object>> classListHashMap : toRegister) { for (Map.Entry<Class, List<Object>> entry : classListHashMap.entrySet()) { Class serviceInterface = entry.getKey(); for (Object serviceImplementation : entry.getValue()) { Hashtable<String, String> serviceProperties = new Hashtable<>(); serviceProperties.put(Constants.BUNDLE_SYMBOLICNAME, pluginId); bundleContext.registerService(serviceInterface, serviceImplementation, serviceProperties); } } } } private HashMap<Class, List<Object>> getAllInterfaceToImplementationsMap(Class candidateGoExtensionClass) { HashMap<Class, List<Object>> interfaceAndItsImplementations = new HashMap<>(); Set<Class> interfaces = findAllInterfacesInHierarchy(candidateGoExtensionClass); Object implementation = createImplementationOf(candidateGoExtensionClass); if (implementation == null) { return interfaceAndItsImplementations; } for (Class anInterface : interfaces) { if (isGoExtensionPointInterface(anInterface)) { List<Object> implementations = interfaceAndItsImplementations.get(anInterface); if (implementations == null) { implementations = new ArrayList<>(); interfaceAndItsImplementations.put(anInterface, implementations); } implementations.add(implementation); } } return interfaceAndItsImplementations; } private Object createImplementationOf(Class candidateGoExtensionClass) { Object implementation = null; try { implementation = createInstance(candidateGoExtensionClass); } catch (InvocationTargetException e) { errors.add(String.format("Class [%s] is annotated with @Extension but cannot be constructed. Reason: %s.", candidateGoExtensionClass.getSimpleName(), e.getTargetException().toString())); } catch (Throwable e) { errors.add(String.format("Class [%s] is annotated with @Extension but cannot be constructed. Reason: [%s].", candidateGoExtensionClass.getSimpleName(), e.getCause())); } if (implementation != null) { validateAndLoad(candidateGoExtensionClass, implementation); } return implementation; } private void validateAndLoad(Class candidateGoExtensionClass, Object implementation) { try { processLoadAndUnloadAnnotatedMethods(implementation); } catch (InvocationTargetException e) { errors.add(String.format("Class [%s] is annotated with @Extension but cannot be registered. Reason: %s.", candidateGoExtensionClass.getSimpleName(), e.getTargetException().toString())); } catch (IllegalAccessException e) { errors.add(String.format("Class [%s] is annotated with @Extension will not be registered. Reason: %s.", candidateGoExtensionClass.getSimpleName(), e.toString())); } catch (RuntimeException e) { errors.add(String.format("Class [%s] is annotated with @Extension will not be registered. Reason: %s.", candidateGoExtensionClass.getSimpleName(), e.toString())); } catch (Throwable e) { errors.add(String.format("Class [%s] is annotated with @Extension but cannot be constructed or registered. Reason: [%s].", candidateGoExtensionClass.getSimpleName(), e.getCause())); } } private void processLoadAndUnloadAnnotatedMethods(Object implementation) throws InvocationTargetException, IllegalAccessException { Method loadAnnotatedMethod = getAnnotatedMethod(implementation, Load.class); Method unloadAnnotatedMethod = getAnnotatedMethod(implementation, UnLoad.class); if (loadAnnotatedMethod != null) { loadAnnotatedMethod.invoke(implementation, DUMMY_PLUGIN_CONTEXT); } if (unloadAnnotatedMethod != null) { this.unloadMethodInvokers.add(new UnloadMethodInvoker(implementation, unloadAnnotatedMethod)); } } private Method getAnnotatedMethod(Object extensionObject, Class<? extends Annotation> annotation) { Method[] methods = getMethodsWithAnnotation(extensionObject, annotation); if (methods.length == 0) { return null; } if (methods.length > 1) { throw new RuntimeException("More than one method with @" + annotation.getSimpleName() + " annotation not allowed. Methods Found: " + Arrays.toString(methods)); } return methods[0]; } private Method[] getMethodsWithAnnotation(Object extensionObject, Class<? extends Annotation> annotation) { // public,non-static,non-inherited zero-argument with @Load annotation Class<? extends Object> extnPointClass = extensionObject.getClass(); ArrayList<Method> methodsWithLoadAnnotation = new ArrayList<>(); for (Method method : extnPointClass.getDeclaredMethods()) { boolean annotated = hasAnnotation(annotation, method); if (annotated && isPublic(method) && isNonStatic(method) && hasOneArgOfPluginContextType(method)) { methodsWithLoadAnnotation.add(method); } else if (annotated) { reportWarningsForAnnotatedMethod(method, annotation); } } return methodsWithLoadAnnotation.toArray(new Method[methodsWithLoadAnnotation.size()]); } private void reportWarningsForAnnotatedMethod(Method method, Class<? extends Annotation> annotation) { if (!isPublic(method)) { reportWarningToHealthService( String.format("Ignoring method [%s] tagged with @%s since its not 'public'", method, annotation.getSimpleName())); return; } if (!isNonStatic(method)) { reportWarningToHealthService( String.format("Ignoring method [%s] tagged with @%s since its 'static' method", method, annotation.getSimpleName())); return; } if (!hasOneArgOfPluginContextType(method)) { reportWarningToHealthService( String.format("Ignoring method [%s] tagged with @%s since it does not have one argument of type PluginContext. Argument Type: []", method, annotation.getSimpleName(), Arrays.toString(method.getParameterTypes()))); return; } } private boolean hasOneArgOfPluginContextType(Method method) { return method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == PluginContext.class; } private boolean isNonStatic(Method method) { return !Modifier.isStatic(method.getModifiers()); } private boolean isPublic(Method method) { return Modifier.isPublic(method.getModifiers()); } private boolean hasAnnotation(Class<? extends Annotation> annotation, Method method) { return method.getAnnotation(annotation) != null; } private List<Class> getCandidateGoExtensionClasses(Bundle bundle) throws ClassNotFoundException { List<Class> candidateClasses = new ArrayList<>(); Enumeration<URL> entries = bundle.findEntries("/", "*.class", true); while (entries.hasMoreElements()) { String entryPath = entries.nextElement().getFile(); if (isInvalidPath(entryPath)) { continue; } Class<?> candidateClass = loadClass(bundle, entryPath); if (candidateClass != null && isValidClass(candidateClass)) { candidateClasses.add(candidateClass); } } return candidateClasses; } Class<?> loadClass(Bundle bundle, String classFilePath) throws ClassNotFoundException { String className = classFilePath.replaceFirst("^/", "").replace('/', '.').replaceFirst(".class$", ""); try { return bundle.loadClass(className); } catch (Throwable e) { errors.add(String.format("Class [%s] could not be loaded. Message: [%s].", className, e.getMessage())); } return null; } private boolean isInvalidPath(String entryPath) { return entryPath.startsWith("/lib/") || entryPath.startsWith("/META-INF/"); } private boolean isValidClass(Class<?> candidateClass) { try { boolean doesNotHaveExtensionAnnotation = candidateClass.getAnnotation(Extension.class) == null; if (doesNotHaveExtensionAnnotation) { return false; } boolean isAbstract = Modifier.isAbstract(candidateClass.getModifiers()); if (isAbstract) { errors.add(String.format("Class [%s] is annotated with @Extension but is abstract.", candidateClass.getSimpleName())); return false; } boolean isNotPublic = !Modifier.isPublic(candidateClass.getModifiers()); if (isNotPublic) { errors.add(String.format("Class [%s] is annotated with @Extension but is not public.", candidateClass.getSimpleName())); return false; } return isInstantiable(candidateClass); } catch (NoSuchMethodException e) { errors.add(String.format( "Class [%s] is annotated with @Extension but cannot be constructed. Make sure it and all of its parent classes have a default constructor.", candidateClass.getSimpleName())); return false; } } private boolean isInstantiable(Class<?> candidateClass) throws NoSuchMethodException { if (!isANonStaticInnerClass(candidateClass)) { boolean hasPublicDefaultConstructor = candidateClass.getConstructor() != null; return hasPublicDefaultConstructor; } boolean hasAConstructorWhichTakesMyOuterClass = candidateClass.getConstructor(candidateClass.getDeclaringClass()) != null; return hasAConstructorWhichTakesMyOuterClass && isInstantiable(candidateClass.getDeclaringClass()); } private boolean isANonStaticInnerClass(Class<?> candidateClass) { return candidateClass.isMemberClass() && !Modifier.isStatic(candidateClass.getModifiers()); } private Set<Class> findAllInterfacesInHierarchy(Class candidateGoExtensionClass) { Stack<Class> classesInHierarchy = new Stack<>(); classesInHierarchy.add(candidateGoExtensionClass); Set<Class> interfaces = new HashSet<>(); while (!classesInHierarchy.empty()) { Class classToCheckFor = classesInHierarchy.pop(); if (classToCheckFor.isInterface()) { interfaces.add(classToCheckFor); } classesInHierarchy.addAll(Arrays.asList(classToCheckFor.getInterfaces())); if (classToCheckFor.getSuperclass() != null) { classesInHierarchy.add(classToCheckFor.getSuperclass()); } } return interfaces; } private Object createInstance(Class candidateGoExtensionClass) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { if (isANonStaticInnerClass(candidateGoExtensionClass)) { Class declaringClass = candidateGoExtensionClass.getDeclaringClass(); Object declaringClassInstance = createInstance(candidateGoExtensionClass.getDeclaringClass()); return candidateGoExtensionClass.getConstructor(declaringClass).newInstance(declaringClassInstance); } Constructor constructor = candidateGoExtensionClass.getConstructor(); return constructor.newInstance(); } private boolean isGoExtensionPointInterface(Class anInterface) { boolean hasGoPluginApiMarkerAnnotation = anInterface.getAnnotation(GoPluginApiMarker.class) != null; boolean isAnInterfaceWhichHasBeenLeakedFromGoSystemBundle = GoPluginApiMarker.class.getClassLoader() == anInterface.getClassLoader(); return isAnInterfaceWhichHasBeenLeakedFromGoSystemBundle && hasGoPluginApiMarkerAnnotation; } private static class UnloadMethodInvoker { private final Object object; private final Method unloadMethod; UnloadMethodInvoker(Object object, Method unloadMethod) { this.object = object; this.unloadMethod = unloadMethod; } void invokeUnloadMethod() throws InvocationTargetException, IllegalAccessException { this.unloadMethod.invoke(this.object, DUMMY_PLUGIN_CONTEXT); } } }