blob: 323ac13e0bc31950e1dc74383217ea618cac7ed9 [file] [log] [blame]
/*
* 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.webconsole.internal.configuration;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
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.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import org.apache.felix.utils.json.JSONWriter;
import org.apache.felix.webconsole.internal.Util;
import org.apache.felix.webconsole.internal.misc.ServletSupport;
import org.apache.felix.webconsole.spi.ConfigurationHandler;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
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.ObjectClassDefinition;
class ConfigJsonSupport {
private final ServletSupport servletSupport;
private final MetaTypeServiceSupport mtss;
private final ConfigurationAdmin configurationAdmin;
private final List<ConfigurationHandler> configurationHandlers;
public ConfigJsonSupport(final ServletSupport support,
final MetaTypeServiceSupport mtss,
final ConfigurationAdmin cfgAdmin,
final List<ConfigurationHandler> cfgHandlers) {
this.servletSupport = support;
this.mtss = mtss;
this.configurationAdmin = cfgAdmin;
this.configurationHandlers = cfgHandlers;
}
public void printConfigurationJson( final PrintWriter pw, final String pid, final Configuration config, final String pidFilter,
final String locale) {
final JSONWriter result = new JSONWriter( pw );
if ( pid != null ) {
try{
result.object();
this.configForm( result, pid, config, pidFilter, locale );
result.endObject();
} catch ( final Exception e ) {
this.servletSupport.log( "Error reading configuration PID " + pid, e );
}
}
}
/**
* Get the list of property names for the form and filter the properties based on this list
*/
List<String> getPropertyNamesForForm(final String factoryPid, final String pid,
final Dictionary<String, Object> props)
throws IOException {
final List<String> names = new ArrayList<>(Collections.list(props.keys()));
if ( !configurationHandlers.isEmpty() && !names.isEmpty()) {
// fill remove list with all names
final List<String> removeList = new ArrayList<>(names);
for(final ConfigurationHandler handler : configurationHandlers) {
handler.filterProperties(factoryPid, pid, names);
}
// update remove list
removeList.removeAll(names);
// remove properties
removeList.forEach(props::remove);
}
return names;
}
void configForm( final JSONWriter json, final String pid, final Configuration config, final String pidFilter, final String locale )
throws IOException {
json.key( ConfigManager.PID );
json.value( pid );
if ( pidFilter != null ) {
json.key( ConfigManager.PID_FILTER );
json.value( pidFilter );
}
Dictionary<String, Object> props = null;
if ( config != null ) {
props = config.getProperties();
}
if ( props == null ) {
props = new Hashtable<>();
}
final List<String> keys = getPropertyNamesForForm(config != null ? config.getFactoryPid() : null, pid, props);
boolean doSimpleMerge = true;
if ( this.mtss != null ) {
ObjectClassDefinition ocd = null;
if ( config != null ) {
ocd = mtss.getObjectClassDefinition( config, locale );
}
if ( ocd == null ) {
ocd = mtss.getObjectClassDefinition( pid, locale );
}
ObjectClassDefinition filteredOcd = ocd;
if ( ocd != null ) {
final ObjectClassDefinition focd = ocd;
filteredOcd = new ObjectClassDefinition() {
@Override
public String getName() {
return focd.getName();
}
@Override
public String getID() {
return focd.getID();
}
@Override
public String getDescription() {
return focd.getDescription();
}
@Override
public AttributeDefinition[] getAttributeDefinitions(int i) {
AttributeDefinition[] allDefinitions = focd.getAttributeDefinitions(i);
if (allDefinitions != null) {
ArrayList<AttributeDefinition> filteredDefinitions = new ArrayList<>();
for (AttributeDefinition def : allDefinitions) {
if (keys.contains(def.getID())) {
filteredDefinitions.add(def);
}
}
return filteredDefinitions.toArray(new AttributeDefinition[0]);
}
return null;
}
@Override
public InputStream getIcon(int i) throws IOException {
return focd.getIcon(i);
}
};
mtss.mergeWithMetaType( props, filteredOcd, json, ConfigAdminSupport.CONFIG_PROPERTIES_HIDE );
doSimpleMerge = false;
}
}
if (doSimpleMerge) {
json.key( "title" ).value( pid ); //$NON-NLS-1$
json.key( "description" ).value( //$NON-NLS-1$
"This form is automatically generated from existing properties because no property "
+ "descriptors are available for this configuration. This may be cause by the absence "
+ "of the OSGi Metatype Service or the absence of a MetaType descriptor for this configuration." );
json.key( "properties" ).object(); //$NON-NLS-1$
for ( Enumeration<String> pe = props.keys(); pe.hasMoreElements(); ) {
final String id = pe.nextElement();
// ignore well known special properties
if ( !id.equals( Constants.SERVICE_PID ) && !id.equals( Constants.SERVICE_DESCRIPTION )
&& !id.equals( Constants.SERVICE_ID ) && !id.equals( Constants.SERVICE_VENDOR )
&& !id.equals( ConfigurationAdmin.SERVICE_BUNDLELOCATION )
&& !id.equals( ConfigurationAdmin.SERVICE_FACTORYPID ) ) {
final Object value = props.get( id );
final PropertyDescriptor ad = MetaTypeServiceSupport.createAttributeDefinition( id, value );
json.key( id );
MetaTypeServiceSupport.attributeToJson( json, ad, value );
}
}
json.endObject();
}
if ( config != null ) {
this.addConfigurationInfo( config, json, locale );
}
}
void addConfigurationInfo( final Configuration config, final JSONWriter json, final String locale )
throws IOException {
if ( config.getFactoryPid() != null ) {
json.key( ConfigManager.FACTORY_PID );
json.value( config.getFactoryPid() );
}
String bundleLocation = config.getBundleLocation();
if ( ConfigManager.UNBOUND_LOCATION.equals(bundleLocation) ) {
bundleLocation = null;
}
String location;
if ( bundleLocation == null ) {
location = ""; //$NON-NLS-1$
} else {
// if the configuration is bound to a bundle location which
// is not related to an installed bundle, we just print the
// raw bundle location binding
Bundle bundle = MetaTypeServiceSupport.getBundle( this.servletSupport.getBundleContext(), bundleLocation );
if ( bundle == null ) {
location = bundleLocation;
} else {
Dictionary<String, String> headers = bundle.getHeaders( locale );
String name = headers.get( Constants.BUNDLE_NAME );
if ( name == null ) {
location = bundle.getSymbolicName();
} else {
location = name + " (" + bundle.getSymbolicName() + ')'; //$NON-NLS-1$
}
Version v = Version.parseVersion( headers.get( Constants.BUNDLE_VERSION ) );
location += ", Version " + v.toString();
}
}
json.key( "bundleLocation" ); //$NON-NLS-1$
json.value( location );
// raw bundle location and service locations
final String pid = config.getPid();
String serviceLocation = ""; //$NON-NLS-1$
try {
final ServiceReference<?>[] refs = this.servletSupport.getBundleContext().getServiceReferences(
(String)null,
"(&(" + Constants.OBJECTCLASS + '=' + ManagedService.class.getName() //$NON-NLS-1$
+ ")(" + Constants.SERVICE_PID + '=' + pid + "))"); //$NON-NLS-1$ //$NON-NLS-2$
if ( refs != null && refs.length > 0 ) {
serviceLocation = refs[0].getBundle().getLocation();
}
} catch (final Throwable t) {
this.servletSupport.log( "Error getting service associated with configuration " + pid, t );
}
json.key( "bundle_location" ); //$NON-NLS-1$
json.value ( bundleLocation );
json.key( "service_location" ); //$NON-NLS-1$
json.value ( serviceLocation );
}
private final Bundle getBoundBundle(final Configuration config) {
if (null == config) {
return null;
}
final String location = config.getBundleLocation();
if (null == location) {
return null;
}
final Bundle bundles[] = this.servletSupport.getBundleContext().getBundles();
for (int i = 0; bundles != null && i < bundles.length; i++) {
if (bundles[i].getLocation().equals(location)) {
return bundles[i];
}
}
return null;
}
final boolean listConfigurations(final JSONWriter jw, final String pidFilter, final String locale, final Locale loc ) {
boolean hasConfigurations = false;
try {
// start with ManagedService instances
Map<String, String> optionsPlain = getServices(ManagedService.class.getName(), pidFilter,
locale, true);
// next are the MetaType informations without ManagedService
if ( mtss != null ) {
addMetaTypeNames( optionsPlain, mtss.getPidObjectClasses( locale ), pidFilter, Constants.SERVICE_PID );
}
// add in existing configuration (not duplicating ManagedServices)
Configuration[] cfgs = this.configurationAdmin.listConfigurations(pidFilter);
for (int i = 0; cfgs != null && i < cfgs.length; i++)
{
// ignore configuration object if an entry already exists in the map
// or if it is invalid
final String pid = cfgs[i].getPid();
if (optionsPlain.containsKey(pid) || !ConfigurationUtil.isAllowedPid(pid) )
{
continue;
}
// insert and entry for the PID
if ( mtss != null )
{
try
{
ObjectClassDefinition ocd = mtss.getObjectClassDefinition( cfgs[i], locale );
if ( ocd != null )
{
optionsPlain.put( pid, ocd.getName() );
continue;
}
}
catch ( IllegalArgumentException t )
{
// 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
}
}
// no object class definition, use plain PID
optionsPlain.put( pid, pid );
}
jw.key("pids");//$NON-NLS-1$
jw.array();
for ( Iterator<String> ii = optionsPlain.keySet().iterator(); ii.hasNext(); )
{
hasConfigurations = true;
String id = ii.next();
Object name = optionsPlain.get( id );
final Configuration c = ConfigurationUtil.findConfiguration( this.configurationAdmin, id );
Configuration config = c;
if (null != config && !this.configurationHandlers.isEmpty()) {
for(final ConfigurationHandler handler : this.configurationHandlers) {
if (!handler.listConfiguration(config.getFactoryPid(), config.getPid())) {
config = null;
break;
}
}
}
if ( null != config )
{
jw.object();
jw.key("id").value( id ); //$NON-NLS-1$
jw.key( "name").value( name ); //$NON-NLS-1$
// FELIX-3848
jw.key("has_config").value( true ); //$NON-NLS-1$
final String fpid = config.getFactoryPid();
if ( null != fpid )
{
jw.key("fpid").value( fpid ); //$NON-NLS-1$
final String val = getConfigurationFactoryNameHint(config);
if ( val != null )
{
jw.key( "nameHint").value(val ); //$NON-NLS-1$
}
}
final Bundle bundle = getBoundBundle( config );
if ( null != bundle ) {
jw.key( "bundle").value( bundle.getBundleId() ); //$NON-NLS-1$
jw.key( "bundle_name").value( Util.getName( bundle, loc ) ); //$NON-NLS-1$
}
jw.endObject();
}
}
jw.endArray();
} catch (final Exception e) {
this.servletSupport.log("listConfigurations: Unexpected problem encountered", e);
}
return hasConfigurations;
}
/**
* Builds a "name hint" for factory configuration based on other property
* values of the config and a "name hint template" defined as hidden
* property in the service.
* @param config The factory configuration.
* @return Name hint or null if none is defined.
*/
private final String getConfigurationFactoryNameHint(Configuration config) {
Map<String, MetatypePropertyDescriptor> adMap = (mtss != null) ? mtss.getAttributeDefinitionMap(config, null) : null;
if (null == adMap) {
return null;
}
final Dictionary<String, Object> props = config.getProperties();
String nameHint = null;
// check for configured name hint template
ServiceReference<?>[] refs;
String filter = "(service.pid=" + config.getPid() + ")";
try {
refs = servletSupport.getBundleContext().getAllServiceReferences(null, filter);
} catch (InvalidSyntaxException e) {
throw new IllegalStateException("Invalid filter: " + filter);
}
// first try via service reference properties
if (refs != null) {
nameHint = getPropertyValueAsString(refs[0].getProperty(ConfigAdminSupport.PROPERTY_FACTORYCONFIG_NAMEHINT));
}
// as fallback use the configuration admin properties
if (nameHint == null) {
nameHint = getConfigurationPropertyValueOrDefault(ConfigAdminSupport.PROPERTY_FACTORYCONFIG_NAMEHINT, props, adMap);
}
if (nameHint == null) {
return null;
}
// search for all variable patterns in name hint and replace them with configured/default values
Matcher matcher = ConfigAdminSupport.NAMEHINT_PLACEHOLER_REGEXP.matcher(nameHint);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String propertyName = matcher.group(1);
String value = getConfigurationPropertyValueOrDefault(propertyName, props, adMap);
if (value == null) {
value = "";
}
matcher.appendReplacement(sb, matcherQuoteReplacement(value));
}
matcher.appendTail(sb);
// make sure name hint does not only contain whitespaces
nameHint = sb.toString().trim();
if (nameHint.length() == 0) {
return null;
} else {
return nameHint;
}
}
/**
* Gets configured service property value, or default value if no value is configured.
* @param propertyName Property name
* @param props Service configuration properties map
* @param adMap Attribute definitions map
* @return Value or null if none found
*/
private static String getConfigurationPropertyValueOrDefault(String propertyName, Dictionary<String, Object> props, Map<String, MetatypePropertyDescriptor> adMap) {
// get configured property value
Object value = props.get(propertyName);
if (value != null) {
return getPropertyValueAsString(value);
} else {
// if not set try to get default value
PropertyDescriptor ad = adMap.get(propertyName);
if (ad != null && ad.getDefaultValue() != null && ad.getDefaultValue().length == 1) {
return ad.getDefaultValue()[0];
}
}
return null;
}
private static String getPropertyValueAsString(Object value) {
if (value == null) {
return null;
}
// convert array to string
if (value.getClass().isArray()) {
StringBuffer valueString = new StringBuffer();
for (int i = 0; i < Array.getLength(value); i++) {
if (i > 0) {
valueString.append(", ");
}
valueString.append(Array.get(value, i));
}
return valueString.toString();
} else {
return value.toString();
}
}
/**
* Replacement for Matcher.quoteReplacement which is only available in JDK 1.5 and up.
* @param str Unquoted string
* @return Quoted string
*/
private static String matcherQuoteReplacement(String str) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '$' || c == '\\') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
final void listFactoryConfigurations(JSONWriter jw, String pidFilter,
String locale) {
try {
final Map<String, String> optionsFactory = getServices(ManagedServiceFactory.class.getName(),
pidFilter, locale, true);
if ( mtss != null ) {
addMetaTypeNames( optionsFactory, mtss.getFactoryPidObjectClasses( locale ), pidFilter,
ConfigurationAdmin.SERVICE_FACTORYPID );
}
jw.key("fpids");
jw.array();
for ( Iterator<String> ii = optionsFactory.keySet().iterator(); ii.hasNext(); ) {
String id = ii.next();
Object name = optionsFactory.get( id );
jw.object();
jw.key("id").value(id ); //$NON-NLS-1$
jw.key("name").value( name ); //$NON-NLS-1$
jw.endObject();
}
jw.endArray();
} catch (final Exception e) {
this.servletSupport.log("listFactoryConfigurations: Unexpected problem encountered", e);
}
}
SortedMap<String, String> getServices( String serviceClass, String serviceFilter, String locale,
boolean ocdRequired ) throws InvalidSyntaxException {
// sorted map of options
SortedMap<String, String> optionsFactory = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
// find all ManagedServiceFactories to get the factoryPIDs
ServiceReference<?>[] refs = this.servletSupport.getBundleContext().getServiceReferences( 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 && ConfigurationUtil.isAllowedPid((String)pidObject) ) {
String pid = ( String ) pidObject;
String name = pid;
boolean haveOcd = !ocdRequired;
if ( mtss != null ) {
final ObjectClassDefinition ocd = mtss.getObjectClassDefinition( refs[i].getBundle(), pid, locale );
if ( ocd != null ) {
name = ocd.getName();
haveOcd = true;
}
}
if ( haveOcd ) {
optionsFactory.put( pid, name );
}
}
}
return optionsFactory;
}
private void addMetaTypeNames( final Map<String, String> pidMap, final Map<String, ObjectClassDefinition> ocdCollection, final String filterSpec, final String type ) {
Filter filter = null;
if ( filterSpec != null ) {
try {
filter = this.servletSupport.getBundleContext().createFilter( filterSpec );
} catch ( InvalidSyntaxException not_expected ){
// filter is correct
}
}
for ( Iterator<Map.Entry<String, ObjectClassDefinition>> ei = ocdCollection.entrySet().iterator(); ei.hasNext(); ) {
Entry<String, ObjectClassDefinition> ociEntry = ei.next();
final String pid = ociEntry.getKey();
final ObjectClassDefinition ocd = ociEntry.getValue();
if ( filter == null ) {
pidMap.put( pid, ocd.getName() );
} else {
final Dictionary<String, Object> props = new Hashtable<>();
props.put( type, pid );
if ( filter.match( props ) ) {
pidMap.put( pid, ocd.getName() );
}
}
}
}
}