This is an automated email from the ASF dual-hosted git repository. cziegeler pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-factory-configuration.git
The following commit(s) were added to refs/heads/master by this push: new 06a94ca SLING-10771 : Support handling of metatype info when merging configurations 06a94ca is described below commit 06a94ca56663ffdd2b9f4c5e0b731b1e71f9f20f Author: Carsten Ziegeler <cziege...@apache.org> AuthorDate: Wed Sep 1 12:42:26 2021 +0200 SLING-10771 : Support handling of metatype info when merging configurations --- .gitignore | 1 + bnd.bnd | 4 + pom.xml | 24 +++++ .../configuration/impl/ConfigTaskCreator.java | 16 ++- .../configuration/impl/MetatypeHandler.java | 115 +++++++++++++++++++++ .../configuration/impl/ServicesListener.java | 24 +++++ .../impl/WebconsoleConfigurationHandler.java | 77 ++++++++++++++ .../configuration/impl/MetatypeHandlerTest.java | 114 ++++++++++++++++++++ 8 files changed, 372 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5b783ed..964bd9e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ maven-eclipse.xml *.iws *.bak .vlt +.vscode/ .DS_Store jcr.log atlassian-ide-plugin.xml diff --git a/bnd.bnd b/bnd.bnd index e69de29..10de141 100644 --- a/bnd.bnd +++ b/bnd.bnd @@ -0,0 +1,4 @@ +Import-Package: !org.osgi.service.metatype, \ + !org.apache.felix.webconsole.spi,* +DynamicImport-Package: org.osgi.service.metatype, \ + org.apache.felix.webconsole.spi diff --git a/pom.xml b/pom.xml index 3451869..d8d7888 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,30 @@ <scope>provided</scope> </dependency> <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.webconsole</artifactId> + <version>4.6.5-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.service.metatype</artifactId> + <version>1.4.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.util.converter</artifactId> + <version>1.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.util.function</artifactId> + <version>1.0.0</version> + <scope>provided</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java index 6b7559b..e4bd388 100644 --- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java +++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java @@ -164,7 +164,7 @@ public class ConfigTaskCreator attrs.put(ConfigurationAdmin.SERVICE_FACTORYPID, event.getFactoryPid()); } - removeDefaultProperties(event.getPid(), dict); + removeDefaultProperties(this.infoProvider, event.getPid(), dict); this.changeListener.resourceAddedOrUpdated(InstallableResource.TYPE_CONFIG, event.getPid(), null, dict, attrs); } else { @@ -177,12 +177,12 @@ public class ConfigTaskCreator } } - private void removeDefaultProperties(final String pid, final Dictionary<String, Object> dict) { + public static Dictionary<String, Object> getDefaultProperties(final InfoProvider infoProvider, final String pid) { if ( Activator.MERGE_SCHEMES != null ) { final List<Dictionary<String, Object>> propertiesList = new ArrayList<>(); final String entityId = InstallableResource.TYPE_CONFIG.concat(":").concat(pid); boolean done = false; - for(final ResourceGroup group : this.infoProvider.getInstallationState().getInstalledResources()) { + for(final ResourceGroup group : infoProvider.getInstallationState().getInstalledResources()) { for(final Resource rsrc : group.getResources()) { if ( rsrc.getEntityId().equals(entityId) ) { done = true; @@ -197,6 +197,16 @@ public class ConfigTaskCreator } if ( !propertiesList.isEmpty() ) { final Dictionary<String, Object> defaultProps = ConfigUtil.mergeReverseOrder(propertiesList); + return defaultProps; + } + } + return null; + } + + public static void removeDefaultProperties(final InfoProvider infoProvider, final String pid, final Dictionary<String, Object> dict) { + if ( Activator.MERGE_SCHEMES != null ) { + final Dictionary<String, Object> defaultProps = getDefaultProperties(infoProvider, pid); + if ( defaultProps != null ) { final Enumeration<String> keyEnum = defaultProps.keys(); while ( keyEnum.hasMoreElements() ) { final String key = keyEnum.nextElement(); diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java new file mode 100644 index 0000000..883643c --- /dev/null +++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandler.java @@ -0,0 +1,115 @@ +/* + * 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.sling.installer.factories.configuration.impl; + +import java.util.Arrays; +import java.util.Dictionary; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +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.converter.Converters; + +public class MetatypeHandler { + + private final MetaTypeService srv; + + private final BundleContext bundleContext; + + public MetatypeHandler(final Object mts, final BundleContext bundleContext) { + this.srv = (MetaTypeService)mts; + this.bundleContext = bundleContext; + } + + public void updateConfiguration(final String factoryPid, + final String pid, + final Dictionary<String, Object> props, + final Dictionary<String, Object> defaultProps) { + // search metatype + final ObjectClassDefinition ocd; + if ( factoryPid != null ) { + ocd = this.getObjectClassDefinition( factoryPid ); + } else { + ocd = this.getObjectClassDefinition( pid ); + } + + if ( ocd != null ) { + for(final AttributeDefinition ad : ocd.getAttributeDefinitions(ObjectClassDefinition.ALL)) { + final String propName = ad.getID(); + final Object newValue = props.get(propName); + if ( newValue != null + && (defaultProps == null || defaultProps.get(propName) == null) ) { + if ( ad.getCardinality() == 0 ) { + if ( !shouldSet(ad, newValue.toString())) { + props.remove(propName); + } + } else { + final String[] array = Converters.standardConverter().convert(newValue).to(String[].class); + if ( !shouldSet(ad, array)) { + props.remove(propName); + } + } + } + } + } + } + + private ObjectClassDefinition getObjectClassDefinition( final String pid ) { + for(final Bundle b : this.bundleContext.getBundles()) { + try { + final MetaTypeInformation mti = this.srv.getMetaTypeInformation( b ); + if ( mti != null ) { + final ObjectClassDefinition ocd = mti.getObjectClassDefinition( pid, null );; + if ( ocd != null ) { + return ocd; + } + } + } catch ( final IllegalArgumentException iae ) { + // ignore + } + } + return null; + } + + boolean shouldSet(final AttributeDefinition ad, final String value) { + if ( value.isEmpty() && ad.getDefaultValue() == null ) { + return false; + } + if ( ad.getDefaultValue() != null && value.equals(ad.getDefaultValue()[0]) ) { + return false; + } + return true; + } + + boolean shouldSet(final AttributeDefinition ad, final String[] values) { + if ( ad.getDefaultValue() == null ) { + if ( values.length == 0 || (values.length == 1 && values[0].isEmpty() ) ) { + return false; + } + } + if ( ad.getDefaultValue() != null && Arrays.equals(ad.getDefaultValue(), values) ) { + return false; + } + return true; + } + +} diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java index 41e37f9..139eb2f 100644 --- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java +++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java @@ -22,10 +22,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.sling.installer.api.ResourceChangeListener; import org.apache.sling.installer.api.info.InfoProvider; +import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceFactory; import org.osgi.framework.ServiceListener; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; @@ -56,6 +58,9 @@ public class ServicesListener { /** Registration the service. */ private volatile ServiceRegistration<?> configTaskCreatorRegistration; + /** Registration for the webconsole support. */ + private volatile ServiceRegistration<?> webconsoleRegistration; + private volatile ConfigTaskCreator configTaskCreator; private final AtomicBoolean active = new AtomicBoolean(false); @@ -83,6 +88,21 @@ public class ServicesListener { this.configTaskCreator = new ConfigTaskCreator(listener, configAdmin, infoProvider); final ConfigUpdateHandler handler = new ConfigUpdateHandler(configAdmin, this); configTaskCreatorRegistration = handler.register(this.bundleContext); + if ( Activator.MERGE_SCHEMES != null ) { + this.webconsoleRegistration = this.bundleContext.registerService("org.apache.felix.webconsole.spi.ConfigurationHandler", new ServiceFactory<Object>(){ + + @Override + public Object getService(final Bundle bundle, final ServiceRegistration<Object> registration) { + return new WebconsoleConfigurationHandler(bundleContext, infoProvider); + } + + @Override + public void ungetService(final Bundle bundle, final ServiceRegistration<Object> registration, final Object service) { + ((WebconsoleConfigurationHandler)service).deactivate(); + } + + }, null); + } } } else { this.stop(); @@ -92,6 +112,10 @@ public class ServicesListener { private synchronized void stop() { active.set(false); // unregister + if ( this.webconsoleRegistration != null ) { + this.webconsoleRegistration.unregister(); + this.webconsoleRegistration = null; + } if ( this.configTaskCreatorRegistration != null ) { this.configTaskCreatorRegistration.unregister(); this.configTaskCreatorRegistration = null; diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java new file mode 100644 index 0000000..3aa0161 --- /dev/null +++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/WebconsoleConfigurationHandler.java @@ -0,0 +1,77 @@ +/* + * 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.sling.installer.factories.configuration.impl; + +import java.io.IOException; +import java.util.Dictionary; + +import org.apache.felix.webconsole.spi.ConfigurationHandler; +import org.apache.felix.webconsole.spi.ValidationException; +import org.apache.sling.installer.api.info.InfoProvider; +import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; + +public class WebconsoleConfigurationHandler implements ConfigurationHandler { + + static final String META_TYPE_NAME = "org.osgi.service.metatype.MetaTypeService"; + + private final InfoProvider infoProvider; + + private final ServiceTracker<Object, Object> metatypeTracker; + + private final BundleContext bundleContext; + + public WebconsoleConfigurationHandler(final BundleContext context, final InfoProvider infoProvider) { + this.infoProvider = infoProvider; + this.bundleContext = context; + this.metatypeTracker = new ServiceTracker<>(context, META_TYPE_NAME, null); + this.metatypeTracker.open(); + } + + public void deactivate() { + this.metatypeTracker.close(); + } + + @Override + public void createConfiguration(final String pid) throws ValidationException, IOException { + // nothing to do + } + + @Override + public void createFactoryConfiguration(final String factoryPid, String name) throws ValidationException, IOException { + // nothing to do + } + + @Override + public void deleteConfiguration(final String factoryPid, final String pid) throws ValidationException, IOException { + // nothing to do + } + + @Override + public void updateConfiguration(final String factoryPid, final String pid, final Dictionary<String, Object> props) + throws ValidationException, IOException { + final Object mts = this.metatypeTracker.getService(); + if ( mts != null ) { + final Dictionary<String, Object> defaultProps = ConfigTaskCreator.getDefaultProperties(infoProvider, pid); + final MetatypeHandler mt = new MetatypeHandler(mts, this.bundleContext); + mt.updateConfiguration(factoryPid, pid, props, defaultProps); + } + } + +} diff --git a/src/test/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandlerTest.java b/src/test/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandlerTest.java new file mode 100644 index 0000000..51c906b --- /dev/null +++ b/src/test/java/org/apache/sling/installer/factories/configuration/impl/MetatypeHandlerTest.java @@ -0,0 +1,114 @@ +/* + * 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.sling.installer.factories.configuration.impl; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.MetaTypeInformation; +import org.osgi.service.metatype.MetaTypeService; +import org.osgi.service.metatype.ObjectClassDefinition; + +public class MetatypeHandlerTest { + + @Test public void testUpdateConfiguration() throws Exception { + final BundleContext bundleContext = Mockito.mock(BundleContext.class); + final MetaTypeService mts = Mockito.mock(MetaTypeService.class); + final Bundle bundle = Mockito.mock(Bundle.class); + Mockito.when(bundleContext.getBundles()).thenReturn(new Bundle[] {bundle}); + + final MetaTypeInformation info = Mockito.mock(MetaTypeInformation.class); + Mockito.when(mts.getMetaTypeInformation(bundle)).thenReturn(info); + final MetatypeHandler handler = new MetatypeHandler(mts, bundleContext); + + final ObjectClassDefinition ocd = Mockito.mock(ObjectClassDefinition.class); + Mockito.when(info.getObjectClassDefinition("my.pid", null)).thenReturn(ocd); + + final AttributeDefinition ada = Mockito.mock(AttributeDefinition.class); + Mockito.when(ada.getID()).thenReturn("a"); + Mockito.when(ada.getDefaultValue()).thenReturn(new String[] {"1"}); + Mockito.when(ada.getCardinality()).thenReturn(1); + Mockito.when(ada.getType()).thenReturn(AttributeDefinition.STRING); + + final AttributeDefinition adb = Mockito.mock(AttributeDefinition.class); + Mockito.when(adb.getID()).thenReturn("b"); + Mockito.when(adb.getDefaultValue()).thenReturn(new String[] {"2"}); + Mockito.when(adb.getCardinality()).thenReturn(1); + Mockito.when(adb.getType()).thenReturn(AttributeDefinition.STRING); + + final AttributeDefinition adc = Mockito.mock(AttributeDefinition.class); + Mockito.when(adc.getID()).thenReturn("c"); + Mockito.when(adc.getDefaultValue()).thenReturn(new String[] {"3"}); + Mockito.when(adc.getCardinality()).thenReturn(1); + Mockito.when(adc.getType()).thenReturn(AttributeDefinition.STRING); + + final AttributeDefinition add = Mockito.mock(AttributeDefinition.class); + Mockito.when(add.getID()).thenReturn("d"); + Mockito.when(add.getDefaultValue()).thenReturn(new String[] {"4"}); + Mockito.when(add.getCardinality()).thenReturn(1); + Mockito.when(add.getType()).thenReturn(AttributeDefinition.INTEGER); + + final AttributeDefinition adE = Mockito.mock(AttributeDefinition.class); + Mockito.when(adE.getID()).thenReturn("e"); + Mockito.when(adE.getDefaultValue()).thenReturn(new String[] {"5"}); + Mockito.when(adE.getCardinality()).thenReturn(1); + Mockito.when(adE.getType()).thenReturn(AttributeDefinition.INTEGER); + + final AttributeDefinition adF = Mockito.mock(AttributeDefinition.class); + Mockito.when(adF.getID()).thenReturn("f"); + Mockito.when(adF.getDefaultValue()).thenReturn(new String[] {"/a", "/b"}); + Mockito.when(adF.getCardinality()).thenReturn(-100); + Mockito.when(adF.getType()).thenReturn(AttributeDefinition.STRING); + + final AttributeDefinition adG = Mockito.mock(AttributeDefinition.class); + Mockito.when(adG.getID()).thenReturn("g"); + Mockito.when(adG.getDefaultValue()).thenReturn(new String[] {"/x", "/y"}); + Mockito.when(adG.getCardinality()).thenReturn(-100); + Mockito.when(adG.getType()).thenReturn(AttributeDefinition.STRING); + + Mockito.when(ocd.getAttributeDefinitions(ObjectClassDefinition.ALL)).thenReturn(new AttributeDefinition[] {ada,adb,adc,add,adE,adF,adG}); + + final Dictionary<String, Object> props = new Hashtable<>(); + props.put("a", "2"); + props.put("c", "3"); + props.put("d", 4); + props.put("e", 5); + props.put("f", Arrays.asList("/a", "/b")); + props.put("g", Arrays.asList("/a", "/b")); + + final Dictionary<String, Object> defaultProps = new Hashtable<>(); + defaultProps.put("b", "5"); + defaultProps.put("d", 7); + + handler.updateConfiguration(null, "my.pid", props, defaultProps); + + assertEquals(3, props.size()); + assertEquals("2", props.get("a")); + assertEquals(4, props.get("d")); + assertEquals(Arrays.asList("/a", "/b"), props.get("g")); + } +}