diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/action/lifecycle/Config.java b/shell/core/src/main/java/org/apache/karaf/shell/api/action/lifecycle/Config.java new file mode 100644 index 00000000000..0b4691d2dc8 --- /dev/null +++ b/shell/core/src/main/java/org/apache/karaf/shell/api/action/lifecycle/Config.java @@ -0,0 +1,51 @@ +/* + * 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.karaf.shell.api.action.lifecycle; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Injects OSGi ConfigurationAdmin properties into a field. + * + *

The annotated field must be of type {@code Map}. The + * configuration properties for the given PID are retrieved from + * {@link org.osgi.service.cm.ConfigurationAdmin} and injected as a + * {@link java.util.LinkedHashMap}. + * + *

Usage example: + *

+ * @Config(value = "service.pid", pid = "foo.bar")
+ * Map<String, Object> properties = new LinkedHashMap<>();
+ * 
+ * + * If the configuration PID does not exist, an empty map is injected. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface Config { + + /** + * The configuration PID to look up in ConfigurationAdmin. + */ + String pid(); + +} diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java index be9af5cfcb1..0438d5113ca 100644 --- a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java +++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ManagerImpl.java @@ -21,14 +21,18 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.karaf.shell.api.action.Action; import org.apache.karaf.shell.api.action.Command; +import org.apache.karaf.shell.api.action.lifecycle.Config; import org.apache.karaf.shell.api.action.lifecycle.Destroy; import org.apache.karaf.shell.api.action.lifecycle.Init; import org.apache.karaf.shell.api.action.lifecycle.Manager; @@ -38,9 +42,15 @@ import org.apache.karaf.shell.api.console.Parser; import org.apache.karaf.shell.api.console.Registry; import org.apache.karaf.shell.support.converter.GenericType; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ManagerImpl implements Manager { + private static final Logger LOGGER = LoggerFactory.getLogger(ManagerImpl.class); + private final Registry dependencies; private final Registry registrations; private final Map, Object> instances = new HashMap<>(); @@ -96,6 +106,40 @@ public T instantiate(Class clazz, Registry registry) throws Exc } } } + // Inject configuration properties + ConfigurationAdmin configAdmin = registry.getService(ConfigurationAdmin.class); + if (configAdmin == null && registry != this.dependencies) { + configAdmin = this.dependencies.getService(ConfigurationAdmin.class); + } + for (Class cl = clazz; cl != Object.class; cl = cl.getSuperclass()) { + for (Field field : cl.getDeclaredFields()) { + Config cfg = field.getAnnotation(Config.class); + if (cfg != null) { + Map props = new LinkedHashMap<>(); + if (configAdmin != null) { + try { + Configuration configuration = configAdmin.getConfiguration(cfg.pid(), "?"); + if (configuration != null) { + Dictionary dict = configuration.getProperties(); + if (dict != null) { + Enumeration keys = dict.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + props.put(key, dict.get(key)); + } + } + } + } catch (Exception e) { + LOGGER.warn("Unable to retrieve configuration for PID {}", cfg.pid(), e); + } + } else { + LOGGER.debug("ConfigurationAdmin service not available, injecting empty map for PID {}", cfg.pid()); + } + field.setAccessible(true); + field.set(instance, props); + } + } + } for (Method method : clazz.getDeclaredMethods()) { Init ann = method.getAnnotation(Init.class); if (ann != null && method.getParameterTypes().length == 0 && method.getReturnType() == void.class) { diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java index a424c26a933..ff56b24d5b7 100644 --- a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java +++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/osgi/CommandExtension.java @@ -30,6 +30,7 @@ import org.apache.felix.utils.extender.Extension; import org.apache.felix.utils.manifest.Clause; import org.apache.felix.utils.manifest.Parser; +import org.apache.karaf.shell.api.action.lifecycle.Config; import org.apache.karaf.shell.api.action.lifecycle.Manager; import org.apache.karaf.shell.api.action.lifecycle.Reference; import org.apache.karaf.shell.api.action.lifecycle.Service; @@ -43,6 +44,7 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.cm.ConfigurationAdmin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -176,8 +178,12 @@ private void inspectClass(final Class clazz) throws Exception { return; } // Create trackers + boolean needsConfigAdmin = false; for (Class cl = clazz; cl != Object.class; cl = cl.getSuperclass()) { for (Field field : cl.getDeclaredFields()) { + if (field.getAnnotation(Config.class) != null) { + needsConfigAdmin = true; + } Reference ref = field.getAnnotation(Reference.class); if (ref != null) { GenericType type = new GenericType(field.getGenericType()); @@ -194,6 +200,9 @@ private void inspectClass(final Class clazz) throws Exception { } } } + if (needsConfigAdmin && !registry.hasService(ConfigurationAdmin.class)) { + tracker.trackSingle(ConfigurationAdmin.class, false, ""); + } classes.add(clazz); } diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java index 440e05afea8..3cf7b175b5e 100644 --- a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java +++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java @@ -77,7 +77,7 @@ public void start() throws Exception { final Subject subject = createLocalKarafSubject(); this.session = JaasHelper.doAs(subject, (PrivilegedAction) () -> { String encoding = getEncoding(); - PrintStream pout = new PrintStream(terminal.output()) { + PrintStream pout = new PrintStream(terminal.output(), true, Charset.forName(encoding)) { @Override public void close() { // do nothing diff --git a/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ManagerImplConfigTest.java b/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ManagerImplConfigTest.java new file mode 100644 index 00000000000..4ef8d7a8400 --- /dev/null +++ b/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ManagerImplConfigTest.java @@ -0,0 +1,235 @@ +/* + * 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.karaf.shell.impl.action.command; + +import java.util.Hashtable; +import java.util.Map; + +import org.apache.karaf.shell.api.action.Action; +import org.apache.karaf.shell.api.action.Command; +import org.apache.karaf.shell.api.action.lifecycle.Config; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.karaf.shell.api.console.Registry; +import org.junit.Test; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +public class ManagerImplConfigTest { + + @Command(scope = "test", name = "config-single") + @Service + public static class SingleConfigCommand implements Action { + @Config(pid = "my.pid") + Map config; + + @Override + public Object execute() throws Exception { + return null; + } + } + + @Command(scope = "test", name = "config-multi") + @Service + public static class MultiConfigCommand implements Action { + @Config(pid = "pid.one") + Map configOne; + + @Config(pid = "pid.two") + Map configTwo; + + @Override + public Object execute() throws Exception { + return null; + } + } + + @Command(scope = "test", name = "config-empty") + @Service + public static class EmptyConfigCommand implements Action { + @Config(pid = "nonexistent.pid") + Map config; + + @Override + public Object execute() throws Exception { + return null; + } + } + + @Test + public void testConfigInjection() throws Exception { + Hashtable props = new Hashtable<>(); + props.put("key1", "value1"); + props.put("key2", 42); + + Configuration configuration = createMock(Configuration.class); + expect(configuration.getProperties()).andReturn(props); + replay(configuration); + + ConfigurationAdmin configAdmin = createMock(ConfigurationAdmin.class); + expect(configAdmin.getConfiguration("my.pid", "?")).andReturn(configuration); + replay(configAdmin); + + Registry registry = createMock(Registry.class); + expect(registry.getService(ConfigurationAdmin.class)).andReturn(configAdmin); + replay(registry); + + ManagerImpl manager = new ManagerImpl(registry, registry, true); + SingleConfigCommand cmd = manager.instantiate(SingleConfigCommand.class, registry); + + assertNotNull(cmd.config); + assertEquals("value1", cmd.config.get("key1")); + assertEquals(42, cmd.config.get("key2")); + assertEquals(2, cmd.config.size()); + + verify(configuration, configAdmin, registry); + } + + @Test + public void testConfigInjectionWithNullProperties() throws Exception { + Configuration configuration = createMock(Configuration.class); + expect(configuration.getProperties()).andReturn(null); + replay(configuration); + + ConfigurationAdmin configAdmin = createMock(ConfigurationAdmin.class); + expect(configAdmin.getConfiguration("nonexistent.pid", "?")).andReturn(configuration); + replay(configAdmin); + + Registry registry = createMock(Registry.class); + expect(registry.getService(ConfigurationAdmin.class)).andReturn(configAdmin); + replay(registry); + + ManagerImpl manager = new ManagerImpl(registry, registry, true); + EmptyConfigCommand cmd = manager.instantiate(EmptyConfigCommand.class, registry); + + assertNotNull(cmd.config); + assertTrue(cmd.config.isEmpty()); + + verify(configuration, configAdmin, registry); + } + + @Test + public void testConfigInjectionWithoutConfigAdmin() throws Exception { + Registry registry = createMock(Registry.class); + expect(registry.getService(ConfigurationAdmin.class)).andReturn(null); + replay(registry); + + ManagerImpl manager = new ManagerImpl(registry, registry, true); + SingleConfigCommand cmd = manager.instantiate(SingleConfigCommand.class, registry); + + assertNotNull(cmd.config); + assertTrue(cmd.config.isEmpty()); + + verify(registry); + } + + @Test + public void testMultipleConfigInjection() throws Exception { + Hashtable props1 = new Hashtable<>(); + props1.put("host", "localhost"); + props1.put("port", 8080); + + Hashtable props2 = new Hashtable<>(); + props2.put("timeout", 30000L); + + Configuration config1 = createMock(Configuration.class); + expect(config1.getProperties()).andReturn(props1); + replay(config1); + + Configuration config2 = createMock(Configuration.class); + expect(config2.getProperties()).andReturn(props2); + replay(config2); + + ConfigurationAdmin configAdmin = createMock(ConfigurationAdmin.class); + expect(configAdmin.getConfiguration("pid.one", "?")).andReturn(config1); + expect(configAdmin.getConfiguration("pid.two", "?")).andReturn(config2); + replay(configAdmin); + + Registry registry = createMock(Registry.class); + expect(registry.getService(ConfigurationAdmin.class)).andReturn(configAdmin); + replay(registry); + + ManagerImpl manager = new ManagerImpl(registry, registry, true); + MultiConfigCommand cmd = manager.instantiate(MultiConfigCommand.class, registry); + + assertNotNull(cmd.configOne); + assertEquals("localhost", cmd.configOne.get("host")); + assertEquals(8080, cmd.configOne.get("port")); + assertEquals(2, cmd.configOne.size()); + + assertNotNull(cmd.configTwo); + assertEquals(30000L, cmd.configTwo.get("timeout")); + assertEquals(1, cmd.configTwo.size()); + + verify(config1, config2, configAdmin, registry); + } + + @Test + public void testConfigInjectionWithConfigAdminException() throws Exception { + ConfigurationAdmin configAdmin = createMock(ConfigurationAdmin.class); + expect(configAdmin.getConfiguration("my.pid", "?")).andThrow(new java.io.IOException("config error")); + replay(configAdmin); + + Registry registry = createMock(Registry.class); + expect(registry.getService(ConfigurationAdmin.class)).andReturn(configAdmin); + replay(registry); + + ManagerImpl manager = new ManagerImpl(registry, registry, true); + SingleConfigCommand cmd = manager.instantiate(SingleConfigCommand.class, registry); + + // Should get empty map when ConfigAdmin throws + assertNotNull(cmd.config); + assertTrue(cmd.config.isEmpty()); + + verify(configAdmin, registry); + } + + @Test + public void testConfigInjectionFallsBackToDependencies() throws Exception { + Hashtable props = new Hashtable<>(); + props.put("key", "value"); + + Configuration configuration = createMock(Configuration.class); + expect(configuration.getProperties()).andReturn(props); + replay(configuration); + + ConfigurationAdmin configAdmin = createMock(ConfigurationAdmin.class); + expect(configAdmin.getConfiguration("my.pid", "?")).andReturn(configuration); + replay(configAdmin); + + // Local registry returns null, dependencies registry has ConfigAdmin + Registry localRegistry = createMock(Registry.class); + expect(localRegistry.getService(ConfigurationAdmin.class)).andReturn(null); + replay(localRegistry); + + Registry dependenciesRegistry = createMock(Registry.class); + expect(dependenciesRegistry.getService(ConfigurationAdmin.class)).andReturn(configAdmin); + replay(dependenciesRegistry); + + ManagerImpl manager = new ManagerImpl(dependenciesRegistry, dependenciesRegistry, true); + SingleConfigCommand cmd = manager.instantiate(SingleConfigCommand.class, localRegistry); + + assertNotNull(cmd.config); + assertEquals("value", cmd.config.get("key")); + + verify(configuration, configAdmin, localRegistry, dependenciesRegistry); + } +} diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellCommand.java b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellCommand.java index 39d0adcd19c..0023522ed4b 100644 --- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellCommand.java +++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellCommand.java @@ -100,7 +100,7 @@ public void run() { commandThread = Thread.currentThread(); int exitStatus = 0; try { - session = sessionFactory.create(in, new PrintStream(out), new PrintStream(err)); + session = sessionFactory.create(in, new PrintStream(out, true), new PrintStream(err, true)); for (Map.Entry e : env.getEnv().entrySet()) { session.put(e.getKey(), e.getValue()); }