| /* |
| * 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 (!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() ); |
| } |
| } |
| } |
| } |
| } |