/*
* RHQ Management Platform
* Copyright (C) 2005-2013 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.core.clientapi.agent.metadata;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.Nullable;
import org.rhq.core.clientapi.descriptor.AgentPluginDescriptorUtil;
import org.rhq.core.clientapi.descriptor.plugin.PluginDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ServerDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ServiceDescriptor;
import org.rhq.core.domain.resource.ClassLoaderType;
import org.rhq.core.domain.resource.ResourceCategory;
import org.rhq.core.domain.resource.ResourceType;
/**
* This is meant to provide an interface to the underlying metadata of a plugin. It will load, translate and cache the
* metadata for the rest of the services in the form of the domain object classes and the jaxb version of the
* descriptors.
*
* This object can also be used to separately store plugin descriptors without converting them into types (i.e.
* the descriptor staging area).
* The thinking here is the server has the ability to get all the plugin descriptors early on and in any order;
* only later does it load/register those plugins (because it needs to order them via the proper dependency graph.
* There may be times when we need a plugin's descriptor but before that plugin has been loaded/registered. This
* manager lets us stage those descriptors prior to converting them into types.
*
* @author Greg Hinkle
* @author John Mazzitelli
*/
public class PluginMetadataManager {
public static final ResourceType TEST_PLATFORM_TYPE = new ResourceType("Anonymous", "test",
ResourceCategory.PLATFORM, null);
static {
TEST_PLATFORM_TYPE.setClassLoaderType(ClassLoaderType.SHARED);
}
private Log log = LogFactory.getLog(PluginMetadataManager.class);
private Map<ResourceCategory, LinkedHashSet<ResourceType>> typesByCategory = new HashMap<ResourceCategory, LinkedHashSet<ResourceType>>();
private Set<ResourceType> types = new HashSet<ResourceType>();
private final Object typesLock = new Object();
private Map<String, PluginMetadataParser> parsersByPlugin = new HashMap<String, PluginMetadataParser>();
private Map<String, PluginDescriptor> descriptorsByPlugin = new HashMap<String, PluginDescriptor>();
// "disabled" types are disabled entirely at initialization time and will be disable for the lifetime of this manager.
// "ignored" types are ignored but can be unignored at a later time.
private List<String> disabledResourceTypesAsStrings = null;
private Map<ResourceType, String> disabledResourceTypes = null;
private Set<ResourceType> ignoredResourceTypes = null;
private final Object disabledIgnoredTypesLock = new Object(); // used when accessing disabled and ignored collections
// these define the discovery callbacks per resource type. The key is the resource type whose discovered details
// need to be funneled through callbacks. The value is a map whose key is plugin names and whose values are
// discovery callback implementation classes defined in the plugins.
private Map<ResourceType, Map<String, List<String>>> discoveryCallbacks = new HashMap<ResourceType, Map<String, List<String>>>();
// similar map for the resource upgrade callbacks
private Map<ResourceType, Map<String, List<String>>> resourceUpgradeCallbacks = new HashMap<ResourceType, Map<String, List<String>>>();
//this lock is shared for filling up both the discoveryCallbacks and resourceUpgradeCallbacks.
private final Object discoveryAndResourceUpgradeCallbacksLock = new Object();
public PluginMetadataManager() {
}
/**
* This will simply squirrel away the given plugin descriptor for later retrieval
* via {@link #getPluginDescriptor(String)}. Use this as a simple storage
* mechanism for descriptors. Nothing is done with descriptor other than store it
* in memory for later retrieval.
* @param descriptor the descriptor to store
*/
public void storePluginDescriptor(PluginDescriptor descriptor) {
this.descriptorsByPlugin.put(descriptor.getName(), descriptor);
}
/**
* Get the plugin descriptor for the named plugin. If the descriptor was previously staged
* via {@link #storePluginDescriptor(PluginDescriptor)}, it will be used. If a new descriptor
* hasn't been staged, but a previous descriptor was loaded and converted into types,
* via {@link #loadPlugin(PluginDescriptor)}, it will be used.
* If the descriptor cannot be found anywhere, returns null.
*
* @param pluginName name of the plugin whose descriptor is to be returned.
* @return the descriptor or null if not available
*/
public PluginDescriptor getPluginDescriptor(String pluginName) {
PluginDescriptor descriptor = this.descriptorsByPlugin.get(pluginName);
if (descriptor == null) {
PluginMetadataParser parser = this.parsersByPlugin.get(pluginName);
if (parser != null) {
descriptor = parser.getDescriptor();
}
}
return descriptor;
}
private void addType(ResourceType type) {
ResourceCategory category = type.getCategory();
synchronized (typesLock) {
if (!typesByCategory.containsKey(category)) {
typesByCategory.put(category, new LinkedHashSet<ResourceType>());
}
typesByCategory.get(category).add(type);
types.add(type);
}
}
/**
* Adds a platform resource type to represent an "anonymous" platform. This should really only ever be used in a
* test scenario when the platform plugin is unavailable.
*
* @return the very thin anonymous resource type object
*/
public ResourceType addTestPlatformType() {
addType(TEST_PLATFORM_TYPE);
return TEST_PLATFORM_TYPE;
}
public String getPluginLifecycleListenerClass(String pluginName) {
PluginMetadataParser parser = this.parsersByPlugin.get(pluginName);
return (parser != null) ? parser.getPluginLifecycleListenerClass() : null;
}
public String getDiscoveryClass(ResourceType resourceType) throws ResourceTypeNotEnabledException {
if (isDisabledOrIgnoredResourceType(resourceType)) {
log.debug("resource type is disabled - rejecting request for discovery class name: " + resourceType);
throw new ResourceTypeNotEnabledException(resourceType);
}
PluginMetadataParser parser = this.parsersByPlugin.get(resourceType.getPlugin());
return (parser != null) ? parser.getDiscoveryComponentClass(resourceType) : null;
}
public String getComponentClass(ResourceType resourceType) throws ResourceTypeNotEnabledException {
if (isDisabledOrIgnoredResourceType(resourceType)) {
log.debug("resource type is disabled - rejecting request for component class name: " + resourceType);
throw new ResourceTypeNotEnabledException(resourceType);
}
PluginMetadataParser parser = this.parsersByPlugin.get(resourceType.getPlugin());
return (parser != null) ? parser.getComponentClass(resourceType) : null;
}
/**
* Transforms the pluginDescriptor into domain object form and stores into this object's type system.
*
* @param pluginDescriptor the descriptor to transform
*
* @return the root resource types represented by this descriptor, or null on failure
*/
public synchronized Set<ResourceType> loadPlugin(PluginDescriptor pluginDescriptor) {
try {
PluginMetadataParser parser = new PluginMetadataParser(pluginDescriptor, parsersByPlugin);
PluginMetadataParser oldParser = this.parsersByPlugin.get(pluginDescriptor.getName());
if (oldParser != null) {
// This is a redeploy, first delete all the original types
synchronized (this.typesLock) {
for (ResourceType oldType : oldParser.getAllTypes()) {
this.typesByCategory.get(oldType.getCategory()).remove(oldType);
this.types.remove(oldType);
}
}
}
this.parsersByPlugin.put(pluginDescriptor.getName(), parser);
this.descriptorsByPlugin.remove(pluginDescriptor.getName()); // don't need it here anymore if its there
synchronized (this.typesLock) {
for (ResourceType resourceType : parser.getAllTypes()) {
if (types.contains(resourceType)) {
throw new InvalidPluginDescriptorException("Type [" + resourceType
+ "] is duplicate for this plugin. This is illegal.");
}
if (resourceType.getName().length() > 100) {
throw new InvalidPluginDescriptorException("Type [" + resourceType
+ "] has longer name than allowed 100 characters.");
}
addType(resourceType);
}
}
synchronized (disabledIgnoredTypesLock) {
findDisabledResourceTypesInAllPlugins();
}
synchronized (discoveryAndResourceUpgradeCallbacksLock) {
// squirrel away all the discovery callbacks
Map<ResourceType, List<String>> discoveryCallbacksMap = parser.getDiscoveryCallbackClasses();
addCallbacks(discoveryCallbacksMap, pluginDescriptor.getName(), discoveryCallbacks);
//and the same for the resource upgrade callbacks
Map<ResourceType, List<String>> resourceUpgradCallbacksMap = parser.getResourceUpgradeCallbackClasses();
addCallbacks(resourceUpgradCallbacksMap, pluginDescriptor.getName(), resourceUpgradeCallbacks);
}
// return the top root types from the descriptor
Set<ResourceType> rootTypes = parser.getRootResourceTypes();
return rootTypes;
} catch (InvalidPluginDescriptorException e) {
// TODO Should we throw back or log partial failures or store them against the definitions?
log.error("Error transforming plugin descriptor [" + pluginDescriptor.getName() + "].", e);
}
return null;
}
/**
* Returns the Resource type with the specified name and plugin, or null if no such Resource type exists.
*
* @param resourceTypeName the Resource type name
* @param pluginName the name of the plugin that defines the Resource type
*
* @return the Resource type with the specified name and plugin, or null if no such Resource type exists
*/
@Nullable
public ResourceType getType(String resourceTypeName, String pluginName) {
ResourceType searchType = new ResourceType(resourceTypeName, pluginName, null, null);
synchronized (this.typesLock) {
for (ResourceType type : types) {
if (type.equals(searchType)) {
return type;
}
}
}
return null;
}
@Nullable
public ResourceType getType(ResourceType resourceType) {
if (TEST_PLATFORM_TYPE.equals(resourceType)) {
return TEST_PLATFORM_TYPE;
}
return getType(resourceType.getName(), resourceType.getPlugin());
}
/**
* Return the Resource types applicable for a category
* @param category ResourceCategory to look up
* @return the types for this category or an empty Set
*/
public Set<ResourceType> getTypesForCategory(ResourceCategory category) {
synchronized (this.typesLock) {
LinkedHashSet<ResourceType> types = this.typesByCategory.get(category);
return (types != null) ? types : new HashSet<ResourceType>();
}
}
public Set<ResourceType> getAllTypes() {
synchronized (this.typesLock) {
return new HashSet<ResourceType>(types);
}
}
public Set<ResourceType> getRootTypes() {
Set<ResourceType> rootTypes = new HashSet<ResourceType>();
for (ResourceType type : getAllTypes()) {
if (type.getParentResourceTypes().size() == 0) {
rootTypes.add(type);
}
}
return rootTypes;
}
public Set<String> getPluginNames() {
return this.parsersByPlugin.keySet();
}
/**
* Builds the dependency graph using all known descriptors.
*
* @return dependency graph
*/
public PluginDependencyGraph buildDependencyGraph() {
PluginDependencyGraph dependencyGraph = new PluginDependencyGraph();
for (PluginDescriptor descriptor : getAllKnownPluginDescriptors().values()) {
AgentPluginDescriptorUtil.addPluginToDependencyGraph(dependencyGraph, descriptor);
}
return dependencyGraph;
}
/**
* Returns a map of plugins and their descriptors where those plugins are child extensions of the given
* parent plugin. The child extensions are those that used the "embedded" plugin extension model (that is,
* those whose types used sourcePlugin attribute in their type metedata).
*
* Note that this will examine all known descriptors, those that were {@link #loadPlugin(PluginDescriptor) loaded}
* and those that were merely {@link #storePluginDescriptor(PluginDescriptor) stored}.
*
* @param parentPlugin the parent plugin
* @return a map of child plugin info where the children are those that extended the given parent plugin.
* If the given parent plugin was not extended by any other plugin, the map will be empty.
*/
public Map<String, PluginDescriptor> getEmbeddedExtensions(String parentPlugin) {
// get all the descriptors we are going to look at
Map<String, PluginDescriptor> allDescriptors = getAllKnownPluginDescriptors();
// look at all the descriptors
Map<String, PluginDescriptor> map = new HashMap<String, PluginDescriptor>();
for (PluginDescriptor descriptor : allDescriptors.values()) {
String pluginName = descriptor.getName();
if (parentPlugin.equals(pluginName)) {
continue; // ignore itself, go on to the next
}
// let's see if any servers extend the parent plugin...
if (doServersExtendParent(descriptor.getServers(), parentPlugin)) {
map.put(pluginName, descriptor);
continue; // no need to keep checking this plugin, go on to the next
}
// if no servers extended the parent plugin, let's check to see if any services do...
if (!map.containsKey(pluginName)) {
if (doServicesExtendParent(descriptor.getServices(), parentPlugin)) {
map.put(pluginName, descriptor);
continue; // no need to keep checking this plugin, go on to the next
}
}
}
return map;
}
private Map<String, PluginDescriptor> getAllKnownPluginDescriptors() {
Map<String, PluginDescriptor> allDescriptors = new HashMap<String, PluginDescriptor>();
allDescriptors.putAll(descriptorsByPlugin);
for (PluginMetadataParser parser : parsersByPlugin.values()) {
allDescriptors.put(parser.getDescriptor().getName(), parser.getDescriptor());
}
return allDescriptors;
}
private boolean doServersExtendParent(List<ServerDescriptor> servers, String parentPlugin) {
if (servers != null && !servers.isEmpty()) {
for (ServerDescriptor serverDescriptor : servers) {
if (doServersExtendParent(serverDescriptor.getServers(), parentPlugin)) {
return true;
}
if (doServicesExtendParent(serverDescriptor.getServices(), parentPlugin)) {
return true;
}
if (parentPlugin.equals(serverDescriptor.getSourcePlugin())) {
return true;
}
}
}
return false;
}
private boolean doServicesExtendParent(List<ServiceDescriptor> services, String parentPlugin) {
if (services != null && !services.isEmpty()) {
for (ServiceDescriptor serviceDescriptor : services) {
if (doServicesExtendParent(serviceDescriptor.getServices(), parentPlugin)) {
return true;
}
if (parentPlugin.equals(serviceDescriptor.getSourcePlugin())) {
return true;
}
}
}
return false;
}
/**
* This will set the given resource types to be the set of types that are to be ignored.
*
* @param ignoredTypes all the types to now ignore - overrides any set of types that previously were ignored
*/
public void setIgnoredResourceTypes(Collection<ResourceType> ignoredTypes) {
synchronized (disabledIgnoredTypesLock) {
if (ignoredTypes == null || ignoredTypes.isEmpty()) {
this.ignoredResourceTypes = null;
} else {
if (this.ignoredResourceTypes == null) {
this.ignoredResourceTypes = new HashSet<ResourceType>(ignoredTypes);
} else {
this.ignoredResourceTypes.clear();
this.ignoredResourceTypes.addAll(ignoredTypes);
}
}
}
}
/**
* This will define resource types that will be disabled.
*
* Owners of this metadata manager need to set this during initialization prior to
* loading any plugins. Calling this after this manager has been initialized will have no effect.
*
* @param disabledTypesAsStrings list of types, in the form "pluginName>parentType>childType"
*/
public void setDisabledResourceTypes(List<String> disabledTypesAsStrings) {
synchronized (disabledIgnoredTypesLock) {
if (disabledTypesAsStrings != null && !disabledTypesAsStrings.isEmpty()) {
this.disabledResourceTypesAsStrings = new ArrayList<String>(disabledTypesAsStrings);
log.info("Will disable the following resource types: " + this.disabledResourceTypesAsStrings);
} else {
this.disabledResourceTypesAsStrings = null;
}
}
return;
}
/**
* Returns true if the given resource type was either disabled at initialization time
* or subsequently ignored. Both have the same effect, a disabled or ignored resource type
* means there should not be resources of that type in inventory.
*
* @param resourceType the type to check
* @return true if no resources of this type should be in inventory; if resourceType is null
* then false is returned
*/
public boolean isDisabledOrIgnoredResourceType(ResourceType resourceType) {
if (resourceType == null) {
return false;
}
synchronized (disabledIgnoredTypesLock) {
// is it ignored?
if (this.ignoredResourceTypes != null) {
if (this.ignoredResourceTypes.contains(resourceType)) {
return true;
}
}
// is it disabled?
if (this.disabledResourceTypes != null) {
if (this.disabledResourceTypes.containsKey(resourceType)) {
return true;
}
}
}
// its neither disabled nor ignored
return false;
}
// finds if type or its children are disabled and if so adds them to the map with their string representation
private void findDisabledResourceTypes(String parentHierarchy, ResourceType type,
// NOTE: we don't synchronize on disabledTypesLock because we ensured our calling context already has the lock
HashMap<ResourceType, String> disabledTypes) {
// this is the current level we are at in the type hierarchy, as written in string form ("plugin>type>type...")
String typeHierarchy = parentHierarchy + '>' + type.getName();
// see if the given type is to be disabled - if so, add it to the map
if (this.disabledResourceTypesAsStrings.contains(typeHierarchy)) {
log.debug("Disabling resource type: " + type);
disabledTypes.put(type, typeHierarchy);
}
// recursively call ourselves to see if any child types are to be disabled
Set<ResourceType> childTypes = type.getChildResourceTypes();
if (childTypes != null) {
for (ResourceType childType : childTypes) {
findDisabledResourceTypes(typeHierarchy, childType, disabledTypes);
}
}
return;
}
private void findDisabledResourceTypesInAllPlugins() {
// NOTE: we don't synchronize on disabledTypesLock because we ensured our calling context already has the lock
if (this.disabledResourceTypesAsStrings != null) {
int totalToBeDisabled = this.disabledResourceTypesAsStrings.size();
// we have to do it all over again over all plugins because we need to support the injection extension model
// that is, <runs-inside> - new plugins coming online might have injected types in previously loaded plugins
HashMap<ResourceType, String> disabledTypes = new HashMap<ResourceType, String>();
for (Map.Entry<String, PluginMetadataParser> entry : parsersByPlugin.entrySet()) {
PluginMetadataParser parser = entry.getValue();
PluginDescriptor pluginDescriptor = parser.getDescriptor();
Set<ResourceType> rootTypes = parser.getRootResourceTypes();
String hierarchyStart = pluginDescriptor.getName();
for (ResourceType rootType : rootTypes) {
findDisabledResourceTypes(hierarchyStart, rootType, disabledTypes);
}
if (disabledTypes.size() == totalToBeDisabled) {
break; // we found them all, no need to look at any more plugins
}
}
this.disabledResourceTypes = (!disabledTypes.isEmpty()) ? disabledTypes : null;
}
return;
}
/**
* Given a resource type, this will return any discovery callbacks that are required to be invoked
* when that resource type is being discovered.
* @param resourceType the type whose discovery callbacks should be returned
* @return the collection of callbacks, grouped by the plugins that defined them (may be null)
*/
public Map<String, List<String>> getDiscoveryCallbacks(ResourceType resourceType) {
synchronized (discoveryAndResourceUpgradeCallbacksLock) {
Map<String, List<String>> map = discoveryCallbacks.get(resourceType);
return map;
}
}
/**
* Given a resource type, this will return any resource upgrade callbacks that are required to be invoked
* when a resource of that resource type is being upgraded.
* @param resourceType the type whose resource upgrade callbacks should be returned
* @return the collection of callbacks, grouped by the plugins that defined them (may be null)
*/
public Map<String, List<String>> getResourceUpgradeCallbacks(ResourceType resourceType) {
synchronized(disabledIgnoredTypesLock) {
return resourceUpgradeCallbacks.get(resourceType);
}
}
private void addCallbacks(Map<ResourceType, List<String>> callbacksMap, String pluginName,
Map<ResourceType, Map<String, List<String>>> targetMap) {
if (callbacksMap != null) {
for (Map.Entry<ResourceType, List<String>> entry : callbacksMap.entrySet()) {
ResourceType resourceType = entry.getKey();
for (String className : entry.getValue()) {
addCallbackClassName(resourceType, pluginName, className, targetMap);
}
}
}
}
private void addCallbackClassName(ResourceType resourceType, String pluginName, String className, Map<ResourceType,
Map<String, List<String>>> callbacks) {
Map<String, List<String>> map = callbacks.get(resourceType);
if (map == null) {
map = new HashMap<String, List<String>>(1);
callbacks.put(resourceType, map);
}
List<String> callbackListForPlugin = map.get(pluginName);
if (callbackListForPlugin == null) {
callbackListForPlugin = new ArrayList<String>(1);
map.put(pluginName, callbackListForPlugin);
}
callbackListForPlugin.add(className);
}
public void cleanupDescriptors() {
descriptorsByPlugin.clear();
for (PluginMetadataParser parser : parsersByPlugin.values()) {
parser.cleanDescriptor();
}
}
}