/* * 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.deployment.rp.autoconf; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.Dictionary; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; import junit.framework.TestCase; import org.apache.felix.dm.Component; import org.apache.felix.dm.DependencyManager; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Filter; 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.deploymentadmin.DeploymentPackage; import org.osgi.service.deploymentadmin.spi.DeploymentSession; import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; import org.osgi.service.log.LogService; public class AutoConfResourceProcessorTest extends TestCase { private static class ConfigurationAdminImpl implements ConfigurationAdmin { private final String[] m_expectedPIDs; private final String[] m_expectedFactoryPIDs; private final Map<String, ConfigurationImpl> m_configs; public ConfigurationAdminImpl(String... expectedPIDs) { this(expectedPIDs, new String[0]); } public ConfigurationAdminImpl(String[] expectedPIDs, String[] expectedFactoryPIDs) { m_expectedPIDs = expectedPIDs; m_expectedFactoryPIDs = expectedFactoryPIDs; m_configs = new LinkedHashMap<String, ConfigurationImpl>(); } public Configuration createFactoryConfiguration(String factoryPid) throws IOException { return createFactoryConfiguration(factoryPid, null); } public Configuration createFactoryConfiguration(String factoryPid, String location) throws IOException { if (!isExpected(m_expectedFactoryPIDs, factoryPid)) { throw new IOException("Unexpected factory PID: " + factoryPid); } // This should be unique enough for our use cases... String pid = String.format("pid%d", m_configs.size()); ConfigurationImpl config = m_configs.get(pid); if (config == null) { config = new ConfigurationImpl(factoryPid, pid, location); m_configs.put(pid, config); } config.setBundleLocation(location); return config; } public Configuration getConfiguration(String pid) throws IOException { return getConfiguration(pid, null); } public Configuration getConfiguration(String pid, String location) throws IOException { if (!isExpected(m_expectedPIDs, pid)) { throw new IOException("Unexpected PID: " + pid); } ConfigurationImpl config = m_configs.get(pid); if (config == null) { config = new ConfigurationImpl(null, pid, location); m_configs.put(pid, config); } config.setBundleLocation(location); return config; } public Configuration[] listConfigurations(String filter) throws IOException, InvalidSyntaxException { return null; } private boolean isExpected(String[] expectedPIDs, String actualPID) { for (String expectedPID : expectedPIDs) { if (actualPID.equals(expectedPID)) { return true; } } return false; } } private static class ConfigurationImpl implements Configuration { private final String m_factoryPID; private final String m_pid; private String m_bundleLocation; private Dictionary m_properties; private boolean m_deleted; public ConfigurationImpl(String factoryPid, String pid, String bundleLocation) { m_factoryPID = factoryPid; m_pid = pid; m_bundleLocation = bundleLocation; } public void delete() throws IOException { m_deleted = true; } public String getBundleLocation() { return m_bundleLocation; } public String getFactoryPid() { return m_factoryPID; } public String getPid() { return m_pid; } public Dictionary getProperties() { return m_properties; } public void setBundleLocation(String bundleLocation) { if (m_bundleLocation != null && !m_bundleLocation.equals(bundleLocation)) { throw new RuntimeException("Configuration already bound to location: " + m_bundleLocation + " (trying to set to: " + bundleLocation + ")"); } m_bundleLocation = bundleLocation; } public void update() throws IOException { } public void update(Dictionary properties) throws IOException { m_properties = properties; } } /** Dummy session. */ private static class DeploymentSessionImpl implements DeploymentSession { public File getDataFile(Bundle bundle) { return null; } public DeploymentPackage getSourceDeploymentPackage() { return null; } public DeploymentPackage getTargetDeploymentPackage() { return null; } @Override public String toString() { return "Test DeploymentSession @ 0x" + System.identityHashCode(this); } } private static class LogServiceImpl implements LogService { private static final String[] LEVEL = { "", "[ERROR]", "[WARN ]", "[INFO ]", "[DEBUG]" }; private Throwable m_exception; public void failOnException() throws Throwable { if (m_exception != null) { throw m_exception; } } public void log(int level, String message) { System.out.println(LEVEL[level] + " - " + message); } public void log(int level, String message, Throwable exception) { System.out.println(LEVEL[level] + " - " + message + " - " + exception.getMessage()); m_exception = exception; } public void log(ServiceReference sr, int level, String message) { System.out.println(LEVEL[level] + " - " + message); } public void log(ServiceReference sr, int level, String message, Throwable exception) { System.out.println(LEVEL[level] + " - " + message + " - " + exception.getMessage()); m_exception = exception; } } private static class ServiceReferenceImpl implements ServiceReference { private final Properties m_properties; public ServiceReferenceImpl() { this(new Properties()); } public ServiceReferenceImpl(Properties properties) { m_properties = properties; } public int compareTo(Object reference) { return 0; } public Bundle getBundle() { return null; } public Object getProperty(String key) { return m_properties.get(key); } public String[] getPropertyKeys() { return Collections.list(m_properties.keys()).toArray(new String[0]); } public Bundle[] getUsingBundles() { return null; } public boolean isAssignableTo(Bundle bundle, String className) { return false; } @Override public String toString() { return "Test ConfigAdmin @ 0x" + System.identityHashCode(this); } } private File m_tempDir; private LogServiceImpl m_logger; /** Go through a simple session, containing two empty configurations. */ public void testBasicConfigurationSession() throws Throwable { AutoConfResourceProcessor p = createAutoConfRP(); createNewSession(p); String config = "<MetaData xmlns:metatype='http://www.osgi.org/xmlns/metatype/v1.0.0'>\n" + " <OCD name='ocd' id='ocd'>\n" + " <AD id='name' type='STRING' cardinality='0' />\n" + " </OCD>\n" + " <Designate pid='simple' bundle='osgi-dp:location'>\n" + " <Object ocdref='ocd'>\n" + " <Attribute adref='name'>\n" + " <Value><![CDATA[test]]></Value>\n" + " </Attribute>\n" + " </Object>\n" + " </Designate>\n" + "</MetaData>\n"; p.process("basic", new ByteArrayInputStream(config.getBytes())); p.prepare(); p.commit(); p.addConfigurationAdmin(new ServiceReferenceImpl(), new ConfigurationAdminImpl("simple")); p.postcommit(); m_logger.failOnException(); } /** Go through a simple session, containing two empty configurations. */ public void testFilteredConfigurationSession() throws Throwable { AutoConfResourceProcessor p = createAutoConfRP(); createNewSession(p); String config = "<MetaData xmlns:metatype='http://www.osgi.org/xmlns/metatype/v1.0.0' filter='(id=42)'>\n" + " <OCD name='ocd' id='ocd'>\n" + " <AD id='name' type='STRING' cardinality='0' />\n" + " </OCD>\n" + " <Designate pid='simple' bundle='osgi-dp:location'>\n" + " <Object ocdref='ocd'>\n" + " <Attribute adref='name'>\n" + " <Value><![CDATA[test]]></Value>\n" + " </Attribute>\n" + " </Object>\n" + " </Designate>\n" + "</MetaData>\n"; p.process("basic", new ByteArrayInputStream(config.getBytes())); p.prepare(); p.commit(); Properties props = new Properties(); props.put("id", Integer.valueOf(42)); ConfigurationAdminImpl ca1 = new ConfigurationAdminImpl("simple"); ConfigurationAdminImpl ca2 = new ConfigurationAdminImpl(); p.addConfigurationAdmin(new ServiceReferenceImpl(props), ca1); p.addConfigurationAdmin(new ServiceReferenceImpl(), ca2); p.postcommit(); m_logger.failOnException(); assertEquals("test", ca1.m_configs.get("simple").getProperties().get("name")); assertTrue(ca2.m_configs.isEmpty()); } /** Go through a simple session, containing two empty configurations. */ public void testMissingMandatoryValueInConfig() throws Throwable { AutoConfResourceProcessor p = createAutoConfRP(); createNewSession(p); String config = "<MetaData xmlns:metatype='http://www.osgi.org/xmlns/metatype/v1.1.0' filter='(id=42)'>\n" + " <OCD name='ocd' id='ocd'>\n" + " <AD id='name' type='Integer' />\n" + " </OCD>\n" + " <Designate pid='simple' bundle='osgi-dp:location'>\n" + " <Object ocdref='ocd'>\n" + " <Attribute adref='name'>\n" + " <Value><![CDATA[]]></Value>\n" + " </Attribute>\n" + " </Object>\n" + " </Designate>\n" + "</MetaData>\n"; try { p.process("missing-value", new ByteArrayInputStream(config.getBytes())); fail("Expected ResourceProcessorException for missing value!"); } catch (ResourceProcessorException e) { // Ok; expected... assertEquals("Unable to parse value for definition: adref=name", e.getMessage()); } } /** Make sure the processor does not accept a 'null' session. */ public void testNullSession() throws Exception { AutoConfResourceProcessor p = new AutoConfResourceProcessor(); try { p.begin(null); fail("Should have gotten an exception when trying to begin with null session."); } catch (Exception e) { // expected } } /** Go through a simple session, containing two empty configurations. */ public void testSimpleInstallAndUninstallSession() throws Throwable { AutoConfResourceProcessor p = createAutoConfRP(); createNewSession(p); p.process("a", new ByteArrayInputStream("<MetaData />".getBytes())); p.prepare(); p.commit(); p.postcommit(); m_logger.failOnException(); createNewSession(p); p.dropAllResources(); p.prepare(); p.commit(); p.postcommit(); m_logger.failOnException(); } /** Go through a simple session, containing two empty configurations. */ public void testSimpleSession() throws Throwable { AutoConfResourceProcessor p = createAutoConfRP(); createNewSession(p); p.process("a", new ByteArrayInputStream("<MetaData />".getBytes())); p.process("b", new ByteArrayInputStream("<MetaData />".getBytes())); p.prepare(); p.commit(); p.postcommit(); m_logger.failOnException(); } /** Tests that we can update an existing configuration and properly handling deleted & updated configurations. */ public void testUpdateConfigurationSession() throws Throwable { AutoConfResourceProcessor p = createAutoConfRP(); createNewSession(p); String config1 = "<MetaData xmlns:metatype='http://www.osgi.org/xmlns/metatype/v1.0.0'>" + "<OCD name='ocd1' id='ocd1'>" + " <AD id='nameA' type='STRING' cardinality='0' />" + "</OCD>" + "<OCD name='ocd2' id='ocd2'>" + " <AD id='nameB' type='STRING' cardinality='0' />" + "</OCD>" + "<Designate pid='pid2' bundle='osgi-dp:location2'>" + " <Object ocdref='ocd2'>" + " <Attribute adref='nameB'>" + " <Value><![CDATA[test2]]></Value>" + " </Attribute>" + " </Object>" + "</Designate>" + "<Designate pid='pid1' bundle='osgi-dp:location1'>" + " <Object ocdref='ocd1'>" + " <Attribute adref='nameA'>" + " <Value><![CDATA[test1]]></Value>" + " </Attribute>" + " </Object>" + "</Designate>" + "</MetaData>"; ConfigurationAdminImpl ca = new ConfigurationAdminImpl("pid1", "pid2", "pid3"); p.process("update", new ByteArrayInputStream(config1.getBytes())); p.prepare(); p.commit(); p.addConfigurationAdmin(new ServiceReferenceImpl(), ca); p.postcommit(); m_logger.failOnException(); assertEquals(2, ca.m_configs.size()); assertTrue(ca.m_configs.containsKey("pid1")); assertFalse(ca.m_configs.get("pid1").m_deleted); assertEquals("test1", ca.m_configs.get("pid1").getProperties().get("nameA")); assertTrue(ca.m_configs.containsKey("pid2")); assertFalse(ca.m_configs.get("pid2").m_deleted); assertEquals("test2", ca.m_configs.get("pid2").getProperties().get("nameB")); String config2 = "<MetaData xmlns:metatype='http://www.osgi.org/xmlns/metatype/v1.0.0'>" + "<OCD name='ocd3' id='ocd3'>" + " <AD id='nameC' type='STRING' cardinality='0' />" + "</OCD>" + "<OCD name='ocd2' id='ocd2'>" + " <AD id='nameB' type='STRING' cardinality='0' />" + "</OCD>" + "<Designate pid='pid2' bundle='osgi-dp:location2'>" + " <Object ocdref='ocd2'>" + " <Attribute adref='nameB'>" + " <Value><![CDATA[test4]]></Value>" + " </Attribute>" + " </Object>" + "</Designate>" + "<Designate pid='pid3' bundle='osgi-dp:location3'>" + " <Object ocdref='ocd3'>" + " <Attribute adref='nameC'>" + " <Value><![CDATA[test3]]></Value>" + " </Attribute>" + " </Object>" + "</Designate>" + "</MetaData>"; createNewSession(p); p.process("update", new ByteArrayInputStream(config2.getBytes())); p.prepare(); p.commit(); p.addConfigurationAdmin(new ServiceReferenceImpl(), ca); p.postcommit(); m_logger.failOnException(); assertEquals(3, ca.m_configs.size()); assertTrue(ca.m_configs.containsKey("pid1")); assertTrue(ca.m_configs.get("pid1").m_deleted); assertEquals("test1", ca.m_configs.get("pid1").getProperties().get("nameA")); assertTrue(ca.m_configs.containsKey("pid2")); assertFalse(ca.m_configs.get("pid2").m_deleted); assertEquals("test4", ca.m_configs.get("pid2").getProperties().get("nameB")); assertTrue(ca.m_configs.containsKey("pid3")); assertFalse(ca.m_configs.get("pid3").m_deleted); assertEquals("test3", ca.m_configs.get("pid3").getProperties().get("nameC")); } @Override protected void setUp() throws IOException { m_tempDir = File.createTempFile("persistence", "dir"); m_tempDir.delete(); m_tempDir.mkdirs(); m_logger = new LogServiceImpl(); } @Override protected void tearDown() throws Exception { Utils.removeDirectoryWithContent(m_tempDir); } private AutoConfResourceProcessor createAutoConfRP() { AutoConfResourceProcessor p = new AutoConfResourceProcessor(); Utils.configureObject(p, LogService.class, m_logger); Utils.configureObject(p, DependencyManager.class, createMockDM()); Utils.configureObject(p, PersistencyManager.class, new PersistencyManager(m_tempDir)); return p; } @SuppressWarnings("unused") private BundleContext createMockBundleContext() { return Utils.createMockObjectAdapter(BundleContext.class, new Object() { public Filter createFilter(String condition) { return Utils.createMockObjectAdapter(Filter.class, new Object() { public boolean match(ServiceReference ref) { Object id = ref.getProperty("id"); if (id != null && id.equals(Integer.valueOf(42))) { return true; } return false; } public void remove(Component service) { } }); } }); } @SuppressWarnings("unused") private Component createMockComponent() { return Utils.createMockObjectAdapter(Component.class, new Object() { public DependencyManager getDependencyManager() { return new DependencyManager(createMockBundleContext()); } }); } private DependencyManager createMockDM() { return new DependencyManager(createMockBundleContext()) { public void remove(Component service) { } }; } private DeploymentSession createNewSession(AutoConfResourceProcessor p) { DeploymentSessionImpl s = new DeploymentSessionImpl(); p.begin(s); Utils.configureObject(p, Component.class, createMockComponent()); return s; } }