/* * Copyright 2008-2014 the original author or authors * * 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 org.kaleidofoundry.core.plugin; import static org.kaleidofoundry.core.i18n.InternalBundleHelper.PluginMessageBundle; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import org.kaleidofoundry.core.plugin.model.Plugin; import org.kaleidofoundry.core.plugin.processor.PluginAnnotationProcessor; import org.kaleidofoundry.core.system.JavaSystemHelper; import org.kaleidofoundry.core.util.ReflectionHelper; import org.kaleidofoundry.core.util.StringHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author jraduget */ public class PluginInspector { private static final Logger LOGGER = LoggerFactory.getLogger(PluginInspector.class); private final Set<Plugin<?>> pluginsSet; private final Set<Plugin<?>> pluginImplsSet; private final Set<String> echoMessages; private final String pluginMetaInfPath; private final String pluginImplementationMetaInfPath; /** * load inspector with default plugin meta inf file : * <ul> * <li>META-INF/org.kaleidofoundry.core.plugin</li> * <li>META-INF/org.kaleidofoundry.core.plugin.implementation</li> * </ul> */ public PluginInspector() { pluginsSet = new LinkedHashSet<Plugin<?>>(); pluginImplsSet = new LinkedHashSet<Plugin<?>>(); echoMessages = new LinkedHashSet<String>(); pluginMetaInfPath = getDefaultPluginMetaInfPath(); pluginImplementationMetaInfPath = getDefaultPluginImplementationMetaInfPath(); } /** * load inspector with specified plugin file * * @param pluginMetaInfPath * @param pluginImplementationMetaInfPath */ public PluginInspector(final String pluginMetaInfPath, final String pluginImplementationMetaInfPath) { pluginsSet = new TreeSet<Plugin<?>>(); pluginImplsSet = new TreeSet<Plugin<?>>(); echoMessages = new LinkedHashSet<String>(); this.pluginMetaInfPath = pluginMetaInfPath; this.pluginImplementationMetaInfPath = pluginImplementationMetaInfPath; } /** * @return default full local path (from classpath) for plugin declared file */ public static String getDefaultPluginMetaInfPath() { final StringBuilder path = new StringBuilder(); path.append(PluginConstants.META_PLUGIN_PATH); if (path.length() > 0) { path.append("/"); } path.append(PluginConstants.META_PLUGIN_FILE); return path.toString(); } /** * @return default full local path (from classpath) for plugin implementation declared file */ public static String getDefaultPluginImplementationMetaInfPath() { final StringBuilder path = new StringBuilder(); path.append(PluginConstants.META_PLUGIN_PATH); if (path.length() > 0) { path.append("/"); } path.append(PluginConstants.META_PLUGIN_IMPLEMENTATION_FILE); return path.toString(); } /** * @return plugin MetaInf file path which have been loaded {@link #PluginInspector()} */ public String getPluginMetaInfPath() { return pluginMetaInfPath; } /** * @return plugin implementation MetaInf file path which have been loaded {@link #PluginInspector()} */ public String getPluginImplementationMetaInfPath() { return pluginImplementationMetaInfPath; } /** * @return set of plugin loaded (be sure to call method {@link #loadPluginMetaData()} first. */ public Set<Plugin<?>> getPluginsSet() { return pluginsSet; } /** * @return set of plugin implementation loaded (be sure to call method {@link #loadPluginImplementationMetaData()} first. */ public Set<Plugin<?>> getPluginImplsSet() { return pluginImplsSet; } /** * @return ordered set of messages, feed at load time {@link #loadPluginMetaData()} & {@link #loadPluginImplementationMetaData()} and at * coherence check {@link #checkDeclarations()} */ public Set<String> getEchoMessages() { return echoMessages; } /** * Load and registered plugin meta data,<br/> * The process start by reading text file, containing interface name list of annotated plugin interface<br/> * this file is generate at compile time, by plugin annotation processor {@link PluginAnnotationProcessor} <br/> * None coherence check is done at this step, use <code>checkDeclaration</code> to do so <br/> * * @return unmodifiable set of plugin interface found by annotation processing * @throws PluginRegistryException target cause can be: * <ul> * <li>IOException if problem reading declare plugin interface file</li> * <li>ClassNotFoundException if annotation plugin processor, provide/visit interface name that is not present in classpath</li> * </ul> */ public Set<Plugin<?>> loadPluginMetaData() throws PluginRegistryException { try { final Enumeration<URL> urls = JavaSystemHelper.getResources(pluginMetaInfPath); pluginsSet.clear(); if (urls != null) { while (urls.hasMoreElements()) { final URL url = urls.nextElement(); echoMessages.add(PluginMessageBundle.getMessage("plugin.info.visitor.resource.found", "interfaces", url.getPath())); InputStream resourceInput = null; Reader reader = null; BufferedReader buffReader = null; String line; try { resourceInput = url.openStream(); reader = new InputStreamReader(resourceInput); buffReader = new BufferedReader(reader); line = buffReader.readLine(); while (line != null) { try { if (!StringHelper.isEmpty(line)) { pluginsSet.add(inspectPlugin(Class.forName(line.trim()))); echoMessages.add(PluginMessageBundle.getMessage("plugin.info.visitor.resource.processing", "interface", line)); } line = buffReader.readLine(); } catch (final ClassNotFoundException cnfe) { throw new PluginRegistryException("plugin.error.load.classnotfound", cnfe, pluginMetaInfPath, line); } } } catch (final IOException ioe) { throw new PluginRegistryException("plugin.error.load.ioe", ioe, url.getFile() + "\n" + url.toString(), ioe.getMessage()); } finally { if (buffReader != null) { buffReader.close(); } if (reader != null) { reader.close(); } if (resourceInput != null) { resourceInput.close(); } } } } return Collections.unmodifiableSet(pluginsSet); } catch (final IOException ioe) { throw new PluginRegistryException("plugin.error.load.ioe", ioe, pluginMetaInfPath, ioe.getMessage()); } } /** * load / registered plugin implementation meta data,<br/> * process start by reading text file, containing implementation class name list of annotated plugin impl<br/> * this file is generate at compile time, by plugin annotation processor {@link PluginAnnotationProcessor} <br/> * None coherence check is done at this step, use <code>checkDeclaration</code> to do so <br/> * * @return unmodifiable set of plugin implementation found by annotation processing * @throws PluginRegistryException target cause can be: * <ul> * <li>IOException if problem reading declare plugin class file</li> * <li>ClassNotFoundException if annotation plugin processor, provide/visit class name that is not present in classpath</li> * </ul> */ public Set<Plugin<?>> loadPluginImplementationMetaData() throws PluginRegistryException { try { final Enumeration<URL> urls = JavaSystemHelper.getResources(pluginImplementationMetaInfPath); pluginImplsSet.clear(); if (urls != null) { while (urls.hasMoreElements()) { final URL url = urls.nextElement(); echoMessages.add(PluginMessageBundle.getMessage("plugin.info.visitor.resource.found", "classes", url.getPath())); InputStream resourceInput = null; Reader reader = null; BufferedReader buffReader = null; String line; try { resourceInput = url.openStream(); reader = new InputStreamReader(resourceInput); buffReader = new BufferedReader(reader); line = buffReader.readLine(); while (line != null) { try { pluginImplsSet.add(inspectPluginImpl(Class.forName(line.trim()))); echoMessages.add(PluginMessageBundle.getMessage("plugin.info.visitor.resource.processing", "class", line)); line = buffReader.readLine(); } catch (final ClassNotFoundException cnfe) { throw new PluginRegistryException("plugin.error.load.classnotfound", cnfe, pluginImplementationMetaInfPath, line); } catch (final LinkageError ncfe) { if (LOGGER.isDebugEnabled()) { echoMessages.add(PluginMessageBundle.getMessage("plugin.info.visitor.resource.linkageError", "class", line, ncfe.getMessage())); } // next plugin line = buffReader.readLine(); } } } catch (final IOException ioe) { throw new PluginRegistryException("plugin.error.load.ioe", ioe, url.getFile(), ioe.getMessage()); } finally { if (buffReader != null) { buffReader.close(); } if (reader != null) { reader.close(); } if (resourceInput != null) { resourceInput.close(); } } } } return Collections.unmodifiableSet(pluginImplsSet); } catch (final IOException ioe) { throw new PluginRegistryException("plugin.error.load.ioe", ioe, pluginImplementationMetaInfPath, ioe.getMessage()); } } /** * introspect class input parameter {@link Declare} annotation , and return bean meta data info * * @param classToinspect * @return null if classToinspect not {@link Declare} annotated, or bean meta otherwise * @param <T> */ public <T> Plugin<T> inspectPlugin(final Class<T> classToinspect) { final Declare declarePlugin = classToinspect.getAnnotation(Declare.class); if (declarePlugin != null) { return PluginFactory.create(declarePlugin, classToinspect); } else { return null; } } /** * introspect class input parameter {@link Plugin} annotation , and return bean meta data info * * @param classToinspect * @return null if classToinspect not {@link Plugin} annotated, or bean meta otherwise * @param <T> */ public <T> Plugin<T> inspectPluginImpl(final Class<T> classToinspect) { final Declare declarePluginImpl = classToinspect.getAnnotation(Declare.class); if (declarePluginImpl != null) { return PluginFactory.create(declarePluginImpl, classToinspect); } else { return null; } } /** * coherence check of loaded plugin and pluginImplementation * <ul> * <li>1. check that plugin annotated interface is right an interface and right {@link Declare} annotated * <li>2. check that plugin implementation is right an concrete class and right {@link Declare} annotated * <li>3. check the uniqueness name for {@link Declare}("name") plugin interface usage</li> * <li>4. check the uniqueness name for {@link Declare}("name") plugin implementation class usage</li> * <li>5. implementation class annotated by {@link Declare} have to implements one interface {@link Declare} annotated</li> * </ul> * * @return set of errors (i18n exception instances, code + message) */ public Set<PluginRegistryException> checkDeclarations() { final Set<PluginRegistryException> pluginRegistryExceptions = new LinkedHashSet<PluginRegistryException>(); // 1 ***** plugin interface simple check **************************************************** for (final Plugin<?> plugin : pluginsSet) { if (!plugin.isInterface()) { pluginRegistryExceptions.add(new PluginRegistryException("plugin.error.load.declare.notaninterface", pluginMetaInfPath, plugin.getAnnotatedClass() .getName())); } } // 2 ***** plugin implementation simple check **************************************************** for (final Plugin<?> plugin : pluginImplsSet) { if (!plugin.isClass()) { pluginRegistryExceptions.add(new PluginRegistryException("plugin.error.load.declare.notaconcreteclass", pluginImplementationMetaInfPath, plugin .getAnnotatedClass().getName())); } } // 3 ***** check unique code @Declare(value="") is used for interface ****************************************** final Map<String, List<Plugin<?>>> pluginCountByCode = new HashMap<String, List<Plugin<?>>>(); // organize plugins in map<name, list<plugin>> for (final Plugin<?> plugin : pluginsSet) { List<Plugin<?>> lplugins = pluginCountByCode.get(plugin.getName()); if (lplugins == null) { lplugins = new ArrayList<Plugin<?>>(); pluginCountByCode.put(plugin.getName(), lplugins); } lplugins.add(plugin); } // if one values of the map<name, list<plugin>> has more than one element, conflict name final List<Plugin<?>> nonUniqueNamePlugin = new ArrayList<Plugin<?>>(); for (final Entry<String, List<Plugin<?>>> entry : pluginCountByCode.entrySet()) { if (entry.getValue().size() > 1) { nonUniqueNamePlugin.addAll(entry.getValue()); } } if (!nonUniqueNamePlugin.isEmpty()) { for (final Plugin<?> p : nonUniqueNamePlugin) { pluginRegistryExceptions.add(new PluginRegistryException("plugin.error.load.declare.nonunique.name", p.getName(), p.getAnnotatedClass().getName())); } } // 4 ***** check unique code @Declare(value="") is used for implementation class ******************************* final Map<String, List<Plugin<?>>> pluginImplCountByCode = new HashMap<String, List<Plugin<?>>>(); // organize plugins in map<name, list<plugin>> for (final Plugin<?> plugin : pluginImplsSet) { List<Plugin<?>> lplugins = pluginImplCountByCode.get(plugin.getName()); if (lplugins == null) { lplugins = new ArrayList<Plugin<?>>(); pluginImplCountByCode.put(plugin.getName(), lplugins); } lplugins.add(plugin); } // if one values of the map<name, list<plugin>> has more than one element, conflict name final List<Plugin<?>> nonUniqueNamePluginImpl = new ArrayList<Plugin<?>>(); for (final Entry<String, List<Plugin<?>>> entry : pluginImplCountByCode.entrySet()) { if (entry.getValue().size() > 1) { nonUniqueNamePluginImpl.addAll(entry.getValue()); } } if (!nonUniqueNamePluginImpl.isEmpty()) { for (final Plugin<?> p : nonUniqueNamePluginImpl) { pluginRegistryExceptions.add(new PluginRegistryException("plugin.error.load.declare.nonunique.name", p.getName(), p.getAnnotatedClass().getName())); } } // 5. ***** class annotated must implements one interface @Declare annotated // ***************************************** for (final Plugin<?> plugin : pluginImplsSet) { int pluginDeclareCount = 0; final Set<Class<?>> pluginInterface = ReflectionHelper.getAllInterfaces(plugin.getAnnotatedClass()); for (final Class<?> c : pluginInterface) { if (c.getAnnotation(Declare.class) != null) { pluginDeclareCount++; } } if (pluginDeclareCount == 0) { pluginRegistryExceptions.add(new PluginRegistryException("plugin.error.load.declare.nointerface", plugin.getAnnotatedClass().getName())); } } return pluginRegistryExceptions; } }