/**
* Copyright (c) Codice Foundation
* <p>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p>
* 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
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package org.codice.ddf.ui.admin.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.shiro.SecurityUtils;
import org.codice.ddf.ui.admin.api.plugin.ConfigurationAdminPlugin;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.cm.ManagedServiceFactory;
import org.osgi.service.metatype.AttributeDefinition;
import org.osgi.service.metatype.MetaTypeInformation;
import org.osgi.service.metatype.MetaTypeService;
import org.osgi.service.metatype.ObjectClassDefinition;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Sets;
import ddf.security.permission.KeyValueCollectionPermission;
import ddf.security.permission.KeyValuePermission;
public class ConfigurationAdminExt {
static final String META_TYPE_NAME = "org.osgi.service.metatype.MetaTypeService";
private static final String ENABLED = "enabled";
private static final String ENABLED_CONFIGURATION = "configurations";
private static final String DISABLED_CONFIGURATION = "disabledConfigurations";
private static final String DISABLED_SERVICE_ID = "_disabled";
public static final String MAP_ENTRY_ID = "id";
private static final String MAP_ENTRY_FPID = "fpid";
private static final String MAP_ENTRY_NAME = "name";
private static final String MAP_ENTRY_BUNDLE = "bundle";
private static final String MAP_ENTRY_BUNDLE_NAME = "bundle_name";
private static final String MAP_ENTRY_BUNDLE_LOCATION = "bundle_location";
private static final String MAP_ENTRY_PROPERTIES = "properties";
public static final String MAP_ENTRY_METATYPE = "metatype";
public static final String MAP_ENTRY_CARDINALITY = "cardinality";
private static final String MAP_ENTRY_DEFAULT_VALUE = "defaultValue";
private static final String MAP_ENTRY_DESCRIPTION = "description";
public static final String MAP_ENTRY_TYPE = "type";
private static final String MAP_ENTRY_OPTION_LABELS = "optionLabels";
private static final String MAP_ENTRY_OPTION_VALUES = "optionValues";
public static final String MAP_FACTORY = "factory";
/**
* The implementation of the {@link IdGetter} interface returning the PIDs listed in the meta
* type information.
*
* @see #getPidObjectClasses()
*/
private static final IdGetter PID_GETTER = new IdGetter() {
public String[] getIds(MetaTypeInformation metaTypeInformation) {
return metaTypeInformation.getPids();
}
};
/**
* The implementation of the {@link IdGetter} interface returning the factory PIDs listed in the
* meta type information.
*/
private static final IdGetter FACTORY_PID_GETTER = new IdGetter() {
public String[] getIds(MetaTypeInformation metaTypeInformation) {
return metaTypeInformation.getFactoryPids();
}
};
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationAdminExt.class);
private final ConfigurationAdmin configurationAdmin;
private final Map<String, ServiceTracker> services = new HashMap<String, ServiceTracker>();
private List<ConfigurationAdminPlugin> configurationAdminPluginList;
/**
* @param configurationAdmin
* @throws ClassCastException if {@code service} is not a MetaTypeService instances
*/
public ConfigurationAdminExt(final Object configurationAdmin) {
this.configurationAdmin = (ConfigurationAdmin) configurationAdmin;
}
static Bundle getBundle(final BundleContext bundleContext, final String bundleLocation) {
if (bundleLocation == null) {
return null;
}
Bundle[] bundles = bundleContext.getBundles();
for (int i = 0; i < bundles.length; i++) {
if (bundleLocation.equals(bundles[i].getLocation())) {
return bundles[i];
}
}
return null;
}
BundleContext getBundleContext() {
Bundle cxfBundle = FrameworkUtil.getBundle(ConfigurationAdminExt.class);
if (cxfBundle != null) {
return cxfBundle.getBundleContext();
}
return null;
}
final Configuration getConfiguration(String pid) {
if (pid != null) {
try {
// we use listConfigurations to not create configuration
// objects persistently without the user providing actual
// configuration
String filter = '(' + Constants.SERVICE_PID + '=' + pid + ')';
Configuration[] configs = this.configurationAdmin.listConfigurations(filter);
if (configs != null && configs.length > 0 && isPermittedToViewService(pid)) {
return configs[0];
}
} catch (InvalidSyntaxException ise) {
LOGGER.info("Invalid LDAP filter", ise);
} catch (IOException ioe) {
LOGGER.info("Unable to retrieve list of configurations.", ioe);
}
}
// fallback to no configuration at all
return null;
}
private final Bundle getBoundBundle(Configuration config) {
if (null == config) {
return null;
}
final String location = config.getBundleLocation();
if (null == location) {
return null;
}
final Bundle bundles[] = getBundleContext().getBundles();
for (int i = 0; bundles != null && i < bundles.length; i++) {
if (bundles[i].getLocation()
.equals(location)) {
return bundles[i];
}
}
return null;
}
public List<Map<String, Object>> listServices(String serviceFactoryFilter,
String serviceFilter) {
List<Map<String, Object>> serviceList = null;
List<Map<String, Object>> serviceFactoryList = null;
try {
// Get ManagedService instances
serviceList = getServices(ManagedService.class.getName(), serviceFilter, true);
// Get ManagedService Metatypes
List<Map<String, Object>> metatypeList = addMetaTypeNamesToMap(getPidObjectClasses(),
serviceFilter,
Constants.SERVICE_PID);
// Get ManagedServiceFactory instances
serviceFactoryList = getServices(ManagedServiceFactory.class.getName(),
serviceFilter,
true);
// Get ManagedServiceFactory Metatypes
metatypeList.addAll(addMetaTypeNamesToMap(getFactoryPidObjectClasses(),
serviceFactoryFilter,
ConfigurationAdmin.SERVICE_FACTORYPID));
for (Map<String, Object> service : serviceFactoryList) {
service.put(MAP_FACTORY, true);
for (Map<String, Object> metatype : metatypeList) {
if (metatype.get(MAP_ENTRY_ID) != null && service.get(MAP_ENTRY_ID) != null
&& metatype.get(MAP_ENTRY_ID)
.equals(service.get(MAP_ENTRY_ID))) {
service.putAll(metatype);
}
}
Configuration[] configs = configurationAdmin.listConfigurations(
"(|(service.factoryPid=" + service.get(MAP_ENTRY_ID)
+ ")(service.factoryPid=" + service.get(MAP_ENTRY_ID)
+ "_disabled))");
if (configs != null) {
addConfigurationData(service, configs);
}
}
for (Map<String, Object> service : serviceList) {
service.put(MAP_FACTORY, false);
for (Map<String, Object> metatype : metatypeList) {
if (metatype.get(MAP_ENTRY_ID) != null && service.get(MAP_ENTRY_ID) != null
&& metatype.get(MAP_ENTRY_ID)
.equals(service.get(MAP_ENTRY_ID))) {
service.putAll(metatype);
}
}
Configuration[] configs = configurationAdmin.listConfigurations(
"(" + Constants.SERVICE_PID + "=" + service.get(MAP_ENTRY_ID) + ")");
if (configs != null) {
addConfigurationData(service, configs);
}
}
serviceList.addAll(serviceFactoryList);
} catch (IOException e) {
LOGGER.warn("Unable to obtain list of Configuration objects from ConfigurationAdmin.",
e);
} catch (InvalidSyntaxException e) {
LOGGER.info("Provided LDAP filter is incorrect: {}", serviceFilter, e);
}
if (serviceList != null) {
return serviceList.stream()
.filter(service -> isPermittedToViewService((String) service.get("id")))
.collect(Collectors.toList());
} else {
return new ArrayList<>();
}
}
private void addConfigurationData(Map<String, Object> service, Configuration[] configs) {
for (Configuration config : configs) {
// ignore configuration object if it is invalid
final String pid = config.getPid();
if (!isAllowedPid(pid)) {
continue;
}
Map<String, Object> configData = new HashMap<String, Object>();
configData.put(MAP_ENTRY_ID, pid);
String fpid = config.getFactoryPid();
if (fpid != null) {
configData.put(MAP_ENTRY_FPID, fpid);
}
// insert an entry for the PID
try {
ObjectClassDefinition ocd = getObjectClassDefinition(config);
if (ocd != null) {
configData.put(MAP_ENTRY_NAME, ocd.getName());
} else {
// no object class definition, use plain PID
configData.put(MAP_ENTRY_NAME, pid);
}
} catch (IllegalArgumentException t) {
// Catch exception thrown by getObjectClassDefinition so other configurations
// are displayed
// no object class definition, use plain PID
configData.put(MAP_ENTRY_NAME, pid);
}
final Bundle bundle = getBoundBundle(config);
if (null != bundle) {
configData.put(MAP_ENTRY_BUNDLE, bundle.getBundleId());
configData.put(MAP_ENTRY_BUNDLE_NAME, getName(bundle));
configData.put(MAP_ENTRY_BUNDLE_LOCATION, bundle.getLocation());
}
Map<String, Object> propertiesTable = new HashMap<String, Object>();
Dictionary<String, Object> properties = config.getProperties();
if (properties != null) {
Enumeration<String> keys = properties.keys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
propertiesTable.put(key, properties.get(key));
}
}
// If the configuration property is a password, set its value to "password" so that
// the real password value will be hidden.
List<HashMap<String, Object>> metatypeList =
(List<HashMap<String, Object>>) service.get("metatype");
metatypeList.stream()
.filter(metatype -> AttributeDefinition.PASSWORD == (Integer) metatype.get(
"type"))
.forEach(metatype -> {
String passwordProperty = (String) metatype.get("id");
propertiesTable.put(passwordProperty, "password");
});
configData.put(MAP_ENTRY_PROPERTIES, propertiesTable);
Map<String, Object> pluginDataMap = getConfigurationPluginData(configData.get(
MAP_ENTRY_ID)
.toString(), Collections.unmodifiableMap(configData));
if (pluginDataMap != null && !pluginDataMap.isEmpty()) {
configData.putAll(pluginDataMap);
}
List<Map<String, Object>> configurations = null;
if (service.containsKey(ENABLED_CONFIGURATION)) {
configurations = (List<Map<String, Object>>) service.get(ENABLED_CONFIGURATION);
} else if (service.containsKey(DISABLED_CONFIGURATION)) {
configurations = (List<Map<String, Object>>) service.get(DISABLED_CONFIGURATION);
} else {
configurations = new ArrayList<Map<String, Object>>();
}
configurations.add(configData);
if (((String) configData.get(MAP_ENTRY_ID)).contains(DISABLED_SERVICE_ID)) {
configData.put(ENABLED, false);
} else {
configData.put(ENABLED, true);
}
service.put(ENABLED_CONFIGURATION, configurations);
}
}
private Map<String, Object> getConfigurationPluginData(String servicePid,
Map<String, Object> dataMap) {
Map<String, Object> allPluginMap = new HashMap<String, Object>();
if (configurationAdminPluginList != null) {
for (ConfigurationAdminPlugin plugin : configurationAdminPluginList) {
Map<String, Object> pluginDataMap = plugin.getConfigurationData(servicePid,
dataMap,
getBundleContext());
allPluginMap.putAll(pluginDataMap);
}
}
return allPluginMap;
}
/**
* Return a display name for the given <code>bundle</code>:
* <ol>
* <li>If the bundle has a non-empty <code>Bundle-Name</code> manifest header that value is
* returned.</li>
* <li>Otherwise the symbolic name is returned if set</li>
* <li>Otherwise the bundle's location is returned if defined</li>
* <li>Finally, as a last resort, the bundles id is returned</li>
* </ol>
*
* @param bundle the bundle which name to retrieve
* @return the bundle name - see the description of the method for more details.
*/
public String getName(Bundle bundle) {
Locale locale = Locale.getDefault();
final String loc = locale == null ? null : locale.toString();
String name = bundle.getHeaders(loc)
.get(Constants.BUNDLE_NAME);
if (name == null || name.length() == 0) {
name = bundle.getSymbolicName();
if (name == null) {
name = bundle.getLocation();
if (name == null) {
name = String.valueOf(bundle.getBundleId());
}
}
}
return name;
}
final boolean isAllowedPid(final String pid) {
if (pid == null) {
return false;
} else {
for (int i = 0; i < pid.length(); i++) {
final char c = pid.charAt(i);
if (c == '&' || c == '<' || c == '>' || c == '"' || c == '\'') {
return false;
}
}
return true;
}
}
public void setConfigurationAdminPluginList(
List<ConfigurationAdminPlugin> configurationAdminPluginList) {
this.configurationAdminPluginList = configurationAdminPluginList;
}
/**
* Returns a map of PIDs and providing bundles of MetaType information. The map is indexed by
* PID and the value of each entry is the bundle providing the MetaType information for that
* PID.
*
* @return see the method description
*/
Map getPidObjectClasses() {
return getObjectClassDefinitions(PID_GETTER);
}
/**
* Returns the <code>ObjectClassDefinition</code> objects for the IDs returned by the
* <code>idGetter</code>. Depending on the <code>idGetter</code> implementation this will be for
* factory PIDs or plain PIDs.
*
* @param idGetter The {@link IdGetter} used to get the list of factory PIDs or PIDs from
* <code>MetaTypeInformation</code> objects.
* @return Map of <code>ObjectClassDefinition</code> objects indexed by the PID (or factory PID)
* to which they pertain
*/
private Map getObjectClassDefinitions(final IdGetter idGetter) {
Locale locale = Locale.getDefault();
final Map objectClassesDefinitions = new HashMap();
final MetaTypeService mts = this.getMetaTypeService();
if (mts != null) {
final Bundle[] bundles = this.getBundleContext()
.getBundles();
for (int i = 0; i < bundles.length; i++) {
final MetaTypeInformation mti = mts.getMetaTypeInformation(bundles[i]);
if (mti != null) {
final String[] idList = idGetter.getIds(mti);
for (int j = 0; idList != null && j < idList.length; j++) {
// After getting the list of PIDs, a configuration might be
// removed. So the getObjectClassDefinition will throw
// an exception, and this will prevent ALL configuration from
// being displayed. By catching it, the configurations will be
// visible
ObjectClassDefinition ocd = null;
try {
ocd = mti.getObjectClassDefinition(idList[j], locale.toString());
} catch (IllegalArgumentException ignore) {
// ignore - just don't show this configuration
}
if (ocd != null) {
objectClassesDefinitions.put(idList[j], ocd);
}
}
}
}
}
return objectClassesDefinitions;
}
ObjectClassDefinition getObjectClassDefinition(Configuration config) {
// if the configuration is bound, try to get the object class
// definition from the bundle installed from the given location
if (config.getBundleLocation() != null) {
Bundle bundle = getBundle(this.getBundleContext(), config.getBundleLocation());
if (bundle != null) {
String id = config.getFactoryPid();
if (null == id) {
id = config.getPid();
}
return getObjectClassDefinition(bundle, id);
}
}
// get here if the configuration is not bound or if no
// bundle with the bound location is installed. We search
// all bundles for a matching [factory] PID
// if the configuration is a factory one, use the factory PID
if (config.getFactoryPid() != null) {
return getObjectClassDefinition(config.getFactoryPid());
}
// otherwise use the configuration PID
return getObjectClassDefinition(config.getPid());
}
ObjectClassDefinition getObjectClassDefinition(Bundle bundle, String pid) {
Locale locale = Locale.getDefault();
if (bundle != null) {
MetaTypeService mts = this.getMetaTypeService();
if (mts != null) {
MetaTypeInformation mti = mts.getMetaTypeInformation(bundle);
if (mti != null) {
// see #getObjectClasses( final IdGetter idGetter, final String locale )
try {
return mti.getObjectClassDefinition(pid, locale.toString());
} catch (IllegalArgumentException e) {
// MetaTypeProvider.getObjectClassDefinition might throw illegal
// argument exception. So we must catch it here, otherwise the
// other configurations will not be shown
// See https://issues.apache.org/jira/browse/FELIX-2390
// https://issues.apache.org/jira/browse/FELIX-3694
}
}
}
}
// fallback to nothing found
return null;
}
MetaTypeService getMetaTypeService() {
return (MetaTypeService) getService(META_TYPE_NAME);
}
/**
* Gets the service with the specified class name. Will create a new {@link ServiceTracker} if
* the service is not already retrieved.
*
* @param serviceName the service name to obtain
* @return the service or <code>null</code> if missing.
*/
final Object getService(String serviceName) {
ServiceTracker serviceTracker = services.get(serviceName);
if (serviceTracker == null) {
serviceTracker = new ServiceTracker(getBundleContext(), serviceName, null);
serviceTracker.open();
services.put(serviceName, serviceTracker);
}
return serviceTracker.getService();
}
ObjectClassDefinition getObjectClassDefinition(String pid) {
Bundle[] bundles = this.getBundleContext()
.getBundles();
for (int i = 0; i < bundles.length; i++) {
try {
ObjectClassDefinition ocd = this.getObjectClassDefinition(bundles[i], pid);
if (ocd != null) {
return ocd;
}
} catch (IllegalArgumentException iae) {
// don't care
}
}
return null;
}
/**
* Returns a map of factory PIDs and providing bundles of MetaType information. The map is
* indexed by factory PID and the value of each entry is the bundle providing the MetaType
* information for that factory PID.
*
* @return see the method description
*/
public Map getFactoryPidObjectClasses() {
return getObjectClassDefinitions(FACTORY_PID_GETTER);
}
List<Map<String, Object>> getServices(String serviceClass, String serviceFilter,
boolean ocdRequired) throws InvalidSyntaxException {
List<Map<String, Object>> serviceList = new ArrayList<Map<String, Object>>();
// find all ManagedServiceFactories to get the factoryPIDs
ServiceReference[] refs = this.getBundleContext()
.getAllServiceReferences(serviceClass, serviceFilter);
for (int i = 0; refs != null && i < refs.length; i++) {
Object pidObject = refs[i].getProperty(Constants.SERVICE_PID);
// only include valid PIDs
if (pidObject instanceof String && isAllowedPid((String) pidObject)) {
String pid = (String) pidObject;
String name = pid;
boolean haveOcd = !ocdRequired;
final ObjectClassDefinition ocd = getObjectClassDefinition(refs[i].getBundle(),
pid);
if (ocd != null) {
name = ocd.getName();
haveOcd = true;
}
if (haveOcd) {
Map<String, Object> service = new HashMap<String, Object>();
service.put(MAP_ENTRY_ID, pid);
service.put(MAP_ENTRY_NAME, name);
serviceList.add(service);
}
}
}
return serviceList.stream()
.filter(service -> isPermittedToViewService((String) service.get("id")))
.collect(Collectors.toList());
}
public List<Map<String, Object>> addMetaTypeNamesToMap(final Map ocdCollection,
final String filterSpec, final String type) {
Filter filter = null;
if (filterSpec != null) {
try {
filter = getBundleContext().createFilter(filterSpec);
} catch (InvalidSyntaxException ignore) {
// don't care
}
}
List<Map<String, Object>> metatypeList = new ArrayList<Map<String, Object>>();
Iterator ei = ocdCollection.entrySet()
.iterator();
while (ei.hasNext()) {
Entry ociEntry = (Entry) ei.next();
final String pid = (String) ociEntry.getKey();
final ObjectClassDefinition ocd = (ObjectClassDefinition) ociEntry.getValue();
if (filter == null) {
Map<String, Object> metatype = new HashMap<String, Object>();
metatype.put(MAP_ENTRY_ID, pid);
metatype.put(MAP_ENTRY_NAME, ocd.getName());
AttributeDefinition[] defs = ocd.getAttributeDefinitions(ObjectClassDefinition.ALL);
metatype.put(MAP_ENTRY_METATYPE, createMetatypeMap(defs));
metatypeList.add(metatype);
} else {
final Dictionary props = new Hashtable();
props.put(type, pid);
if (filter.match(props)) {
Map<String, Object> metatype = new HashMap<String, Object>();
metatype.put(MAP_ENTRY_ID, pid);
metatype.put(MAP_ENTRY_NAME, ocd.getName());
AttributeDefinition[] defs =
ocd.getAttributeDefinitions(ObjectClassDefinition.ALL);
metatype.put(MAP_ENTRY_METATYPE, createMetatypeMap(defs));
metatypeList.add(metatype);
}
}
}
return metatypeList;
}
private List<Map<String, Object>> createMetatypeMap(AttributeDefinition[] definitions) {
List<Map<String, Object>> metatypeList = new ArrayList<Map<String, Object>>();
if (definitions != null) {
for (AttributeDefinition definition : definitions) {
Map<String, Object> attributeMap = new HashMap<String, Object>();
attributeMap.put(MAP_ENTRY_ID, definition.getID());
attributeMap.put(MAP_ENTRY_NAME, definition.getName());
attributeMap.put(MAP_ENTRY_CARDINALITY, definition.getCardinality());
attributeMap.put(MAP_ENTRY_DEFAULT_VALUE, definition.getDefaultValue());
attributeMap.put(MAP_ENTRY_DESCRIPTION, definition.getDescription());
attributeMap.put(MAP_ENTRY_TYPE, definition.getType());
attributeMap.put(MAP_ENTRY_OPTION_LABELS, definition.getOptionLabels());
attributeMap.put(MAP_ENTRY_OPTION_VALUES, definition.getOptionValues());
metatypeList.add(attributeMap);
}
}
return metatypeList;
}
public boolean isPermittedToViewService(String servicePid) {
KeyValueCollectionPermission serviceToCheck = new KeyValueCollectionPermission(
"view-service.pid",
new KeyValuePermission("service.pid", Sets.newHashSet(servicePid)));
return SecurityUtils.getSubject()
.isPermitted(serviceToCheck);
}
/**
* The <code>IdGetter</code> interface is an internal helper to abstract retrieving object class
* definitions from all bundles for either pids or factory pids.
*
* @see #PID_GETTER
* @see #FACTORY_PID_GETTER
*/
private interface IdGetter {
String[] getIds(MetaTypeInformation metaTypeInformation);
}
}