diff --git a/converter.bundle/.gitignore b/converter.bundle/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/converter.bundle/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/converter.bundle/pom.xml b/converter.bundle/pom.xml
new file mode 100644
index 0000000..3739845
--- /dev/null
+++ b/converter.bundle/pom.xml
@@ -0,0 +1,93 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ converter.bundle
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/converter.bundle/src/main/java/org/eclipse/osgi/technology/command/converter/bundle/Activator.java b/converter.bundle/src/main/java/org/eclipse/osgi/technology/command/converter/bundle/Activator.java
new file mode 100644
index 0000000..5046df7
--- /dev/null
+++ b/converter.bundle/src/main/java/org/eclipse/osgi/technology/command/converter/bundle/Activator.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.converter.bundle;
+
+import java.util.Hashtable;
+
+import org.apache.felix.service.command.Converter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator implements BundleActivator {
+
+ private ServiceRegistration reg;
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ var properties = new Hashtable();
+ properties.put(Constants.SERVICE_RANKING, 10000);
+ reg = context.registerService(Converter.class, new BundleConverter(context), properties);
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ if (reg != null) {
+ reg.unregister();
+ }
+ }
+}
diff --git a/converter.bundle/src/main/java/org/eclipse/osgi/technology/command/converter/bundle/BundleConverter.java b/converter.bundle/src/main/java/org/eclipse/osgi/technology/command/converter/bundle/BundleConverter.java
new file mode 100644
index 0000000..474bec2
--- /dev/null
+++ b/converter.bundle/src/main/java/org/eclipse/osgi/technology/command/converter/bundle/BundleConverter.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+
+package org.eclipse.osgi.technology.command.converter.bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.felix.service.command.Converter;
+import org.eclipse.osgi.technology.command.util.GlobFilter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+
+@org.osgi.annotation.bundle.Capability(
+ namespace = "osgi.commands",
+ name = "converter.bundle",
+ version = "1.0.0"
+)
+public class BundleConverter implements Converter {
+
+ private BundleContext context;
+
+ public BundleConverter(BundleContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public Object convert(Class> desiredType, Object sourceObject) throws Exception {
+ if (desiredType == Bundle.class) {
+ if (sourceObject instanceof Number n) {
+ var bundle = context.getBundle(n.longValue());
+ if (bundle != null) {
+ return bundle;
+ }
+ } else if (sourceObject instanceof String sourceString) {
+ try {
+ var bundleId = Long.parseLong(sourceString);
+ return context.getBundle(bundleId);
+ } catch (Exception e) {
+
+ }
+
+ for (Bundle b : context.getBundles()) {
+
+ if (b.getSymbolicName().equals(sourceString)) {
+ return b;
+ }
+ }
+
+ GlobFilter glob = new GlobFilter(sourceString);
+ List matchingBundles = new ArrayList<>();
+ for (Bundle b : context.getBundles()) {
+ if (glob.matches(b.getSymbolicName())) {
+ matchingBundles.add(b);
+ }
+ }
+
+ if (matchingBundles.size() == 1) {
+ return matchingBundles.get(0);
+ }
+ }
+
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence format(Object target, int level, Converter escape) throws Exception {
+ return null;
+ }
+
+}
diff --git a/converter.bundle/src/test/java/org/eclipse/osgi/technology/command/converter/bundle/Test.java b/converter.bundle/src/test/java/org/eclipse/osgi/technology/command/converter/bundle/Test.java
new file mode 100644
index 0000000..919bf34
--- /dev/null
+++ b/converter.bundle/src/test/java/org/eclipse/osgi/technology/command/converter/bundle/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.converter.bundle;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/converter.file/.gitignore b/converter.file/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/converter.file/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/converter.file/pom.xml b/converter.file/pom.xml
new file mode 100644
index 0000000..1226dd5
--- /dev/null
+++ b/converter.file/pom.xml
@@ -0,0 +1,93 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ converter.file
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/converter.file/src/main/java/org/eclipse/osgi/technology/command/converter/file/Activator.java b/converter.file/src/main/java/org/eclipse/osgi/technology/command/converter/file/Activator.java
new file mode 100644
index 0000000..672b8ff
--- /dev/null
+++ b/converter.file/src/main/java/org/eclipse/osgi/technology/command/converter/file/Activator.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.converter.file;
+
+import java.util.Hashtable;
+
+import org.apache.felix.service.command.Converter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator implements BundleActivator {
+
+ private ServiceRegistration reg;
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+
+ var properties = new Hashtable();
+ properties.put(Constants.SERVICE_RANKING, 10000);
+ reg = context.registerService(Converter.class, new FileConverter(), properties);
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ if (reg != null) {
+ reg.unregister();
+ }
+ }
+}
diff --git a/converter.file/src/main/java/org/eclipse/osgi/technology/command/converter/file/FileConverter.java b/converter.file/src/main/java/org/eclipse/osgi/technology/command/converter/file/FileConverter.java
new file mode 100644
index 0000000..89cb35f
--- /dev/null
+++ b/converter.file/src/main/java/org/eclipse/osgi/technology/command/converter/file/FileConverter.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+
+package org.eclipse.osgi.technology.command.converter.file;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import org.apache.felix.service.command.Converter;
+
+public class FileConverter implements Converter {
+
+
+ public FileConverter() {
+ }
+
+ @Override
+ public Object convert(Class> desiredType, Object sourceObject) throws Exception {
+ if (desiredType == File.class && sourceObject instanceof String s) {
+ return Path.of(s).toFile();
+ }
+ if (desiredType == Path.class && sourceObject instanceof String s) {
+ return Path.of(s);
+ }
+ if (desiredType == Path.class && sourceObject instanceof File f) {
+ return f.toPath();
+ }
+ if (desiredType == File.class && sourceObject instanceof Path p) {
+ return p.toFile();
+ }
+ return null;
+ }
+
+ @Override
+ public CharSequence format(Object target, int level, Converter escape) throws Exception {
+ return null;
+ }
+
+}
diff --git a/converter.file/src/test/java/org/eclipse/osgi/technology/command/converter/file/Test.java b/converter.file/src/test/java/org/eclipse/osgi/technology/command/converter/file/Test.java
new file mode 100644
index 0000000..a5794e5
--- /dev/null
+++ b/converter.file/src/test/java/org/eclipse/osgi/technology/command/converter/file/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.converter.file;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/diagnostics/.gitignore b/diagnostics/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/diagnostics/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/diagnostics/bnd.bnd b/diagnostics/bnd.bnd
new file mode 100644
index 0000000..b71c964
--- /dev/null
+++ b/diagnostics/bnd.bnd
@@ -0,0 +1,2 @@
+Import-Package: \
+ *
\ No newline at end of file
diff --git a/diagnostics/play.bndrun b/diagnostics/play.bndrun
new file mode 100644
index 0000000..13f6ac6
--- /dev/null
+++ b/diagnostics/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.diagnostics;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.diagnostics-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/diagnostics/pom.xml b/diagnostics/pom.xml
new file mode 100644
index 0000000..2a4a00e
--- /dev/null
+++ b/diagnostics/pom.xml
@@ -0,0 +1,92 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ diagnostics
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/Activator.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/Activator.java
new file mode 100644
index 0000000..972de7c
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/Activator.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new DiagnosticsCommand(context, formatter), "tech", Map.of());
+ registerCommandService(new InspectCommand(context, formatter), "tech", Map.of());
+
+ registerConverterService(new DiagnosticsConverter(formatter));
+ }
+
+ private static class DiagnosticsConverter extends BaseDTOFormatterConverter {
+
+ public DiagnosticsConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/DiagnosticsCommand.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/DiagnosticsCommand.java
new file mode 100644
index 0000000..df33103
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/DiagnosticsCommand.java
@@ -0,0 +1,295 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.diagnostics.util.Export;
+import org.eclipse.osgi.technology.command.diagnostics.util.FilterListener;
+import org.eclipse.osgi.technology.command.diagnostics.util.Search;
+import org.eclipse.osgi.technology.command.util.GlobFilter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+
+public class DiagnosticsCommand implements Closeable {
+
+ private final BundleContext context;
+ private final FilterListener fl;
+
+ public DiagnosticsCommand(BundleContext context, DTOFormatter dtof) {
+ this.context = context;
+ this.fl = new FilterListener(context);
+ }
+
+ @Override
+ public void close() {
+ fl.close();
+ }
+
+ @Descriptor("Show all requirements. Iterates over all (or one) bundles and gathers their requirements.")
+ public List reqs(
+ @Descriptor("Only show the requirements of the given bundle") @Parameter(names = { "-b",
+ "--bundle" }, absentValue = "*") GlobFilter bundle,
+ @Descriptor("Only show the requirements when one of the given namespace matches. You can use wildcards.") GlobFilter... ns) {
+
+ List reqs = new ArrayList<>();
+
+ for (Bundle b : context.getBundles()) {
+ if (!bundle.createMatcher(b.getSymbolicName()).matches()) {
+ continue;
+ }
+
+ var wiring = b.adapt(BundleWiring.class);
+
+ var requirements = wiring.getRequirements(null);
+ for (Requirement r : requirements) {
+ if (matches(ns, r.getNamespace())) {
+ reqs.add(r);
+ }
+ }
+ }
+ return reqs;
+ }
+
+ private boolean matches(GlobFilter[] ns, String namespace) {
+ if (ns == null || ns.length == 0) {
+ return true;
+ }
+
+ for (GlobFilter g : ns) {
+ if (g.matches(namespace)) {
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Descriptor("Show all capabilities of all bundles. It is possible to list by bundle and/or by a specific namespace.")
+ public List caps(
+ @Descriptor("Only show the capabilities of the given bundle") @Parameter(names = { "-b",
+ "--bundle" }, absentValue = "-1") long bundle,
+ @Descriptor("""
+ Only show the capabilities when the given namespace matches. You can use wildcards. A number of namespaces are shortcutted:
+ p = osgi.wiring.package
+ i = osgi.wiring.identity
+ h = osgi.wiring.host
+ b = osgi.wiring.bundle
+ e = osgi.extender
+ s = osgi.service
+ c = osgi.contract""") @Parameter(names = {
+ "-n", "--namespace" }, absentValue = "*") String ns) {
+
+ var nsg = shortcuts(ns);
+
+ List result = new ArrayList<>();
+
+ for (Bundle b : context.getBundles()) {
+ if (bundle != -1 && b.getBundleId() != bundle) {
+ continue;
+ }
+
+ var wiring = b.adapt(BundleWiring.class);
+
+ var capabilities = wiring.getCapabilities(null);
+ for (Capability r : capabilities) {
+ if (nsg.createMatcher(r.getNamespace()).matches()) {
+ result.add(r);
+ }
+ }
+ }
+ return result;
+ }
+
+ @Descriptor("""
+ Show bundles that are listening for certain services. This will check for the following fishy cases:\s
+ * ? – No matching registered service found
+ * ! – Matching registered service found in another classpace
+ The first set show is the set of bundle that register such a service in the proper class space. The second \
+ set (only shown when not empty) shows the bundles that have a registered service for this but are not \
+ compatible because they are registered in another class space""")
+ public List wanted(
+ @Descriptor("If specified will only show for the given bundle") @Parameter(names = { "-b",
+ "--bundle" }, absentValue = "-1") long exporter,
+ @Descriptor("If specified, this glob expression must match the name of the service class/interface name") @Parameter(names = {
+ "-n", "--name" }, absentValue = "*") GlobFilter name)
+ throws InvalidSyntaxException {
+ List searches = new ArrayList<>();
+ synchronized (fl) {
+ for (Map.Entry> e : fl.listenerContexts.entrySet()) {
+
+ var serviceName = e.getKey();
+
+ if (!name.createMatcher(serviceName).matches()) {
+ continue;
+ }
+
+ ServiceReference> refs[] = context.getAllServiceReferences(serviceName, null);
+ for (BundleContext bc : e.getValue()) {
+
+ if (exporter != -1 && exporter != bc.getBundle().getBundleId()) {
+ continue;
+ }
+
+ var wiring = bc.getBundle().adapt(BundleWiring.class);
+
+ var s = new Search();
+ s.serviceName = serviceName;
+ s.searcher = wiring.getRevision();
+
+ var classLoader = wiring.getClassLoader();
+ Class> type = load(classLoader, serviceName);
+
+ if (refs != null) {
+ for (ServiceReference> ref : refs) {
+ var registrar = ref.getBundle();
+
+ Class> registeredClass = load(registrar, serviceName);
+ var bundleId = registrar.getBundleId();
+ if (type == null || registeredClass == null || type == registeredClass) {
+ s.matched.add(bundleId);
+ } else {
+ s.mismatched.add(bundleId);
+ }
+ }
+ }
+ searches.add(s);
+ }
+ }
+ }
+ return searches;
+ }
+
+ @Descriptor("Show exported packages of all bundles that look fishy. Options are provided to filter for a specific bundle and/or the package name (glob). You can also specify -a for all packages")
+ public Collection exports(
+ @Descriptor("If specified will only show for the given bundle") @Parameter(names = { "-b",
+ "--bundle" }, absentValue = "-1") long exporter,
+ @Descriptor("If specified, this glob expression must match the name of the service class/interface name") @Parameter(names = {
+ "-n", "--name" }, absentValue = "*") GlobFilter name,
+ @Descriptor("Show all packages, not just the ones that look fishy") @Parameter(names = { "-a",
+ "--all" }, absentValue = "false", presentValue = "true") boolean all,
+ @Descriptor("Check exports against private packages") @Parameter(names = { "-p",
+ "--private" }, absentValue = "false", presentValue = "true") boolean privatePackages) {
+ Map map = new HashMap<>();
+
+ var caps = caps(-1, PackageNamespace.PACKAGE_NAMESPACE);
+
+ for (Capability c : caps) {
+ var packageName = (String) c.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
+ if (!name.createMatcher(packageName).matches()) {
+ continue;
+ }
+
+ var resource = c.getResource();
+ if (resource instanceof BundleRevision) {
+ var bundle = ((BundleRevision) resource).getBundle();
+ if (exporter != -1 && bundle.getBundleId() != exporter) {
+ continue;
+ }
+ var e = map.get(packageName);
+ if (e == null) {
+ e = new Export(packageName);
+ map.put(packageName, e);
+ }
+ e.exporters.add(bundle.getBundleId());
+ }
+
+ }
+
+ for (Export e : map.values()) {
+ for (Bundle b : context.getBundles()) {
+
+ if (e.exporters.contains(b.getBundleId())) {
+ continue;
+ }
+
+ if (hasPackage(b, e.pack)) {
+ e.privates.add(b.getBundleId());
+ }
+ }
+ }
+
+ if (all) {
+ return map.values();
+ }
+
+ Set s = new HashSet<>(map.values());
+ s.removeIf(e -> e.exporters.size() == 1 && e.privates.isEmpty());
+ return s;
+ }
+
+ private boolean hasPackage(Bundle b, String pack) {
+ var entries = b.findEntries(pack.replace('.', '/'), "*", false);
+ return entries != null && entries.hasMoreElements();
+ }
+
+ private Class> load(Bundle bundle, String name) {
+ return load(bundle.adapt(BundleWiring.class).getClassLoader(), name);
+ }
+
+ private Class> load(ClassLoader classLoader, String name) {
+ try {
+ return classLoader.loadClass(name);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private GlobFilter shortcuts(String ns) {
+ switch (ns) {
+ case "p":
+ ns = "osgi.wiring.package";
+ break;
+
+ case "i":
+ ns = "osgi.wiring.identity";
+ break;
+ case "h":
+ ns = "osgi.wiring.host";
+ break;
+ case "b":
+ ns = "osgi.wiring.bundle";
+ break;
+ case "e":
+ ns = "osgi.extender";
+ break;
+ case "s":
+ ns = "osgi.service";
+ break;
+ case "c":
+ ns = "osgi.contract";
+ break;
+ }
+ var nsg = new GlobFilter(ns);
+ return nsg;
+ }
+
+}
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/InspectCommand.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/InspectCommand.java
new file mode 100644
index 0000000..211a103
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/InspectCommand.java
@@ -0,0 +1,319 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.service.command.Descriptor;
+import org.eclipse.osgi.technology.command.diagnostics.util.Util;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+
+public class InspectCommand {
+ public static final String NONSTANDARD_SERVICE_NAMESPACE = "service";
+
+ public static final String CAPABILITY = "capability";
+ public static final String REQUIREMENT = "requirement";
+
+ private static final String EMPTY_MESSAGE = "[EMPTY]";
+ private static final String UNUSED_MESSAGE = "[UNUSED]";
+ private static final String UNRESOLVED_MESSAGE = "[UNRESOLVED]";
+
+ private final BundleContext m_bc;
+
+ public InspectCommand(BundleContext bc, DTOFormatter formatter) {
+ m_bc = bc;
+ }
+
+ @Descriptor("inspects bundle capabilities and requirements")
+ public void inspect(@Descriptor("('capability' | 'requirement')") String direction,
+ @Descriptor("( | 'service')") String namespace, @Descriptor("target bundles") Bundle[] bundles) {
+ inspect(m_bc, direction, namespace, bundles);
+ }
+
+ private static void inspect(BundleContext bc, String direction, String namespace, Bundle[] bundles) {
+ // Verify arguments.
+ if (isValidDirection(direction)) {
+ bundles = ((bundles == null) || (bundles.length == 0)) ? bc.getBundles() : bundles;
+
+ if (CAPABILITY.startsWith(direction)) {
+ printCapabilities(bc, Util.parseSubstring(namespace), bundles);
+ } else {
+ printRequirements(bc, Util.parseSubstring(namespace), bundles);
+ }
+ } else {
+ if (!isValidDirection(direction)) {
+ System.out.println("Invalid argument: " + direction);
+ }
+ }
+ }
+
+ public static void printCapabilities(BundleContext bc, List namespace, Bundle[] bundles) {
+ var separatorNeeded = false;
+ for (Bundle b : bundles) {
+ if (separatorNeeded) {
+ System.out.println();
+ }
+
+ // Print out any matching generic capabilities.
+ var wiring = b.adapt(BundleWiring.class);
+ if (wiring != null) {
+ var title = b + " provides:";
+ System.out.println(title);
+ System.out.println(Util.getUnderlineString(title.length()));
+
+ // Print generic capabilities for matching namespaces.
+ var matches = printMatchingCapabilities(wiring, namespace);
+
+ // Handle service capabilities separately, since they aren't
+ // part
+ // of the generic model in OSGi.
+ if (matchNamespace(namespace, NONSTANDARD_SERVICE_NAMESPACE)) {
+ matches |= printServiceCapabilities(b);
+ }
+
+ // If there were no capabilities for the specified namespace,
+ // then say so.
+ if (!matches) {
+ System.out.println(Util.unparseSubstring(namespace) + " " + EMPTY_MESSAGE);
+ }
+ } else {
+ System.out.println("Bundle " + b.getBundleId() + " is not resolved.");
+ }
+ separatorNeeded = true;
+ }
+ }
+
+ private static boolean printMatchingCapabilities(BundleWiring wiring, List namespace) {
+ var wires = wiring.getProvidedWires(null);
+ var aggregateCaps = aggregateCapabilities(namespace, wires);
+ var allCaps = wiring.getCapabilities(null);
+ var matches = false;
+ for (BundleCapability cap : allCaps) {
+ if (matchNamespace(namespace, cap.getNamespace())) {
+ matches = true;
+ var dependents = aggregateCaps.get(cap);
+ var keyAttr = cap.getAttributes().get(cap.getNamespace());
+ if (dependents != null) {
+ String msg;
+ if (keyAttr != null) {
+ msg = cap.getNamespace() + "; " + keyAttr + " " + getVersionFromCapability(cap);
+ } else {
+ msg = cap.toString();
+ }
+ msg = msg + " required by:";
+ System.out.println(msg);
+ for (BundleWire wire : dependents) {
+ System.out.println(" " + wire.getRequirerWiring().getBundle());
+ }
+ } else if (keyAttr != null) {
+ System.out.println(cap.getNamespace() + "; " + cap.getAttributes().get(cap.getNamespace()) + " "
+ + getVersionFromCapability(cap) + " " + UNUSED_MESSAGE);
+ } else {
+ System.out.println(cap + " " + UNUSED_MESSAGE);
+ }
+ }
+ }
+ return matches;
+ }
+
+ private static Map> aggregateCapabilities(List namespace,
+ List wires) {
+ // Aggregate matching capabilities.
+ Map> map = new HashMap<>();
+ for (BundleWire wire : wires) {
+ if (matchNamespace(namespace, wire.getCapability().getNamespace())) {
+ var dependents = map.get(wire.getCapability());
+ if (dependents == null) {
+ dependents = new ArrayList<>();
+ map.put(wire.getCapability(), dependents);
+ }
+ dependents.add(wire);
+ }
+ }
+ return map;
+ }
+
+ static boolean printServiceCapabilities(Bundle b) {
+ var matches = false;
+
+ try {
+ var refs = b.getRegisteredServices();
+
+ if ((refs != null) && (refs.length > 0)) {
+ matches = true;
+ // Print properties for each service.
+ for (ServiceReference> ref : refs) {
+ // Print object class with "namespace".
+ System.out.println(NONSTANDARD_SERVICE_NAMESPACE + "; "
+ + Util.getValueString(ref.getProperty("objectClass")) + " with properties:");
+ // Print service properties.
+ var keys = ref.getPropertyKeys();
+ for (String key : keys) {
+ if (!key.equalsIgnoreCase(Constants.OBJECTCLASS)) {
+ var v = ref.getProperty(key);
+ System.out.println(" " + key + " = " + Util.getValueString(v));
+ }
+ }
+ var users = ref.getUsingBundles();
+ if ((users != null) && (users.length > 0)) {
+ System.out.println(" Used by:");
+ for (Bundle user : users) {
+ System.out.println(" " + user);
+ }
+ }
+ }
+ }
+ } catch (Exception ex) {
+ System.err.println(ex.toString());
+ }
+
+ return matches;
+ }
+
+ public static void printRequirements(BundleContext bc, List namespace, Bundle[] bundles) {
+ var separatorNeeded = false;
+ for (Bundle b : bundles) {
+ if (separatorNeeded) {
+ System.out.println();
+ }
+
+ // Print out any matching generic requirements.
+ var wiring = b.adapt(BundleWiring.class);
+ if (wiring != null) {
+ var title = b + " requires:";
+ System.out.println(title);
+ System.out.println(Util.getUnderlineString(title.length()));
+ var matches = printMatchingRequirements(wiring, namespace);
+
+ // Handle service requirements separately, since they aren't
+ // part
+ // of the generic model in OSGi.
+ if (matchNamespace(namespace, NONSTANDARD_SERVICE_NAMESPACE)) {
+ matches |= printServiceRequirements(b);
+ }
+
+ // If there were no requirements for the specified namespace,
+ // then say so.
+ if (!matches) {
+ System.out.println(Util.unparseSubstring(namespace) + " " + EMPTY_MESSAGE);
+ }
+ } else {
+ System.out.println("Bundle " + b.getBundleId() + " is not resolved.");
+ }
+
+ separatorNeeded = true;
+ }
+ }
+
+ private static boolean printMatchingRequirements(BundleWiring wiring, List namespace) {
+ var wires = wiring.getRequiredWires(null);
+ var aggregateReqs = aggregateRequirements(namespace, wires);
+ var allReqs = wiring.getRequirements(null);
+ var matches = false;
+ for (BundleRequirement req : allReqs) {
+ if (matchNamespace(namespace, req.getNamespace())) {
+ matches = true;
+ var providers = aggregateReqs.get(req);
+ if (providers != null) {
+ System.out.println(req.getNamespace() + "; " + req.getDirectives().get(Constants.FILTER_DIRECTIVE)
+ + " resolved by:");
+ for (BundleWire wire : providers) {
+ String msg;
+ var keyAttr = wire.getCapability().getAttributes().get(wire.getCapability().getNamespace());
+ if (keyAttr != null) {
+ msg = wire.getCapability().getNamespace() + "; " + keyAttr + " "
+ + getVersionFromCapability(wire.getCapability());
+ } else {
+ msg = wire.getCapability().toString();
+ }
+ msg = " " + msg + " from " + wire.getProviderWiring().getBundle();
+ System.out.println(msg);
+ }
+ } else {
+ System.out.println(req.getNamespace() + "; " + req.getDirectives().get(Constants.FILTER_DIRECTIVE)
+ + " " + UNRESOLVED_MESSAGE);
+ }
+ }
+ }
+ return matches;
+ }
+
+ private static Map> aggregateRequirements(List namespace,
+ List wires) {
+ // Aggregate matching capabilities.
+ Map> map = new HashMap<>();
+ for (BundleWire wire : wires) {
+ if (matchNamespace(namespace, wire.getRequirement().getNamespace())) {
+ var providers = map.get(wire.getRequirement());
+ if (providers == null) {
+ providers = new ArrayList<>();
+ map.put(wire.getRequirement(), providers);
+ }
+ providers.add(wire);
+ }
+ }
+ return map;
+ }
+
+ static boolean printServiceRequirements(Bundle b) {
+ var matches = false;
+
+ try {
+ var refs = b.getServicesInUse();
+
+ if ((refs != null) && (refs.length > 0)) {
+ matches = true;
+ // Print properties for each service.
+ for (ServiceReference> ref : refs) {
+ // Print object class with "namespace".
+ System.out.println(NONSTANDARD_SERVICE_NAMESPACE + "; "
+ + Util.getValueString(ref.getProperty("objectClass")) + " provided by:");
+ System.out.println(" " + ref.getBundle());
+ }
+ }
+ } catch (Exception ex) {
+ System.err.println(ex.toString());
+ }
+
+ return matches;
+ }
+
+ private static String getVersionFromCapability(BundleCapability c) {
+ var o = c.getAttributes().get(Constants.VERSION_ATTRIBUTE);
+ if (o == null) {
+ o = c.getAttributes().get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+ }
+ return (o == null) ? "" : o.toString();
+ }
+
+ private static boolean matchNamespace(List namespace, String actual) {
+ return Util.compareSubstring(namespace, actual);
+ }
+
+ private static boolean isValidDirection(String direction) {
+ return (CAPABILITY.startsWith(direction) || REQUIREMENT.startsWith(direction));
+ }
+
+}
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Export.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Export.java
new file mode 100644
index 0000000..834bb3c
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Export.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics.util;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+public class Export {
+ public String pack;
+ public Set exporters = new TreeSet<>();
+ public Set privates = new TreeSet<>();
+
+ public Export(String packageName) {
+ pack = packageName;
+ }
+
+ @Override
+ public String toString() {
+ var sb = new StringBuilder();
+ sb.append(state()).append(" ").append(pack);
+
+ if (!exporters.isEmpty()) {
+ sb.append(" exporters=").append(exporters);
+ }
+ if (!privates.isEmpty()) {
+ sb.append(" privates=").append(privates);
+ }
+ return sb.toString();
+ }
+
+ private String state() {
+ return privates.isEmpty() ? " " : "!";
+ }
+}
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/FilterListener.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/FilterListener.java
new file mode 100644
index 0000000..f296c54
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/FilterListener.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+public class FilterListener implements ListenerHook {
+ private final static Pattern LISTENER_INFO_PATTERN = Pattern.compile("\\(objectClass=([^)]+)\\)");
+ public final Map> listenerContexts = new HashMap<>();
+
+ volatile boolean quiting;
+ private ServiceRegistration lhook;
+
+ public FilterListener(BundleContext context) {
+ lhook = context.registerService(ListenerHook.class, this, null);
+ }
+
+ @Override
+ public synchronized void added(Collection listeners) {
+ if (quiting) {
+ return;
+ }
+ for (Object o : listeners) {
+ addListenerInfo((ListenerInfo) o);
+ }
+ }
+
+ @Override
+ public synchronized void removed(Collection listeners) {
+ if (quiting) {
+ return;
+ }
+ for (Object o : listeners) {
+ removeListenerInfo((ListenerInfo) o);
+ }
+ }
+
+ private void addListenerInfo(ListenerInfo o) {
+ var filter = o.getFilter();
+ if (filter != null) {
+ var m = LISTENER_INFO_PATTERN.matcher(filter);
+ while (m.find()) {
+ listenerContexts.compute(m.group(1), (key, list) -> {
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ list.add(o.getBundleContext());
+ return list;
+ });
+
+ }
+ }
+ }
+
+ private void removeListenerInfo(ListenerInfo o) {
+ var filter = o.getFilter();
+ if (filter != null) {
+ var m = LISTENER_INFO_PATTERN.matcher(filter);
+ while (m.find()) {
+ listenerContexts.computeIfPresent(m.group(1), (key, list) -> {
+
+ list.remove(o.getBundleContext());
+ return list;
+ });
+ }
+ }
+ }
+
+ public void close() {
+ quiting = true;
+ lhook.unregister();
+ }
+}
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Search.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Search.java
new file mode 100644
index 0000000..a1184b1
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Search.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics.util;
+
+import java.util.Formatter;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.felix.service.command.Converter;
+import org.osgi.framework.wiring.BundleRevision;
+
+public class Search implements Converter {
+ public String serviceName;
+ public BundleRevision searcher;
+ public Set matched = new HashSet<>();
+ public Set mismatched = new HashSet<>();
+
+ @Override
+ public String toString() {
+ var sb = new StringBuilder();
+ sb.append(getState());
+
+ sb.append("[").append(searcher.getBundle().getBundleId()).append("] ");
+ sb.append(serviceName);
+
+ sb.append(" ").append(matched);
+ if (!mismatched.isEmpty()) {
+ sb.append(" !! ").append(mismatched);
+ }
+ return sb.toString();
+ }
+
+ private String getState() {
+ if (!mismatched.isEmpty()) {
+ return "! ";
+ } else if (matched.isEmpty()) {
+ return "? ";
+ } else {
+ return " ";
+ }
+ }
+
+ @Override
+ public Object convert(Class> targetType, Object source) throws Exception {
+ return null;
+ }
+
+ @Override
+ public CharSequence format(Object source, int level, Converter next) throws Exception {
+ switch (level) {
+ case Converter.INSPECT:
+ return inspect(this, next);
+
+ case Converter.LINE:
+ return line(this, next);
+
+ case Converter.PART:
+ return part(this, next);
+ }
+ return null;
+ }
+
+ private CharSequence part(Search search, Converter next) {
+ return toString();
+ }
+
+ private CharSequence line(Search search, Converter next) throws Exception {
+ try (var f = new Formatter()) {
+ f.format("%s %-60s %-50s %s %s", getState(), search.serviceName, search.searcher.getBundle(),
+ search.matched.isEmpty() ? "" : search.matched,
+ search.mismatched.isEmpty() ? "" : "!! " + search.mismatched);
+ return f.toString();
+ }
+ }
+
+ private CharSequence inspect(Search search, Converter next) throws Exception {
+ var context = search.searcher.getBundle().getBundleContext();
+
+ try (var f = new Formatter()) {
+ f.format("Searching Bundle %s\n", search.searcher.getBundle());
+ f.format("Service Name %s\n", search.serviceName);
+ if (!search.matched.isEmpty()) {
+ f.format("Registrars in same class space \n");
+ for (Long b : search.matched) {
+ var bundle = context.getBundle(b);
+ f.format(" %s\n", next.format(bundle, Converter.LINE, next));
+ }
+ }
+
+ if (!search.mismatched.isEmpty()) {
+ f.format("!!! Registrars in different class space \n");
+ for (Long b : search.mismatched) {
+ var bundle = context.getBundle(b);
+ f.format(" %s\n", next.format(bundle, Converter.LINE, next));
+ }
+ }
+ return f.toString();
+ }
+ }
+
+}
diff --git a/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Util.java b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Util.java
new file mode 100644
index 0000000..f3df7fb
--- /dev/null
+++ b/diagnostics/src/main/java/org/eclipse/osgi/technology/command/diagnostics/util/Util.java
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Util {
+
+ static final String CWD = "_cwd";
+
+ private final static StringBuffer m_sb = new StringBuffer();
+
+ public static String getUnderlineString(int len) {
+ synchronized (m_sb) {
+ m_sb.delete(0, m_sb.length());
+ for (var i = 0; i < len; i++) {
+ m_sb.append('-');
+ }
+ return m_sb.toString();
+ }
+ }
+
+ public static String getValueString(Object obj) {
+ synchronized (m_sb) {
+ if (obj instanceof String) {
+ return (String) obj;
+ } else if (obj instanceof String[] array) {
+ m_sb.delete(0, m_sb.length());
+ for (var i = 0; i < array.length; i++) {
+ if (i != 0) {
+ m_sb.append(", ");
+ }
+ m_sb.append(array[i]);
+ }
+ return m_sb.toString();
+ } else if (obj instanceof Boolean) {
+ return ((Boolean) obj).toString();
+ } else if (obj instanceof Long) {
+ return ((Long) obj).toString();
+ } else if (obj instanceof Integer) {
+ return ((Integer) obj).toString();
+ } else if (obj instanceof Short) {
+ return ((Short) obj).toString();
+ } else if (obj instanceof Double) {
+ return obj.toString();
+ } else if (obj instanceof Float) {
+ return obj.toString();
+ } else if (obj == null) {
+ return "null";
+ } else {
+ return obj.toString();
+ }
+ }
+ }
+
+ public static List parseSubstring(String value) {
+ List pieces = new ArrayList<>();
+ var ss = new StringBuilder();
+ // int kind = SIMPLE; // assume until proven otherwise
+ var wasStar = false; // indicates last piece was a star
+ var leftstar = false; // track if the initial piece is a star
+ var rightstar = false; // track if the final piece is a star
+
+ var idx = 0;
+
+ // We assume (sub)strings can contain leading and trailing blanks
+ var escaped = false;
+ loop: for (;;) {
+ if (idx >= value.length()) {
+ if (wasStar) {
+ // insert last piece as "" to handle trailing star
+ rightstar = true;
+ } else {
+ pieces.add(ss.toString());
+ // accumulate the last piece
+ // note that in the case of
+ // (cn=); this might be
+ // the string "" (!=null)
+ }
+ ss.setLength(0);
+ break loop;
+ }
+
+ // Read the next character and account for escapes.
+ var c = value.charAt(idx++);
+ if (!escaped && ((c == '(') || (c == ')'))) {
+ throw new IllegalArgumentException("Illegal value: " + value);
+ } else if (!escaped && (c == '*')) {
+ if (wasStar) {
+ // encountered two successive stars;
+ // I assume this is illegal
+ throw new IllegalArgumentException("Invalid filter string: " + value);
+ }
+ if (ss.length() > 0) {
+ pieces.add(ss.toString()); // accumulate the pieces
+ // between '*' occurrences
+ }
+ ss.setLength(0);
+ // if this is a leading star, then track it
+ if (pieces.isEmpty()) {
+ leftstar = true;
+ }
+ wasStar = true;
+ } else if (!escaped && (c == '\\')) {
+ escaped = true;
+ } else {
+ escaped = false;
+ wasStar = false;
+ ss.append(c);
+ }
+ }
+ if (leftstar || rightstar || pieces.size() > 1) {
+ // insert leading and/or trailing "" to anchor ends
+ if (rightstar) {
+ pieces.add("");
+ }
+ if (leftstar) {
+ pieces.add(0, "");
+ }
+ }
+ return pieces;
+ }
+
+ public static String unparseSubstring(List pieces) {
+ var sb = new StringBuilder();
+ for (var i = 0; i < pieces.size(); i++) {
+ if (i > 0) {
+ sb.append("*");
+ }
+ sb.append(pieces.get(i));
+ }
+ return sb.toString();
+ }
+
+ public static boolean compareSubstring(List pieces, String s) {
+ // Walk the pieces to match the string
+ // There are implicit stars between each piece,
+ // and the first and last pieces might be "" to anchor the match.
+ // assert (pieces.length > 1)
+ // minimal case is *
+
+ var len = pieces.size();
+
+ // Special case, if there is only one piece, then
+ // we must perform an equality test.
+ if (len == 1) {
+ return s.equals(pieces.get(0));
+ }
+
+ // Otherwise, check whether the pieces match
+ // the specified string.
+
+ var index = 0;
+
+ for (var i = 0; i < len; i++) {
+ var piece = pieces.get(i);
+
+ // If this is the first piece, then make sure the
+ // string starts with it.
+ if (i == 0) {
+ if (!s.startsWith(piece)) {
+ return false;
+ }
+ }
+
+ // If this is the last piece, then make sure the
+ // string ends with it.
+ if (i == len - 1) {
+ return s.endsWith(piece);
+ }
+
+ // If this is neither the first or last piece, then
+ // make sure the string contains it.
+ if (i > 0) {
+ index = s.indexOf(piece, index);
+ if (index < 0) {
+ return false;
+ }
+ }
+
+ // Move string index beyond the matching piece.
+ index += piece.length();
+ }
+
+ return true;
+ }
+
+}
diff --git a/diagnostics/src/test/java/org/eclipse/osgi/technology/command/diagnostics/Test.java b/diagnostics/src/test/java/org/eclipse/osgi/technology/command/diagnostics/Test.java
new file mode 100644
index 0000000..5caf799
--- /dev/null
+++ b/diagnostics/src/test/java/org/eclipse/osgi/technology/command/diagnostics/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.diagnostics;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/fs.navigate/.gitignore b/fs.navigate/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/fs.navigate/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/fs.navigate/bnd.bnd b/fs.navigate/bnd.bnd
new file mode 100644
index 0000000..b71c964
--- /dev/null
+++ b/fs.navigate/bnd.bnd
@@ -0,0 +1,2 @@
+Import-Package: \
+ *
\ No newline at end of file
diff --git a/fs.navigate/play.bndrun b/fs.navigate/play.bndrun
new file mode 100644
index 0000000..c7926d6
--- /dev/null
+++ b/fs.navigate/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.fs.navigate;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.fs.navigate-tests;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/fs.navigate/pom.xml b/fs.navigate/pom.xml
new file mode 100644
index 0000000..9280ab0
--- /dev/null
+++ b/fs.navigate/pom.xml
@@ -0,0 +1,93 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+
+ fs.navigate
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/fs.navigate/src/main/java/org/eclipse/osgi/technology/command/fs/navigate/Activator.java b/fs.navigate/src/main/java/org/eclipse/osgi/technology/command/fs/navigate/Activator.java
new file mode 100644
index 0000000..7a5108c
--- /dev/null
+++ b/fs.navigate/src/main/java/org/eclipse/osgi/technology/command/fs/navigate/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.fs.navigate;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new FileSystemNavigateCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new FrameworkConverter(formatter));
+ }
+
+ private static class FrameworkConverter extends BaseDTOFormatterConverter {
+
+ public FrameworkConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/fs.navigate/src/main/java/org/eclipse/osgi/technology/command/fs/navigate/FileSystemNavigateCommands.java b/fs.navigate/src/main/java/org/eclipse/osgi/technology/command/fs/navigate/FileSystemNavigateCommands.java
new file mode 100644
index 0000000..b216887
--- /dev/null
+++ b/fs.navigate/src/main/java/org/eclipse/osgi/technology/command/fs/navigate/FileSystemNavigateCommands.java
@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.fs.navigate;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Descriptor;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.BundleContext;
+
+public class FileSystemNavigateCommands {
+ private static final String CWD = ".cwd";
+
+ public FileSystemNavigateCommands(BundleContext bc, DTOFormatter formatter) {
+ }
+
+ @Descriptor("get current directory")
+ public File pwd(CommandSession session) {
+ var cwd = (File) session.get(CWD);
+ if (cwd == null) {
+ cwd = session.currentDir().toFile();
+ }
+ return cwd;
+ }
+
+ @Descriptor("get current directory")
+ public File cd(CommandSession session) {
+ try {
+ return cd(session, null);
+ } catch (IOException ex) {
+ throw new RuntimeException("Unable to get current directory");
+ }
+ }
+
+ @Descriptor("change current directory")
+ public File cd(CommandSession session, @Descriptor("target directory") String dir) throws IOException {
+ var cwd = pwd(session);
+ if ((dir == null) || (dir.length() == 0)) {
+ return cwd;
+ }
+ cwd = cwd.getAbsoluteFile().toPath().resolve(dir).toFile();
+
+ session.put(CWD, cwd);
+ return cwd;
+ }
+
+ @Descriptor("get current directory contents")
+ public File[] ls(CommandSession session) throws IOException {
+ return ls(session, null);
+ }
+
+ @Descriptor("get specified path contents")
+ public File[] ls(CommandSession session, @Descriptor("path with optionally wildcarded file name") String pattern)
+ throws IOException {
+ pattern = ((pattern == null) || (pattern.length() == 0)) ? "." : pattern;
+ pattern = ((pattern.charAt(0) != File.separatorChar) && (pattern.charAt(0) != '.')) ? "./" + pattern : pattern;
+ var idx = pattern.lastIndexOf(File.separatorChar);
+ var parent = (idx < 0) ? "." : pattern.substring(0, idx + 1);
+ var target = (idx < 0) ? pattern : pattern.substring(idx + 1);
+
+ var actualParent = ((parent.charAt(0) == File.separatorChar) ? new File(parent) : new File(cd(session), parent))
+ .getCanonicalFile();
+
+ idx = target.indexOf(File.separatorChar, idx);
+ var isWildcarded = (target.indexOf('*', idx) >= 0);
+ File[] files;
+ if (isWildcarded) {
+ if (!actualParent.exists()) {
+ throw new IOException("File does not exist");
+ }
+ final var pieces = parseSubstring(target);
+ files = actualParent.listFiles((FileFilter) pathname -> compareSubstring(pieces, pathname.getName()));
+ } else {
+ var actualTarget = new File(actualParent, target).getCanonicalFile();
+ if (!actualTarget.exists()) {
+ throw new IOException("File does not exist");
+ }
+ if (actualTarget.isDirectory()) {
+ files = actualTarget.listFiles();
+ } else {
+ files = new File[] { actualTarget };
+ }
+ }
+ return files;
+ }
+
+ @Descriptor("get tree under current directory")
+ public String tree(CommandSession session) {
+ var cwd = (File) session.get(CWD);
+ if (cwd == null) {
+ cwd = session.currentDir().toFile();
+ }
+
+ StringBuilder sb = new StringBuilder();
+ printTree(sb, cwd, "");
+ return sb.toString();
+ }
+
+ private static List parseSubstring(String value) {
+ List pieces = new ArrayList<>();
+ var ss = new StringBuilder();
+ // int kind = SIMPLE; // assume until proven otherwise
+ var wasStar = false; // indicates last piece was a star
+ var leftstar = false; // track if the initial piece is a star
+ var rightstar = false; // track if the final piece is a star
+
+ var idx = 0;
+
+ // We assume (sub)strings can contain leading and trailing blanks
+ var escaped = false;
+ loop: for (;;) {
+ if (idx >= value.length()) {
+ if (wasStar) {
+ // insert last piece as "" to handle trailing star
+ rightstar = true;
+ } else {
+ pieces.add(ss.toString());
+ // accumulate the last piece
+ // note that in the case of
+ // (cn=); this might be
+ // the string "" (!=null)
+ }
+ ss.setLength(0);
+ break loop;
+ }
+
+ // Read the next character and account for escapes.
+ var c = value.charAt(idx++);
+ if (!escaped && ((c == '(') || (c == ')'))) {
+ throw new IllegalArgumentException("Illegal value: " + value);
+ } else if (!escaped && (c == '*')) {
+ if (wasStar) {
+ // encountered two successive stars;
+ // I assume this is illegal
+ throw new IllegalArgumentException("Invalid filter string: " + value);
+ }
+ if (ss.length() > 0) {
+ pieces.add(ss.toString()); // accumulate the pieces
+ // between '*' occurrences
+ }
+ ss.setLength(0);
+ // if this is a leading star, then track it
+ if (pieces.isEmpty()) {
+ leftstar = true;
+ }
+ wasStar = true;
+ } else if (!escaped && (c == '\\')) {
+ escaped = true;
+ } else {
+ escaped = false;
+ wasStar = false;
+ ss.append(c);
+ }
+ }
+ if (leftstar || rightstar || pieces.size() > 1) {
+ // insert leading and/or trailing "" to anchor ends
+ if (rightstar) {
+ pieces.add("");
+ }
+ if (leftstar) {
+ pieces.add(0, "");
+ }
+ }
+ return pieces;
+ }
+
+ private static boolean compareSubstring(List pieces, String s) {
+ // Walk the pieces to match the string
+ // There are implicit stars between each piece,
+ // and the first and last pieces might be "" to anchor the match.
+ // assert (pieces.length > 1)
+ // minimal case is *
+
+ var len = pieces.size();
+
+ var index = 0;
+
+ for (var i = 0; i < len; i++) {
+ var piece = pieces.get(i);
+
+ // If this is the first piece, then make sure the
+ // string starts with it.
+ if (i == 0) {
+ if (!s.startsWith(piece)) {
+ return false;
+ }
+ }
+
+ // If this is the last piece, then make sure the
+ // string ends with it.
+ if (i == len - 1) {
+ return s.endsWith(piece);
+ }
+
+ // If this is neither the first or last piece, then
+ // make sure the string contains it.
+ if (i > 0) {
+ index = s.indexOf(piece, index);
+ if (index < 0) {
+ return false;
+ }
+ }
+
+ // Move string index beyond the matching piece.
+ index += piece.length();
+ }
+
+ return true;
+ }
+
+
+ private static void printTree(StringBuilder sb, File folder, String prefix) {
+ File[] files = folder.listFiles();
+ if (files == null || files.length == 0) return;
+
+ Arrays.sort(files);
+
+ for (int i = 0; i < files.length; i++) {
+ File file = files[i];
+ boolean isLast = (i == files.length - 1);
+ sb.append(prefix)
+ .append(isLast ? "└── " : "├── ")
+ .append(file.getName());
+
+ if (file.isDirectory()) {
+ sb.append("/\n");
+ printTree(sb, file, prefix + (isLast ? " " : "│ "));
+ } else {
+ sb.append("\n");
+ }
+ }
+ }
+}
diff --git a/fs.navigate/src/test/java/org/eclipse/osgi/technology/command/fs/navigate/Test.java b/fs.navigate/src/test/java/org/eclipse/osgi/technology/command/fs/navigate/Test.java
new file mode 100644
index 0000000..aeeeb21
--- /dev/null
+++ b/fs.navigate/src/test/java/org/eclipse/osgi/technology/command/fs/navigate/Test.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+
+package org.eclipse.osgi.technology.command.fs.navigate;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/help/.gitignore b/help/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/help/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/help/bnd.bnd b/help/bnd.bnd
new file mode 100644
index 0000000..b71c964
--- /dev/null
+++ b/help/bnd.bnd
@@ -0,0 +1,2 @@
+Import-Package: \
+ *
\ No newline at end of file
diff --git a/help/play.bndrun b/help/play.bndrun
new file mode 100644
index 0000000..f94829f
--- /dev/null
+++ b/help/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.help;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.help-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/help/pom.xml b/help/pom.xml
new file mode 100644
index 0000000..d8b2fb4
--- /dev/null
+++ b/help/pom.xml
@@ -0,0 +1,92 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ help
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/help/src/main/java/org/eclipse/osgi/technology/command/help/Activator.java b/help/src/main/java/org/eclipse/osgi/technology/command/help/Activator.java
new file mode 100644
index 0000000..f889b97
--- /dev/null
+++ b/help/src/main/java/org/eclipse/osgi/technology/command/help/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.help;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new Help(context, formatter), "tech", Map.of());
+ registerConverterService(new HelpConverter(formatter));
+ }
+
+ private static class HelpConverter extends BaseDTOFormatterConverter {
+
+ public HelpConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/help/src/main/java/org/eclipse/osgi/technology/command/help/Help.java b/help/src/main/java/org/eclipse/osgi/technology/command/help/Help.java
new file mode 100644
index 0000000..2650f8d
--- /dev/null
+++ b/help/src/main/java/org/eclipse/osgi/technology/command/help/Help.java
@@ -0,0 +1,393 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.help;
+
+import java.io.InputStreamReader;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.util.GlobFilter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.Justif;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+public class Help {
+ private final BundleContext context;
+
+ public Help(BundleContext bc, DTOFormatter formatter) {
+ context = bc;
+ formatter.build(Scope.class).inspect().field("name").field("description").format("commands", Scope::commands)
+ .line().field("name").format("commands", Scope::commands).part().as(Scope::toString);
+
+ formatter.build(Command.class).inspect().as(Command::inspect).line().as(Command::line).part()
+ .as(Command::toString);
+ }
+
+ @Descriptor("Clear the screen")
+ public void cls(CommandSession session) {
+ session.getConsole().append("\u001B[2J").flush();
+ }
+
+ @Descriptor("Format in box form or turn it off")
+ public boolean box(boolean on) {
+ return DTOFormatter.boxes = on;
+ }
+
+ static public class Scope implements Comparable {
+ public String name;
+ public String description = "";
+ public final Map commands = new TreeMap<>();
+
+ Scope(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public String commands() {
+ var size = commands.size();
+ try (var f = new Formatter()) {
+ var column = 0;
+ for (Command command : commands.values()) {
+ f.format("%-20s ", command.name);
+ if (column++ == 4) {
+ f.format("\n");
+ column = 0;
+ }
+ }
+ return f.toString();
+ }
+ }
+
+ @Override
+ public int compareTo(Scope o) {
+ return name.compareTo(o.name);
+ }
+ }
+
+ static public class Command {
+ String name;
+ List methods = new ArrayList<>();
+ Properties properties;
+
+ Command(String name) {
+ this.name = name;
+ }
+
+ public String inspect() {
+ var j = new Justif(100, tabs(8));
+
+ var f = j.formatter();
+ f.format("COMMAND\n\t1%s", name);
+ var title = getTitle();
+ if (title != null) {
+ f.format("\t3-- %s\n", fixup(title));
+ }
+ f.format("\n\u2007\n");
+ var topDescription = fixup(getDescription());
+ if (topDescription != null) {
+ f.format("DESCRIPTION\n");
+ f.format("\t1%s\n", topDescription);
+ }
+ f.format("\u2007\nSYNOPSIS %s\n", name);
+ for (Method m : methods) {
+ f.format("\t1%s", name);
+ var description = Help.getDescription(m);
+ if (description != null) {
+ f.format("\t6%s", description);
+ }
+ f.format("\n");
+
+ var parameters = m.getParameters();
+ var mdescription = m.getAnnotation(Descriptor.class);
+ for (java.lang.reflect.Parameter p : parameters) {
+ if (p.getType() == CommandSession.class) {
+ continue;
+ }
+ var ann = p.getAnnotation(Parameter.class);
+ var d = p.getAnnotation(Descriptor.class);
+ if (ann != null) {
+ f.format("\t1 [");
+ for (String name : ann.names()) {
+ f.format("%s", name);
+ break;
+ }
+ if (Parameter.UNSPECIFIED.equals(ann.presentValue())) {
+ // not a flag
+ f.format(" %s:%s", p.getName(), type(p.getParameterizedType()));
+ } else {
+ f.format(" %s", p.getName());
+ }
+ f.format("]");
+ f.format("\t4%s", ann.absentValue());
+ } else {
+ if (p.isVarArgs()) {
+ f.format("\t1 %s...:%s", p.getName(), type(p.getType().getComponentType()));
+ } else {
+ f.format("\t1 %s:%s", p.getName(), type(p.getParameterizedType()));
+ }
+ }
+ if (d != null) {
+ f.format("\t6- %s", d.value());
+ }
+ f.format("\n");
+ }
+ f.format("\n\n");
+ }
+
+ var example = getExample();
+ if (example != null) {
+ f.format("EXAMPLE\n\t1%s\n", example);
+ }
+ var see = getSee();
+ if (see != null) {
+ f.format("SEE\n\t1%s\n", see);
+ }
+ f.format("\n");
+ return j.toString();
+ }
+
+ private String getExample() {
+ return getProperties().getProperty(name + ".example");
+ }
+
+ private String getSee() {
+ return getProperties().getProperty(name + ".see");
+ }
+
+ private String getDescription() {
+ return getProperties().getProperty(name + ".description");
+ }
+
+ private String getTitle() {
+ return getProperties().getProperty(name + ".title", Help.getDescription(methods.get(0)));
+ }
+
+ private Properties getProperties() {
+ if (properties == null) {
+ List> collect = methods.stream().map(Method::getDeclaringClass).filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ properties = getResource("help.properties", collect);
+ }
+ return properties;
+ }
+
+ private String type(Type type) {
+ if (type instanceof Class) {
+
+ Class> clazz = (Class>) type;
+
+ if (clazz.isPrimitive()) {
+ return clazz.getName();
+ }
+
+ if (clazz.isArray()) {
+ return type(clazz.getComponentType()) + "[]";
+ }
+
+ return clazz.getSimpleName().toLowerCase();
+ } else if (type instanceof ParameterizedType) {
+
+ }
+ if (type instanceof ParameterizedType ptype) {
+ var raw = type(ptype.getRawType());
+ var sb = new StringBuilder();
+ sb.append(raw).append("<");
+ for (Type t : ptype.getActualTypeArguments()) {
+ sb.append(type(t));
+ }
+ sb.append(">");
+ return sb.toString();
+ }
+ return type.toString().toLowerCase();
+ }
+
+ public String line() {
+ return inspect();
+ }
+ }
+
+ static public class Argument {
+ public String name;
+ public String description;
+ public Type type;
+ }
+
+ @Descriptor("Displays help for each available scopes")
+ public Collection help() throws Exception {
+ return getCommands().values();
+ }
+
+ @Descriptor("Displays help for a command")
+ public List help(
+ // @formatter:off
+ @Parameter(names= {"-s","--scope"}, absentValue="*")
+ @Descriptor("Limit to matching scopes")
+ GlobFilter scope,
+ @Descriptor("Glob for the command name")
+ GlobFilter command
+ // @formatter:on
+ ) throws Exception {
+ return getCommands().entrySet().stream().filter(e -> scope.matches(e.getKey()))
+ .flatMap(e -> e.getValue().commands.entrySet().stream()).filter(e -> command.matches(e.getKey()))
+ .map(Entry::getValue).collect(Collectors.toList());
+ }
+
+ private Map getCommands() throws Exception {
+ Map scopes = new TreeMap<>();
+
+ ServiceReference>[] refs = null;
+ try {
+ refs = context.getAllServiceReferences(null, "(osgi.command.scope=*)");
+ } catch (InvalidSyntaxException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ for (ServiceReference> ref : refs) {
+ Object svc = context.getService(ref);
+ if (svc != null) {
+ try {
+ var description = getDescription(svc.getClass());
+ var name = (String) ref.getProperty("osgi.command.scope");
+ var scope = scopes.computeIfAbsent(name, Scope::new);
+ scope.description = concat(scope.description, description);
+
+ var ofunc = ref.getProperty("osgi.command.function");
+ String[] funcs = null;
+ if (ofunc instanceof String s) {
+ funcs = new String[] { s };
+ } else if (ofunc instanceof String[] sarr) {
+ funcs = sarr;
+ } else if (ofunc instanceof Collection> c) {
+ funcs = c.toArray(new String[0]);
+ }
+
+ for (String func : funcs) {
+ scope.commands.computeIfAbsent(func, Command::new);
+ }
+
+ var methods = svc.getClass().getMethods();
+ for (Method method : methods) {
+ for (String func : funcs) {
+ if (matches(func, method)) {
+ var command = scope.commands.get(func);
+ command.methods.add(method);
+ }
+ }
+ }
+
+ } finally {
+ context.ungetService(ref);
+ }
+ }
+ }
+ return scopes;
+ }
+
+ private boolean matches(String func, Method method) {
+ func = func.toLowerCase();
+ var name = method.getName().toLowerCase();
+ if (func.equals(name)) {
+ return true;
+ }
+ if (name.startsWith("_")) {
+ name = name.substring(1);
+ }
+
+ if (name.startsWith("get") || name.startsWith("set")) {
+ name = name.substring(3);
+ } else if (name.startsWith("is")) {
+ name = name.substring(2);
+ }
+
+ return func.equals(name);
+ }
+
+ private String concat(String description, String description2) {
+ if (description == null) {
+ return description2;
+ }
+ if (description2 == null) {
+ return description;
+ }
+
+ return description.concat("\n").concat(description2);
+ }
+
+ private static String getDescription(AnnotatedElement class1) {
+ var descriptor = class1.getAnnotation(Descriptor.class);
+ if (descriptor == null) {
+ return null;
+ }
+
+ return descriptor.value();
+ }
+
+ private static int[] tabs(int width) {
+ var tabs = new int[100];
+ for (var i = 0; i < 100; i++) {
+ tabs[i] = i * width;
+ }
+ return tabs;
+ }
+
+ private static Properties getResource(String path, Class> clazz) {
+ try (var in = clazz.getResourceAsStream(path)) {
+ if (in == null) {
+ return null;
+ }
+
+ try (var rd = new InputStreamReader(in)) {
+ var p = new Properties();
+ p.load(rd);
+ return p;
+ }
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static Properties getResource(String name, Collection> clazz) {
+ return clazz.stream().map(c -> getResource(name, c)).filter(Objects::nonNull).findFirst()
+ .orElse(new Properties());
+ }
+
+ private static String fixup(String s) {
+ if (s == null) {
+ return null;
+ }
+ // the boxing causes empty lines to be stripped
+ return s.replace("\n\n", "\n\u2007\n").replace('\n', '\f');
+ }
+}
diff --git a/help/src/test/java/org/eclipse/osgi/technology/command/help/Test.java b/help/src/test/java/org/eclipse/osgi/technology/command/help/Test.java
new file mode 100644
index 0000000..1663758
--- /dev/null
+++ b/help/src/test/java/org/eclipse/osgi/technology/command/help/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.help;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/mxbeans/.gitignore b/mxbeans/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/mxbeans/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/mxbeans/play.bndrun b/mxbeans/play.bndrun
new file mode 100644
index 0000000..583ec8c
--- /dev/null
+++ b/mxbeans/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.mxbeans;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.mxbeans-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/mxbeans/pom.xml b/mxbeans/pom.xml
new file mode 100644
index 0000000..e528549
--- /dev/null
+++ b/mxbeans/pom.xml
@@ -0,0 +1,93 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ mxbeans
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/mxbeans/src/main/java/org/eclipse/osgi/technology/command/mxbean/Activator.java b/mxbeans/src/main/java/org/eclipse/osgi/technology/command/mxbean/Activator.java
new file mode 100644
index 0000000..ff60ab9
--- /dev/null
+++ b/mxbeans/src/main/java/org/eclipse/osgi/technology/command/mxbean/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.mxbean;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new RuntimeInfoCommands(formatter), "tech", Map.of());
+ registerConverterService(new FrameworkConverter(formatter));
+ }
+
+ private static class FrameworkConverter extends BaseDTOFormatterConverter {
+
+ public FrameworkConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/mxbeans/src/main/java/org/eclipse/osgi/technology/command/mxbean/RuntimeInfoCommands.java b/mxbeans/src/main/java/org/eclipse/osgi/technology/command/mxbean/RuntimeInfoCommands.java
new file mode 100644
index 0000000..8ec718f
--- /dev/null
+++ b/mxbeans/src/main/java/org/eclipse/osgi/technology/command/mxbean/RuntimeInfoCommands.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+
+package org.eclipse.osgi.technology.command.mxbean;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.RuntimeMXBean;
+
+import org.apache.felix.service.command.Descriptor;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+
+public class RuntimeInfoCommands {
+ private final RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
+
+ public RuntimeInfoCommands(DTOFormatter formatter) {
+ }
+
+ @Descriptor("Returns the name representing the running Java virtual machine")
+ public String name() {
+ return runtime.getName();
+ }
+
+ @Descriptor("Returns the Java specification name")
+ public String specName() {
+ return runtime.getSpecName();
+ }
+
+ @Descriptor("Returns the Java specification vendor")
+ public String specVendor() {
+ return runtime.getSpecVendor();
+ }
+
+ @Descriptor("Returns the Java specification version")
+ public String specVersion() {
+ return runtime.getSpecVersion();
+ }
+
+ @Descriptor("Returns the uptime of the Java virtual machine in milliseconds")
+ public String uptime() {
+ return runtime.getUptime() + "";
+ }
+
+ @Descriptor("Returns the start time of the Java virtual machine in milliseconds since epoch")
+ public String startTime() {
+ return runtime.getStartTime() + "";
+ }
+
+ @Descriptor("Returns the Java vendor")
+ public String vmVendor() {
+ return runtime.getVmVendor();
+ }
+
+ @Descriptor("Returns the Java VM name")
+ public String vmName() {
+ return runtime.getVmName();
+ }
+
+ @Descriptor("Returns the Java VM version")
+ public String vmVersion() {
+ return runtime.getVmVersion();
+ }
+
+ @Descriptor("Returns the process ID (pid) from the VM name")
+ public String pid() {
+ String name = runtime.getName(); // Format: pid@hostname
+ return name.contains("@") ? name.split("@")[0] : "unknown";
+ }
+}
\ No newline at end of file
diff --git a/mxbeans/src/test/java/org/eclipse/osgi/technology/command/mxbean/Test.java b/mxbeans/src/test/java/org/eclipse/osgi/technology/command/mxbean/Test.java
new file mode 100644
index 0000000..e99387c
--- /dev/null
+++ b/mxbeans/src/test/java/org/eclipse/osgi/technology/command/mxbean/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.mxbean;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/osgi.framework.modify/.gitignore b/osgi.framework.modify/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/osgi.framework.modify/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/osgi.framework.modify/bnd.bnd b/osgi.framework.modify/bnd.bnd
new file mode 100644
index 0000000..b71c964
--- /dev/null
+++ b/osgi.framework.modify/bnd.bnd
@@ -0,0 +1,2 @@
+Import-Package: \
+ *
\ No newline at end of file
diff --git a/osgi.framework.modify/play.bndrun b/osgi.framework.modify/play.bndrun
new file mode 100644
index 0000000..4b5007a
--- /dev/null
+++ b/osgi.framework.modify/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.osgi.framework.modify;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.framework.modify-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/osgi.framework.modify/pom.xml b/osgi.framework.modify/pom.xml
new file mode 100644
index 0000000..aee5898
--- /dev/null
+++ b/osgi.framework.modify/pom.xml
@@ -0,0 +1,98 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ osgi.framework.modify
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+ org.eclipse.osgi-technology.command
+ osgi.framework
+ 0.0.1-SNAPSHOT
+ compile
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/osgi.framework.modify/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/Activator.java b/osgi.framework.modify/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/Activator.java
new file mode 100644
index 0000000..c8b90ff
--- /dev/null
+++ b/osgi.framework.modify/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.framework;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new FrameworkModifyCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new FrameworkConverter(formatter));
+ }
+
+ private static class FrameworkConverter extends BaseDTOFormatterConverter {
+
+ public FrameworkConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/osgi.framework.modify/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/FrameworkModifyCommands.java b/osgi.framework.modify/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/FrameworkModifyCommands.java
new file mode 100644
index 0000000..f7007ae
--- /dev/null
+++ b/osgi.framework.modify/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/FrameworkModifyCommands.java
@@ -0,0 +1,325 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.framework;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.osgi.framework.FrameworkCommands;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.wiring.FrameworkWiring;
+
+public class FrameworkModifyCommands {
+
+ private static final String CWD = ".cwd";
+
+ private BundleContext context;
+
+ public FrameworkModifyCommands(BundleContext context, DTOFormatter formatter) {
+ this.context = context;
+ dtos(formatter);
+ }
+
+ void dtos(DTOFormatter f) {
+
+ }
+
+ @Descriptor("Set the start level of bundles")
+ public void startlevel(@Descriptor("startlevel, >0") int startlevel,
+ @Descriptor("bundles to set. No bundles imply all bundles except the framework bundle") Bundle bundle) {
+
+ if (bundle.getBundleId() == 0L) {
+ return;
+ }
+
+ var s = bundle.adapt(BundleStartLevel.class);
+ s.setStartLevel(startlevel);
+ }
+
+ enum Modifier {
+ framework, initial
+ }
+
+ //@formatter:off
+ @Descriptor("set either the framework or the initial bundle start level")
+ public int startlevel(
+
+ @Parameter(names = {"-w", "--wait"}, absentValue = "false", presentValue = "true")
+ boolean wait,
+
+ Modifier modifier,
+
+ @Descriptor("either framework or initial level. If <0 then not set, currently value returned")
+ int level
+ ) throws InterruptedException { //@formatter:on
+
+ var fsl = FrameworkCommands.startlevel(context);
+ switch (modifier) {
+ case framework: {
+ var oldlevel = fsl.getStartLevel();
+ if (level >= 0) {
+ if (wait) {
+ var s = new Semaphore(0);
+ fsl.setStartLevel(level, e -> {
+ s.release();
+ });
+ s.acquire();
+ } else {
+ fsl.setStartLevel(level);
+ }
+ }
+ return oldlevel;
+ }
+
+ case initial: {
+ var oldlevel = fsl.getInitialBundleStartLevel();
+ fsl.setInitialBundleStartLevel(level);
+ return oldlevel;
+ }
+ default:
+ throw new IllegalArgumentException("invalid modifier " + modifier);
+ }
+ }
+
+ @Descriptor("refresh bundles")
+ //@formatter:off
+ public List refresh(
+
+ @Descriptor("Wait for refresh to finish before returning. The maxium time this will wait is 60 seconds. It will return the affected bundles")
+ @Parameter(absentValue="false", presentValue="true", names= {"-w","--wait"})
+ boolean wait,
+
+ @Descriptor("target bundles (can be empty). If no bundles are specified then all bundles are refreshed")
+ Bundle ... bundles
+
+ // @formatter:on
+ ) {
+ List bs = Arrays.asList(bundles);
+
+ var fw = context.getBundle(0L).adapt(FrameworkWiring.class);
+ if (wait) {
+ try {
+ Bundle older[] = context.getBundles();
+ var s = new Semaphore(0);
+ fw.refreshBundles(bs, e -> {
+ if (e.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
+ s.release();
+ }
+ });
+ s.tryAcquire(60000, TimeUnit.MILLISECONDS);
+ Bundle newer[] = context.getBundles();
+
+ Arrays.sort(older, Comparator.comparing(Bundle::getBundleId));
+ Arrays.sort(newer, Comparator.comparing(Bundle::getBundleId));
+ return diff(older, newer);
+ } catch (InterruptedException e1) {
+ // ignore, just return
+ return null;
+ }
+ } else {
+ fw.refreshBundles(bs);
+ return null;
+ }
+ }
+
+ private List diff(Bundle[] older, Bundle[] newer) {
+ List diffs = new ArrayList<>();
+ int o = 0, n = 0;
+ while (o < older.length || n < older.length) {
+
+ if (o < older.length && n < older.length) {
+ if (older[o].getBundleId() == newer[n].getBundleId()) {
+ if (older[o].getLastModified() != newer[n].getLastModified()) {
+ diffs.add(older[o]);
+ }
+ o++;
+ n++;
+ } else {
+ if (older[o].getBundleId() < newer[n].getBundleId()) {
+ diffs.add(older[o]);
+ o++;
+ } else {
+ diffs.add(newer[n]);
+ n++;
+ }
+ }
+ } else if (o < older.length) {
+ diffs.add(older[o]);
+ o++;
+ } else {
+ diffs.add(newer[n]);
+ n++;
+ }
+ }
+ return diffs;
+ }
+
+ @Descriptor("resolve bundles")
+ public List resolve(
+ @Descriptor("to be resolved bundles. If no bundles are specified then all bundles are attempted to be resolved") Bundle... bundles) {
+ List bs = Arrays.asList(bundles);
+
+ var fw = context.getBundle(0L).adapt(FrameworkWiring.class);
+ fw.resolveBundles(bs);
+ return FrameworkCommands.lb(context, false, null, false).stream()
+ .filter(b -> (b.getState() & Bundle.UNINSTALLED + Bundle.INSTALLED) != 0).collect(Collectors.toList());
+ }
+
+ @Descriptor("start bundles")
+ public void start(
+ //@formatter:off
+
+ @Descriptor("start bundle transiently")
+ @Parameter(names = {"-t", "--transient"}, presentValue = "true", absentValue = "false")
+ boolean trans,
+
+ @Descriptor("use declared activation policy")
+ @Parameter(names = {"-p", "--policy"}, presentValue = "true", absentValue = "false")
+ boolean policy,
+
+ @Descriptor("target bundle")
+ Bundle ...bundles
+
+ //@formatter:on
+ ) throws BundleException {
+ var options = 0;
+
+ // Check for "transient" switch.
+ if (trans) {
+ options |= Bundle.START_TRANSIENT;
+ }
+
+ // Check for "start policy" switch.
+ if (policy) {
+ options |= Bundle.START_ACTIVATION_POLICY;
+ }
+
+ for (Bundle bundle : bundles) {
+ bundle.start(options);
+ }
+ }
+
+ @Descriptor("stop bundles")
+ public void stop(
+ // @formatter:off
+ @Parameter(names = {"-t", "--transient"}, presentValue = "true", absentValue = "false")
+ @Descriptor( "stop bundle transiently")
+ boolean trans,
+
+ @Descriptor("target bundles")
+ Bundle ...bundles
+ // @formatter:on
+ ) throws BundleException {
+ var options = 0;
+
+ if (trans) {
+ options |= Bundle.STOP_TRANSIENT;
+ }
+
+ for (Bundle bundle : bundles) {
+ bundle.stop(options);
+ }
+ }
+
+ @Descriptor("uninstall bundles")
+ public void uninstall(
+ //@formatter:off
+
+ @Descriptor("the bundles to uninstall")
+ Bundle ... bundles
+
+ // @formatter:on
+ ) throws BundleException {
+ for (Bundle bundle : bundles) {
+ bundle.uninstall();
+ }
+ }
+
+ @Descriptor("update bundle")
+ public void update(
+ //@formatter:off
+
+ @Descriptor("the bundles to update")
+ Bundle ... bundles
+
+ // @formatter:on
+ ) throws BundleException {
+ for (Bundle b : bundles) {
+ b.update();
+ }
+ }
+
+ @Descriptor("update bundle from URL")
+ public void update(
+ // @formatter:off
+ CommandSession session,
+
+ @Descriptor("bundle to update")
+ Bundle bundle,
+
+ @Descriptor("URL from where to retrieve bundle")
+ String location
+
+ //@formatter:on
+ ) throws IOException, BundleException, URISyntaxException {
+
+ Objects.requireNonNull(bundle);
+ Objects.requireNonNull(location);
+
+ location = resolveUri(session, location.trim());
+ var is = new URI(location).toURL().openStream();
+ bundle.update(is);
+ }
+
+ /**
+ * Intepret a string as a URI relative to the current working directory.
+ *
+ * @param session the session (where the CWD is stored)
+ * @param relativeUri the input URI
+ * @return the resulting URI as a string
+ * @throws IOException
+ */
+ public static String resolveUri(CommandSession session, String relativeUri) throws IOException {
+ var cwd = (File) session.get(CWD);
+ if (cwd == null) {
+ cwd = new File("").getCanonicalFile();
+ session.put(CWD, cwd);
+ }
+ if ((relativeUri == null) || (relativeUri.length() == 0)) {
+ return relativeUri;
+ }
+
+ var curUri = cwd.toURI();
+ var newUri = curUri.resolve(relativeUri);
+ return newUri.toString();
+ }
+}
diff --git a/osgi.framework.modify/src/test/java/org/eclipse/osgi/technology/command/osgi/framework/Test.java b/osgi.framework.modify/src/test/java/org/eclipse/osgi/technology/command/osgi/framework/Test.java
new file mode 100644
index 0000000..05fb73a
--- /dev/null
+++ b/osgi.framework.modify/src/test/java/org/eclipse/osgi/technology/command/osgi/framework/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.framework;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/osgi.framework/.gitignore b/osgi.framework/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/osgi.framework/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/osgi.framework/bnd.bnd b/osgi.framework/bnd.bnd
new file mode 100644
index 0000000..b71c964
--- /dev/null
+++ b/osgi.framework/bnd.bnd
@@ -0,0 +1,2 @@
+Import-Package: \
+ *
\ No newline at end of file
diff --git a/osgi.framework/play.bndrun b/osgi.framework/play.bndrun
new file mode 100644
index 0000000..6436550
--- /dev/null
+++ b/osgi.framework/play.bndrun
@@ -0,0 +1,24 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ assertj-core;version='[3.26.0,3.26.1)',\
+ junit-jupiter-params;version='[5.11.1,5.11.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ net.bytebuddy.byte-buddy;version='[1.15.3,1.15.4)',\
+ org.eclipse.osgi-technology.command.osgi.framework;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.framework-tests;version='[0.0.1,0.0.2)',\
+ org.osgi.test.common;version='[1.3.0,1.3.1)',\
+ org.osgi.test.junit5;version='[1.3.0,1.3.1)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/osgi.framework/pom.xml b/osgi.framework/pom.xml
new file mode 100644
index 0000000..01e5204
--- /dev/null
+++ b/osgi.framework/pom.xml
@@ -0,0 +1,97 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ osgi.framework
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+ org.eclipse.osgi-technology.command
+ converter.bundle
+ 0.0.1-SNAPSHOT
+ test
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/osgi.framework/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/Activator.java b/osgi.framework/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/Activator.java
new file mode 100644
index 0000000..0acae92
--- /dev/null
+++ b/osgi.framework/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/Activator.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.framework;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@org.osgi.annotation.bundle.Requirement(
+ effective = "active",
+ namespace = "osgi.commands",
+ name = "converter.bundle",
+ version = "1.0.0"
+ )
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new FrameworkCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new FrameworkConverter(formatter));
+ }
+
+ private static class FrameworkConverter extends BaseDTOFormatterConverter {
+
+ public FrameworkConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/osgi.framework/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/FrameworkCommands.java b/osgi.framework/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/FrameworkCommands.java
new file mode 100644
index 0000000..8c5757e
--- /dev/null
+++ b/osgi.framework/src/main/java/org/eclipse/osgi/technology/command/osgi/framework/FrameworkCommands.java
@@ -0,0 +1,332 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.framework;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.util.DisplayUtil;
+import org.eclipse.osgi.technology.command.util.GlobFilter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.dto.BundleDTO;
+import org.osgi.framework.dto.FrameworkDTO;
+import org.osgi.framework.dto.ServiceReferenceDTO;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.startlevel.FrameworkStartLevel;
+
+public class FrameworkCommands {
+
+ private BundleContext context;
+
+ public FrameworkCommands(BundleContext context, DTOFormatter formatter) {
+ this.context = context;
+ dtos(formatter);
+ }
+
+ void dtos(DTOFormatter f) {
+ f.build(FrameworkDTO.class).inspect().fields("*").line().fields("*").part();
+ f.build(BundleDTO.class).inspect().fields("*").line().fields("*").part();
+ f.build(ServiceReferenceDTO.class).inspect().fields("*").line().fields("*").part();
+
+ f.build(Bundle.class).inspect().method("bundleId").format("STATE", FrameworkCommands::state)
+ .method("symbolicName").method("version").method("location")
+ .format("LAST MODIFIED", b -> DisplayUtil.lastModified(b.getLastModified()))
+ .format("servicesInUse", b -> b.getServicesInUse()).method("registeredServices")
+ .format("HEADERS", Bundle::getHeaders).part()
+ .as(b -> "[" + b.getBundleId() + "] " + b.getSymbolicName()).line().method("bundleId")
+ .format("STATE", FrameworkCommands::state).method("symbolicName").method("version")
+ .format("START LEVEL", this::startlevel)
+ .format("LAST MODIFIED", b -> DisplayUtil.lastModified(b.getLastModified()));
+
+ f.build(BundleDTO.class).inspect().fields("*").line().field("id").field("symbolicName").field("version")
+ .field("state").part().as(b -> String.format("[%s]%s", b.id, b.symbolicName));
+
+ f.build(ServiceReference.class).inspect().format("id", s -> getServiceId(s) + "")
+ .format("objectClass", FrameworkCommands::objectClass)
+ .format("bundle", s -> s.getBundle().getBundleId() + "")
+ .format("usingBundles", s -> bundles(s.getUsingBundles())).format("properties", DisplayUtil::toMap)
+ .line().format("id", s -> getServiceId(s) + "").format("bundle", s -> s.getBundle().getBundleId() + "")
+ .format("service", FrameworkCommands::objectClass)
+ .format("ranking", s -> s.getProperty(Constants.SERVICE_RANKING))
+ .format("component", s -> s.getProperty("component.id"))
+ .format("usingBundles", s -> bundles(s.getUsingBundles())).part()
+ .as(s -> String.format("(%s) %s", getServiceId(s), objectClass(s)));
+
+ f.build(BundleStartLevel.class).inspect().format("level", s -> s.getStartLevel() + "")
+ .format("persistent", s -> s.isPersistentlyStarted())
+ .format("act. policy", s -> s.isActivationPolicyUsed()).line()
+ .format("level", s -> s.getStartLevel() + "").format("persistent", s -> s.isPersistentlyStarted())
+ .format("act. policy", s -> s.isActivationPolicyUsed()).part()
+ .as(s -> String.format("%s %s", s.getStartLevel(),
+ (s.isPersistentlyStarted() ? "" : "T") + (s.isActivationPolicyUsed() ? "A" : "")));
+
+ }
+
+ private static String bundles(Bundle[] usingBundles) {
+ if (usingBundles == null) {
+ return null;
+ }
+
+ return Stream.of(usingBundles).map(b -> b.getBundleId() + "").collect(Collectors.joining("\n"));
+ }
+
+ private static long getServiceId(ServiceReference> s) {
+ return (Long) s.getProperty(Constants.SERVICE_ID);
+ }
+
+ static String objectClass(ServiceReference> ref) {
+ return DisplayUtil.objectClass(DisplayUtil.toMap(ref));
+ }
+
+ private static String state(Bundle b) {
+
+ switch (b.getState()) {
+ case Bundle.ACTIVE:
+ return "ACTV";
+ case Bundle.INSTALLED:
+ return "INST";
+ case Bundle.RESOLVED:
+ return "RSLV";
+ case Bundle.STARTING:
+ return "⬆︎︎";
+ case Bundle.STOPPING:
+ return "⬇︎︎";
+ case Bundle.UNINSTALLED:
+ return "UNIN";
+ }
+ return null;
+ }
+
+ @Descriptor(value = "delivers the FrameworkDTO")
+ public FrameworkDTO frameworkDTO() {
+ final var bundle = context.getBundle(0);
+ final var frameworkDTO = bundle.adapt(FrameworkDTO.class);
+ return frameworkDTO;
+ }
+
+ @Descriptor(value = "delivers the BundleDTOs")
+ public List bundleDTO() {
+ return frameworkDTO().bundles;
+ }
+
+ @Descriptor(value = "delivers the BundleDTO by id")
+ public BundleDTO bundleDTO(long id) {
+ return bundleDTO().stream().filter(dto -> dto.id == id).findAny()
+ .orElseThrow(() -> new IllegalArgumentException(""));
+ }
+
+ @Descriptor(value = "delivers the ServiceReferenceDTOs")
+ public List serviceReferenceDTO() {
+ return frameworkDTO().services;
+ }
+
+ @Descriptor(value = "delivers the ServiceReferenceDTO by id")
+ public ServiceReferenceDTO serviceReferenceDTO(long id) {
+ return serviceReferenceDTO().stream().filter(dto -> dto.id == id).findAny()
+ .orElseThrow(() -> new IllegalArgumentException(""));
+ }
+
+ @Descriptor("Show the Bundle Symbolic Name")
+ public String bsn(Bundle b) {
+ return b.getSymbolicName();
+ }
+
+ /**
+ * Services
+ */
+ @Descriptor("shows the services")
+ public List> srv(
+ @Descriptor("Registering bundle") @Parameter(absentValue = "0", names = { "-b", "--bundle" }) Bundle owner)
+ throws InvalidSyntaxException {
+ return srv(owner, GlobFilter.ALL);
+ }
+
+ @Descriptor("shows the services and filter")
+ public List> srv(
+ @Descriptor("Registering bundle") @Parameter(absentValue = "0", names = { "-b", "--bundle" }) Bundle owner,
+ @Descriptor("Filter") GlobFilter glob) throws InvalidSyntaxException {
+
+ var filter = "(objectClass=*" + glob + "*)";
+ var refs = context.getAllServiceReferences((String) null, filter);
+
+ var bundleId = owner.getBundleId();
+ if (refs == null) {
+ return Collections.emptyList();
+ }
+
+ return Stream.of(refs).filter(ref -> {
+ return bundleId == 0L || bundleId == ref.getBundle().getBundleId();
+ }).collect(Collectors.toList());
+ }
+
+ @Descriptor("shows the service")
+ public ServiceReference> srv(int id) throws InvalidSyntaxException {
+ var allServiceReferences = context.getAllServiceReferences((String) null, "(service.id=" + id + ")");
+ if (allServiceReferences == null) {
+ return null;
+ }
+ assert allServiceReferences.length == 1;
+ return allServiceReferences[0];
+ }
+
+ /**
+ * Startlevel
+ */
+ @Descriptor("query the bundle start level")
+ public BundleStartLevel startlevel(@Descriptor("bundle to query") Bundle bundle) {
+ return _startlevel(bundle);
+ }
+
+ public static BundleStartLevel _startlevel(@Descriptor("bundle to query") Bundle bundle) {
+ var startlevel = bundle.adapt(BundleStartLevel.class);
+ if (startlevel == null) {
+ return null;
+ }
+ return startlevel;
+ }
+
+ enum Sort {
+ id, bsn, level, time
+ }
+
+ public static List lb(BundleContext bc, boolean notactive, Sort sort, boolean descending,
+ GlobFilter... matches) {
+
+ Comparator cmp;
+ if (sort == null) {
+ sort = Sort.id;
+ }
+
+ cmp = switch (sort) {
+ case id -> (a, b) -> Long.compare(a.getBundleId(), b.getBundleId());
+ default -> (a, b) -> Long.compare(a.getBundleId(), b.getBundleId());
+ case bsn -> (a, b) -> a.getSymbolicName().compareTo(b.getSymbolicName());
+ case level -> (a, b) -> Integer.compare(_startlevel(a).getStartLevel(), _startlevel(b).getStartLevel());
+ case time -> (a, b) -> Long.compare(a.getLastModified(), b.getLastModified());
+ };
+ if (descending) {
+ var old = cmp;
+ cmp = (a, b) -> old.compare(b, a);
+ }
+
+ return Arrays.asList(bc.getBundles()).stream().filter(k -> !notactive || in(k.getState(), ~Bundle.ACTIVE))
+ .sorted(cmp).filter(k -> any(matches, k.getSymbolicName())).collect(Collectors.toList());
+
+ }
+
+ @Descriptor("List all current bundles")
+ public List lb(
+ @Descriptor("show only the not active bundles") @Parameter(absentValue = "false", presentValue = "true", names = {
+ "-n", "--notactive" }) boolean notactive,
+ @Descriptor("sort by: id | bsn | time | level. Default is an ascending sort") @Parameter(absentValue = "id", names = {
+ "-s", "--sort" }) Sort sort,
+ @Descriptor("sort in descending order (the default is ascending)") @Parameter(absentValue = "false", presentValue = "true", names = {
+ "-d", "--descending" }) boolean descending,
+ GlobFilter... matches) {
+ return lb(context, notactive, sort, descending, matches);
+ }
+
+ private static boolean in(int state, int... s) {
+ for (int x : s) {
+ if ((x & state) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean any(GlobFilter[] matches, String symbolicName) {
+ if (matches == null || matches.length == 0) {
+ return true;
+ }
+
+ for (GlobFilter g : matches) {
+ if (g.matches(symbolicName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Headers
+ */
+ @Descriptor("display bundle headers")
+ public Map> headers(
+ @Descriptor("header name, can be globbed") @Parameter(absentValue = "*", names = { "-h",
+ "--header" }) String header,
+ @Descriptor("filter on value, can use globbing") @Parameter(absentValue = "*", names = { "-v",
+ "--value" }) String filter,
+ @Descriptor("target bundles, if none specified all bundles are used") Bundle... bundles) {
+ bundles = ((bundles == null) || (bundles.length == 0)) ? context.getBundles() : bundles;
+
+ var hp = new GlobFilter(header);
+ var vp = new GlobFilter(filter);
+
+ Map> result = new HashMap<>();
+
+ for (Bundle bundle : bundles) {
+
+ Map headers = new TreeMap<>();
+
+ var dict = bundle.getHeaders();
+ var keys = dict.keys();
+ while (keys.hasMoreElements()) {
+ var k = keys.nextElement();
+ var v = dict.get(k);
+ if (hp.createMatcher(k).find() && vp.createMatcher(v).find()) {
+ headers.put(k, v);
+ }
+ }
+ if (headers.size() > 0) {
+ result.put(bundle, headers);
+ }
+ }
+
+ return result;
+ }
+
+ @Descriptor("determines the class loader for a class name and a bundle")
+ public ClassLoader which(@Descriptor("the bundle to load the class from") Bundle bundle,
+ @Descriptor("the name of the class to load from bundle") String className) throws ClassNotFoundException {
+ Objects.requireNonNull(bundle);
+ Objects.requireNonNull(className);
+
+ return bundle.loadClass(className).getClassLoader();
+ }
+
+ @Descriptor("query the framework start level")
+ public FrameworkStartLevel startlevel() {
+ return startlevel(context);
+ }
+
+ public static FrameworkStartLevel startlevel(BundleContext bc) {
+ return bc.getBundle(0L).adapt(FrameworkStartLevel.class);
+ }
+}
diff --git a/osgi.framework/src/test/java/org/eclipse/osgi/technology/command/osgi/framework/integration/Test.java b/osgi.framework/src/test/java/org/eclipse/osgi/technology/command/osgi/framework/integration/Test.java
new file mode 100644
index 0000000..422821a
--- /dev/null
+++ b/osgi.framework/src/test/java/org/eclipse/osgi/technology/command/osgi/framework/integration/Test.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.framework.integration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.BeforeEach;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.dto.FrameworkDTO;
+import org.osgi.framework.dto.ServiceReferenceDTO;
+import org.osgi.test.common.annotation.InjectBundleContext;
+import org.osgi.test.common.annotation.InjectService;
+
+public class Test {
+
+ @InjectBundleContext
+ BundleContext context;
+
+ @InjectService(cardinality = 1, timeout = 200)
+ CommandProcessor commandProcessor;
+
+ ByteArrayInputStream in;
+ ByteArrayOutputStream out;
+ ByteArrayOutputStream err;
+ CommandSession session;
+
+ @BeforeEach
+ void beforeEach() {
+
+ in = new ByteArrayInputStream("".getBytes());
+ out = new ByteArrayOutputStream();
+ err = new ByteArrayOutputStream();
+
+ session = commandProcessor.createSession(in, out, err);
+ }
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+ Object o = session.execute("lb");
+
+ assertThat(o).isInstanceOf(List.class).asInstanceOf(InstanceOfAssertFactories.LIST).hasSizeGreaterThan(10);
+ }
+
+ @org.junit.jupiter.api.Test
+ void testFrameworkDTOCommand() throws Exception {
+ Object result = session.execute("frameworkDTO");
+ assertThat(result).isInstanceOf(FrameworkDTO.class);
+
+ FrameworkDTO frameworkDTO = (FrameworkDTO) result;
+ assertThat(frameworkDTO.bundles).isNotNull();
+ }
+
+ @org.junit.jupiter.api.Test
+ void testBundleDTOCommand() throws Exception {
+ Object result = session.execute("bundleDTO");
+ assertThat(result).isInstanceOf(List.class).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
+
+ }
+
+ @org.junit.jupiter.api.Test
+ void testBundleDTOByIdCommand() throws Exception {
+ Object result = session.execute("bundleDTO 0");
+ assertThat(result).hasFieldOrProperty("id");
+ }
+
+ @org.junit.jupiter.api.Test
+ void testServiceReferenceDTOCommand() throws Exception {
+ Object result = session.execute("serviceReferenceDTO");
+ assertThat(result).isInstanceOf(List.class).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
+ }
+
+ @org.junit.jupiter.api.Test
+ void testServiceReferenceDTOByIdCommand() throws Exception {
+ Object allServices = session.execute("serviceReferenceDTO");
+ assertThat(allServices).isInstanceOf(List.class).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
+
+ List> services = (List>) allServices;
+ ServiceReferenceDTO firstDTO = (ServiceReferenceDTO) services.get(0);
+
+ long serviceId = firstDTO.id;
+ Object singleService = session.execute("serviceReferenceDTO " + serviceId);
+ assertThat(singleService).isInstanceOf(ServiceReferenceDTO.class);
+
+ ServiceReferenceDTO singleRef = (ServiceReferenceDTO) singleService;
+ assertThat(singleRef.id).isEqualTo(serviceId);
+ }
+
+ @org.junit.jupiter.api.Test
+ void testSrvCommand() throws Exception {
+// Object result = session.execute("srv 1");
+// assertThat(result).isInstanceOf(List.class).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
+ }
+
+ @org.junit.jupiter.api.Test
+ void testBsnCommand() throws Exception {
+ Object result = session.execute("bsn 0");
+ assertThat(result).isInstanceOf(String.class).asString().isNotBlank();
+ }
+
+ @org.junit.jupiter.api.Test
+ void testStartlevelCommand() throws Exception {
+ Object result = session.execute("startlevel");
+ assertThat(result).isNotNull();
+ }
+
+ @org.junit.jupiter.api.Test
+ void testStartlevelOfBundle() throws Exception {
+ Object result = session.execute("startlevel 0");
+ assertThat(result).isNotNull();
+ }
+
+ @org.junit.jupiter.api.Test
+ void testHeadersCommand() throws Exception {
+ String command = "headers -h Bundle-Name";
+ Object result = session.execute(command);
+
+ assertThat(result).isInstanceOf(Map.class);
+
+ }
+
+}
diff --git a/osgi.framework/test.bndrun b/osgi.framework/test.bndrun
new file mode 100644
index 0000000..a12a44b
--- /dev/null
+++ b/osgi.framework/test.bndrun
@@ -0,0 +1,27 @@
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}'
+
+-runfw: org.apache.felix.framework
+
+-runee: JavaSE-17
+
+-tester: biz.aQute.tester.junit-platform
+-resolve.effective: active; skip:='org.apache.felix.gogo'
+-runbundles: \
+ assertj-core;version='[3.26.0,3.26.1)',\
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-jupiter-engine;version='[5.11.1,5.11.2)',\
+ junit-jupiter-params;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ junit-platform-launcher;version='[1.11.1,1.11.2)',\
+ net.bytebuddy.byte-buddy;version='[1.15.3,1.15.4)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.osgi.framework;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.framework-tests;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.osgi.test.common;version='[1.3.0,1.3.1)',\
+ org.osgi.test.junit5;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.command.converter.bundle;version='[0.0.1,0.0.2)'
\ No newline at end of file
diff --git a/osgi.service.http/.gitignore b/osgi.service.http/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/osgi.service.http/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/osgi.service.http/bnd.bnd b/osgi.service.http/bnd.bnd
new file mode 100644
index 0000000..f2ce7c6
--- /dev/null
+++ b/osgi.service.http/bnd.bnd
@@ -0,0 +1,4 @@
+Import-Package: \
+ org.osgi.service.http.runtime;'resolution:'=optional,\
+ org.osgi.service.http.runtime.dto;'resolution:'=optional,\
+ *
\ No newline at end of file
diff --git a/osgi.service.http/play.bndrun b/osgi.service.http/play.bndrun
new file mode 100644
index 0000000..9f9f6df
--- /dev/null
+++ b/osgi.service.http/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.http;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.http-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/osgi.service.http/pom.xml b/osgi.service.http/pom.xml
new file mode 100644
index 0000000..3783300
--- /dev/null
+++ b/osgi.service.http/pom.xml
@@ -0,0 +1,109 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ osgi.service.http
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.service.http.whiteboard
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.jetty
+ 5.1.32
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.servlet-api
+ 3.0.0
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/osgi.service.http/src/main/java/org/eclipse/osgi/technology/command/osgi/service/http/Activator.java b/osgi.service.http/src/main/java/org/eclipse/osgi/technology/command/osgi/service/http/Activator.java
new file mode 100644
index 0000000..c7393ec
--- /dev/null
+++ b/osgi.service.http/src/main/java/org/eclipse/osgi/technology/command/osgi/service/http/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.http;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new HttpWhiteboardCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new HttpWhiteboardConverter(formatter));
+ }
+
+ private static class HttpWhiteboardConverter extends BaseDTOFormatterConverter {
+
+ public HttpWhiteboardConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/osgi.service.http/src/main/java/org/eclipse/osgi/technology/command/osgi/service/http/HttpWhiteboardCommands.java b/osgi.service.http/src/main/java/org/eclipse/osgi/technology/command/osgi/service/http/HttpWhiteboardCommands.java
new file mode 100644
index 0000000..0d02a07
--- /dev/null
+++ b/osgi.service.http/src/main/java/org/eclipse/osgi/technology/command/osgi/service/http/HttpWhiteboardCommands.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.http;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.dto.ServiceReferenceDTO;
+import org.osgi.service.http.runtime.HttpServiceRuntime;
+import org.osgi.service.http.runtime.dto.DTOConstants;
+import org.osgi.service.http.runtime.dto.ErrorPageDTO;
+import org.osgi.service.http.runtime.dto.FailedErrorPageDTO;
+import org.osgi.service.http.runtime.dto.FailedFilterDTO;
+import org.osgi.service.http.runtime.dto.FailedListenerDTO;
+import org.osgi.service.http.runtime.dto.FailedPreprocessorDTO;
+import org.osgi.service.http.runtime.dto.FailedResourceDTO;
+import org.osgi.service.http.runtime.dto.FailedServletContextDTO;
+import org.osgi.service.http.runtime.dto.FailedServletDTO;
+import org.osgi.service.http.runtime.dto.FilterDTO;
+import org.osgi.service.http.runtime.dto.ListenerDTO;
+import org.osgi.service.http.runtime.dto.PreprocessorDTO;
+import org.osgi.service.http.runtime.dto.RequestInfoDTO;
+import org.osgi.service.http.runtime.dto.ResourceDTO;
+import org.osgi.service.http.runtime.dto.RuntimeDTO;
+import org.osgi.service.http.runtime.dto.ServletContextDTO;
+import org.osgi.service.http.runtime.dto.ServletDTO;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class HttpWhiteboardCommands {
+
+ final ServiceTracker tracker;
+ final BundleContext context;
+
+ public HttpWhiteboardCommands(BundleContext context, DTOFormatter formatter) {
+ this.context = context;
+ dtos(formatter);
+ tracker = new ServiceTracker<>(context, HttpServiceRuntime.class, null);
+ tracker.open();
+ }
+
+ void dtos(DTOFormatter formatter) {
+ formatter.build(RuntimeDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] port: %s", dto.serviceDTO.id, port(dto)));
+
+ formatter.build(FailedServletContextDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ServletContextDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedPreprocessorDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s]", dto.serviceId));
+
+ formatter.build(PreprocessorDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] ", dto.serviceId));
+
+ formatter.build(FailedServletDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ServletDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedResourceDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s]", dto.serviceId));
+
+ formatter.build(ResourceDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s]", dto.serviceId));
+
+ formatter.build(FailedFilterDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s]", dto.serviceId));
+
+ formatter.build(FilterDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedErrorPageDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ErrorPageDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedListenerDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s]", dto.serviceId));
+
+ formatter.build(ListenerDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s]", dto.serviceId));
+
+ formatter.build(RequestInfoDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] ", dto.path));
+
+ formatter.build(ServiceReferenceDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] port[%s] ", dto.id, port(dto)));
+
+ }
+
+ private List serviceRuntimes() {
+
+ List list = new ArrayList<>();
+
+ if (!tracker.isEmpty()) {
+ for (Object httpServiceRuntimeO : tracker.getServices()) {
+ var h = (HttpServiceRuntime) httpServiceRuntimeO;
+ list.add(h);
+ }
+ }
+
+ return list;
+ }
+
+ private HttpServiceRuntime serviceRuntime() {
+
+ return tracker.getService();
+ }
+
+ @Descriptor("Show the RuntimeDTO of the HttpServiceRuntime")
+ public List httpRts() throws InterruptedException {
+
+ return serviceRuntimes().stream().map(HttpServiceRuntime::getRuntimeDTO).collect(Collectors.toList());
+ }
+
+ @Descriptor("Show the RuntimeDTO of the HttpServiceRuntime")
+ public RuntimeDTO httpRt(@Descriptor("Port") @Parameter(absentValue = "", names = "-p") String port)
+ throws InterruptedException {
+
+ return runtime(port).map(HttpServiceRuntime::getRuntimeDTO).orElse(null);
+ }
+
+ @Descriptor("Show the RequestInfoDTO of the HttpServiceRuntime")
+ public RequestInfoDTO httpRi(@Descriptor("Port") @Parameter(absentValue = "", names = "-p") String port,
+ @Descriptor("Path") @Parameter(absentValue = "", names = "-pa") String path) throws InterruptedException {
+
+ return runtime(port).map(runtime -> runtime.calculateRequestInfoDTO(path)).orElse(null);
+ }
+
+ private Optional runtime(String port) {
+
+ if (port.isEmpty()) {
+ return Optional.ofNullable(serviceRuntime());
+ }
+ return serviceRuntimes().stream().filter(runtime -> runtimeHasPort(runtime, port)).findAny();
+ }
+
+ private static boolean runtimeHasPort(HttpServiceRuntime runtime, String port) {
+ return port == port(runtime);
+ }
+
+ private static String port(HttpServiceRuntime runtime) {
+ var runtimeDTO = runtime.getRuntimeDTO();
+ return port(runtimeDTO);
+ }
+
+ private static String port(RuntimeDTO runtimeDTO) {
+ return port(runtimeDTO.serviceDTO);
+ }
+
+ private static String port(ServiceReferenceDTO srDTO) {
+ var map = srDTO.properties;
+ var p = map.get("org.osgi.service.http.port").toString();
+ return p;
+ }
+
+ private String failedReason(int failureReason) {
+
+ return switch (failureReason) {
+ case DTOConstants.FAILURE_REASON_UNKNOWN -> "UNKNOWN";
+ case DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE -> "FAILURE_REASON_UNKNOWN";
+ case DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE -> "SERVICE_NOT_GETTABLE";
+ case DTOConstants.FAILURE_REASON_VALIDATION_FAILED -> "VALIDATION_FAILED";
+ case DTOConstants.FAILURE_REASON_SERVICE_IN_USE -> "SERVICE_IN_USE";
+ case DTOConstants.FAILURE_REASON_SERVLET_WRITE_TO_LOCATION_DENIED -> "SERVLET_WRITE_TO_LOCATION_DENIED";
+ case DTOConstants.FAILURE_REASON_WHITEBOARD_WRITE_TO_DEFAULT_DENIED -> "WHITEBOARD_WRITE_TO_DEFAULT_DENIED";
+ case DTOConstants.FAILURE_REASON_SERVLET_READ_FROM_DEFAULT_DENIED -> "SERVLET_READ_FROM_DEFAULT_DENIED";
+ case DTOConstants.FAILURE_REASON_WHITEBOARD_WRITE_TO_LOCATION_DENIED -> "WHITEBOARD_WRITE_TO_LOCATION_DENIED";
+ default -> failureReason + "";
+ };
+ }
+
+}
diff --git a/osgi.service.http/src/test/java/org/eclipse/osgi/technology/command/osgi/service/http/Test.java b/osgi.service.http/src/test/java/org/eclipse/osgi/technology/command/osgi/service/http/Test.java
new file mode 100644
index 0000000..e1c52fe
--- /dev/null
+++ b/osgi.service.http/src/test/java/org/eclipse/osgi/technology/command/osgi/service/http/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.http;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/osgi.service.jakartars/.gitignore b/osgi.service.jakartars/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/osgi.service.jakartars/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/osgi.service.jakartars/bnd.bnd b/osgi.service.jakartars/bnd.bnd
new file mode 100644
index 0000000..a0fec05
--- /dev/null
+++ b/osgi.service.jakartars/bnd.bnd
@@ -0,0 +1,4 @@
+Import-Package: \
+ org.osgi.service.jakartars.runtime;'resolution:'=optional,\
+ org.osgi.service.jakartars.runtime.dto;'resolution:'=optional,\
+ *
\ No newline at end of file
diff --git a/osgi.service.jakartars/play.bndrun b/osgi.service.jakartars/play.bndrun
new file mode 100644
index 0000000..a80f86d
--- /dev/null
+++ b/osgi.service.jakartars/play.bndrun
@@ -0,0 +1,20 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ jakarta.ws.rs-api;version='[3.1.0,3.1.1)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.jakartars;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.jakartars-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/osgi.service.jakartars/pom.xml b/osgi.service.jakartars/pom.xml
new file mode 100644
index 0000000..8618536
--- /dev/null
+++ b/osgi.service.jakartars/pom.xml
@@ -0,0 +1,148 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ osgi.service.jakartars
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.service.jakartars
+ 2.0.0
+
+
+ org.eclipse.osgi-technology.rest
+ org.eclipse.osgitech.rest.jetty
+ 1.2.3
+ test
+
+
+ org.eclipse.osgi-technology.rest
+ org.eclipse.osgitech.rest.config
+ 1.2.3
+ test
+
+
+ org.apache.felix
+ org.apache.felix.configurator
+ 1.0.18
+ test
+
+
+ org.eclipse.parsson
+ jakarta.json
+ 1.1.7
+
+
+ org.apache.felix
+ org.apache.felix.cm.json
+ 2.0.6
+
+
+ jakarta.json
+ jakarta.json-api
+ 2.1.3
+
+
+ org.eclipse.osgi-technology.rest
+ org.eclipse.osgitech.rest.jetty
+ 1.2.3
+ test
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.jetty
+ 5.1.32
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.servlet-api
+ 3.0.0
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/osgi.service.jakartars/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/Activator.java b/osgi.service.jakartars/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/Activator.java
new file mode 100644
index 0000000..d23913f
--- /dev/null
+++ b/osgi.service.jakartars/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.jakartars;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new JakartaRsCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new JakartaRsConverter(formatter));
+ }
+
+ private static class JakartaRsConverter extends BaseDTOFormatterConverter {
+
+ public JakartaRsConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/osgi.service.jakartars/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/JakartaRsCommands.java b/osgi.service.jakartars/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/JakartaRsCommands.java
new file mode 100644
index 0000000..a6a5cf1
--- /dev/null
+++ b/osgi.service.jakartars/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/JakartaRsCommands.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.jakartars;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.dto.ServiceReferenceDTO;
+import org.osgi.service.jakartars.runtime.JakartarsServiceRuntime;
+import org.osgi.service.jakartars.runtime.dto.ApplicationDTO;
+import org.osgi.service.jakartars.runtime.dto.DTOConstants;
+import org.osgi.service.jakartars.runtime.dto.ExtensionDTO;
+import org.osgi.service.jakartars.runtime.dto.FailedApplicationDTO;
+import org.osgi.service.jakartars.runtime.dto.FailedExtensionDTO;
+import org.osgi.service.jakartars.runtime.dto.FailedResourceDTO;
+import org.osgi.service.jakartars.runtime.dto.ResourceDTO;
+import org.osgi.service.jakartars.runtime.dto.ResourceMethodInfoDTO;
+import org.osgi.service.jakartars.runtime.dto.RuntimeDTO;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class JakartaRsCommands {
+
+ final ServiceTracker tracker;
+ final BundleContext context;
+
+ public JakartaRsCommands(BundleContext context, DTOFormatter formatter) {
+ this.context = context;
+ dtos(formatter);
+ // dtos(formatter);
+ tracker = new ServiceTracker<>(context, JakartarsServiceRuntime.class, null);
+ tracker.open();
+ }
+
+ void dtos(DTOFormatter formatter) {
+ formatter.build(RuntimeDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] ", dto.serviceDTO.id));
+
+ formatter.build(FailedApplicationDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ApplicationDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedExtensionDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ExtensionDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedResourceDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ResourceDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ResourceMethodInfoDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.method, dto.path));
+
+ formatter.build(ServiceReferenceDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] endpoint [%s] ", dto.id, endpoint(dto)));
+
+ }
+
+ private static String endpoint(ServiceReferenceDTO srDTO) {
+ var map = srDTO.properties;
+ var p = map.get("osgi.jakartars.endpoint");
+
+ String s = "-";
+ if (p instanceof String) {
+ s = (String) p;
+ } else if (p instanceof String[] sarr) {
+ s = Stream.of(sarr).collect(Collectors.joining(","));
+ }
+
+ return s;
+ }
+
+ private List serviceRuntimes() {
+
+ return Arrays.asList((JakartarsServiceRuntime[]) tracker.getServices());
+ }
+
+ private JakartarsServiceRuntime serviceRuntime() {
+
+ return tracker.getService();
+ }
+
+ @Descriptor("Show the RuntimeDTO of the JakartarsServiceRuntime")
+ public List jakartarsRts() throws InterruptedException {
+
+ return serviceRuntimes().stream().map(JakartarsServiceRuntime::getRuntimeDTO).collect(Collectors.toList());
+ }
+
+ @Descriptor("Show the RuntimeDTO of the JakartarsServiceRuntime")
+ public RuntimeDTO jakartarsRt(
+ @Descriptor("service.id") @Parameter(absentValue = "-1", names = "-s") long service_id)
+ throws InterruptedException {
+
+ return runtime(service_id).map(JakartarsServiceRuntime::getRuntimeDTO).orElse(null);
+ }
+
+ private Optional runtime(long service_id) {
+
+ if (service_id < 0) {
+ return Optional.ofNullable(serviceRuntime());
+ }
+ return serviceRuntimes().stream().filter(runtime -> runtimeHasServiceId(runtime, service_id)).findAny();
+ }
+
+ private static boolean runtimeHasServiceId(JakartarsServiceRuntime runtime, long service_id) {
+
+ return service_id == runtime.getRuntimeDTO().serviceDTO.id;
+ }
+
+ private String failedReason(int failureReason) {
+
+ return switch (failureReason) {
+ case DTOConstants.FAILURE_REASON_UNKNOWN -> "UNKNOWN";
+ case DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE -> "FAILURE_REASON_UNKNOWN";
+ case DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE -> "SERVICE_NOT_GETTABLE";
+ case DTOConstants.FAILURE_REASON_VALIDATION_FAILED -> "VALIDATION_FAILED";
+ case DTOConstants.FAILURE_REASON_REQUIRED_EXTENSIONS_UNAVAILABLE -> "REQUIRED_EXTENSIONS_UNAVAILABLE";
+ case DTOConstants.FAILURE_REASON_DUPLICATE_NAME -> "DUPLICATE_NAME";
+ case DTOConstants.FAILURE_REASON_REQUIRED_APPLICATION_UNAVAILABLE -> "REQUIRED_APPLICATION_UNAVAILABLE";
+ default -> failureReason + "";
+ };
+ }
+
+}
diff --git a/osgi.service.jakartars/src/test/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/Test.java b/osgi.service.jakartars/src/test/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/Test.java
new file mode 100644
index 0000000..dc4ed48
--- /dev/null
+++ b/osgi.service.jakartars/src/test/java/org/eclipse/osgi/technology/command/osgi/service/jakartars/Test.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.jakartars;
+
+import jakarta.ws.rs.GET;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+
+ static class Res {
+
+ @GET
+ String me() {
+ return "foo";
+ }
+ }
+}
diff --git a/osgi.service.jaxrs/.gitignore b/osgi.service.jaxrs/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/osgi.service.jaxrs/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/osgi.service.jaxrs/bnd.bnd b/osgi.service.jaxrs/bnd.bnd
new file mode 100644
index 0000000..39c0f1a
--- /dev/null
+++ b/osgi.service.jaxrs/bnd.bnd
@@ -0,0 +1,4 @@
+Import-Package: \
+ org.osgi.service.jaxrs.runtime;'resolution:'=optional,\
+ org.osgi.service.jaxrs.runtime.dto;'resolution:'=optional,\
+ *
\ No newline at end of file
diff --git a/osgi.service.jaxrs/play.bndrun b/osgi.service.jaxrs/play.bndrun
new file mode 100644
index 0000000..ecc59a1
--- /dev/null
+++ b/osgi.service.jaxrs/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.jaxrs;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.jaxrs-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/osgi.service.jaxrs/pom.xml b/osgi.service.jaxrs/pom.xml
new file mode 100644
index 0000000..a832088
--- /dev/null
+++ b/osgi.service.jaxrs/pom.xml
@@ -0,0 +1,127 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ osgi.service.jaxrs
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.service.jaxrs
+ 1.0.0
+
+
+ org.apache.aries.spec
+ org.apache.aries.javax.jax.rs-api
+ 1.0.4
+ test
+
+
+ com.sun.xml.bind
+ jaxb-osgi
+ 2.3.3
+ test
+
+
+ org.apache.aries.jax.rs
+ org.apache.aries.jax.rs.whiteboard
+ 2.0.2
+ test
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.jetty
+ 5.1.32
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.servlet-api
+ 3.0.0
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/osgi.service.jaxrs/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/Activator.java b/osgi.service.jaxrs/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/Activator.java
new file mode 100644
index 0000000..5af1b61
--- /dev/null
+++ b/osgi.service.jaxrs/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.jaxrs;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new JaxRsWhiteboardCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new JaxRsWhiteboardConverter(formatter));
+ }
+
+ private static class JaxRsWhiteboardConverter extends BaseDTOFormatterConverter {
+
+ public JaxRsWhiteboardConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/osgi.service.jaxrs/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/JaxRsWhiteboardCommands.java b/osgi.service.jaxrs/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/JaxRsWhiteboardCommands.java
new file mode 100644
index 0000000..b9f4973
--- /dev/null
+++ b/osgi.service.jaxrs/src/main/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/JaxRsWhiteboardCommands.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.jaxrs;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.jaxrs.runtime.JaxrsServiceRuntime;
+import org.osgi.service.jaxrs.runtime.dto.ApplicationDTO;
+import org.osgi.service.jaxrs.runtime.dto.DTOConstants;
+import org.osgi.service.jaxrs.runtime.dto.ExtensionDTO;
+import org.osgi.service.jaxrs.runtime.dto.FailedApplicationDTO;
+import org.osgi.service.jaxrs.runtime.dto.FailedExtensionDTO;
+import org.osgi.service.jaxrs.runtime.dto.FailedResourceDTO;
+import org.osgi.service.jaxrs.runtime.dto.ResourceDTO;
+import org.osgi.service.jaxrs.runtime.dto.ResourceMethodInfoDTO;
+import org.osgi.service.jaxrs.runtime.dto.RuntimeDTO;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class JaxRsWhiteboardCommands {
+
+ final ServiceTracker tracker;
+ final BundleContext context;
+
+ public JaxRsWhiteboardCommands(BundleContext context, DTOFormatter formatter) {
+ this.context = context;
+ dtos(formatter);
+ // dtos(formatter);
+ tracker = new ServiceTracker<>(context, JaxrsServiceRuntime.class, null);
+ tracker.open();
+ }
+
+ void dtos(DTOFormatter formatter) {
+ formatter.build(RuntimeDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] ", dto.serviceDTO.id));
+
+ formatter.build(FailedApplicationDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ApplicationDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedExtensionDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ExtensionDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(FailedResourceDTO.class).inspect().fields("*").line().fields("*")
+ .format("failureReason", f -> failedReason(f.failureReason)).part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ResourceDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.serviceId, dto.name));
+
+ formatter.build(ResourceMethodInfoDTO.class).inspect().fields("*").line().fields("*").part()
+ .as(dto -> String.format("[%s] %s", dto.method, dto.path));
+
+ }
+
+ private List serviceRuntimes() {
+
+ return Arrays.asList((JaxrsServiceRuntime[]) tracker.getServices());
+ }
+
+ private JaxrsServiceRuntime serviceRuntime() {
+
+ return tracker.getService();
+ }
+
+ @Descriptor("Show the RuntimeDTO of the JaxrsServiceRuntime")
+ public List jaxrsRts() throws InterruptedException {
+
+ return serviceRuntimes().stream().map(JaxrsServiceRuntime::getRuntimeDTO).collect(Collectors.toList());
+ }
+
+ @Descriptor("Show the RuntimeDTO of the JaxrsServiceRuntime")
+ public RuntimeDTO jaxrsRt(@Descriptor("service.id") @Parameter(absentValue = "-1", names = "-s") long service_id)
+ throws InterruptedException {
+
+ return runtime(service_id).map(JaxrsServiceRuntime::getRuntimeDTO).orElse(null);
+ }
+
+ private Optional runtime(long service_id) {
+
+ if (service_id < 0) {
+ return Optional.ofNullable(serviceRuntime());
+ }
+ return serviceRuntimes().stream().filter(runtime -> runtimeHasServiceId(runtime, service_id)).findAny();
+ }
+
+ private static boolean runtimeHasServiceId(JaxrsServiceRuntime runtime, long service_id) {
+
+ return service_id == runtime.getRuntimeDTO().serviceDTO.id;
+ }
+
+ private String failedReason(int failureReason) {
+
+ return switch (failureReason) {
+ case DTOConstants.FAILURE_REASON_UNKNOWN -> "UNKNOWN";
+ case DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE -> "FAILURE_REASON_UNKNOWN";
+ case DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE -> "SERVICE_NOT_GETTABLE";
+ case DTOConstants.FAILURE_REASON_VALIDATION_FAILED -> "VALIDATION_FAILED";
+ case DTOConstants.FAILURE_REASON_REQUIRED_EXTENSIONS_UNAVAILABLE -> "REQUIRED_EXTENSIONS_UNAVAILABLE";
+ case DTOConstants.FAILURE_REASON_DUPLICATE_NAME -> "DUPLICATE_NAME";
+ case DTOConstants.FAILURE_REASON_REQUIRED_APPLICATION_UNAVAILABLE -> "REQUIRED_APPLICATION_UNAVAILABLE";
+ default -> failureReason + "";
+ };
+ }
+
+}
diff --git a/osgi.service.jaxrs/src/test/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/Test.java b/osgi.service.jaxrs/src/test/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/Test.java
new file mode 100644
index 0000000..499e164
--- /dev/null
+++ b/osgi.service.jaxrs/src/test/java/org/eclipse/osgi/technology/command/osgi/service/jaxrs/Test.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.jaxrs;
+
+public class Test {
+
+ @org.junit.jupiter.api.Test
+ void testName() throws Exception {
+
+ }
+}
diff --git a/osgi.service.scr/.gitignore b/osgi.service.scr/.gitignore
new file mode 100644
index 0000000..651f810
--- /dev/null
+++ b/osgi.service.scr/.gitignore
@@ -0,0 +1,2 @@
+/target/
+/generated/
\ No newline at end of file
diff --git a/osgi.service.scr/bnd.bnd b/osgi.service.scr/bnd.bnd
new file mode 100644
index 0000000..0b19d32
--- /dev/null
+++ b/osgi.service.scr/bnd.bnd
@@ -0,0 +1,4 @@
+Import-Package: \
+ org.osgi.service.component.runtime;'resolution:'=optional,\
+ org.osgi.service.component.runtime.dto;'resolution:'=optional,\
+ *
\ No newline at end of file
diff --git a/osgi.service.scr/play.bndrun b/osgi.service.scr/play.bndrun
new file mode 100644
index 0000000..9c0c8ae
--- /dev/null
+++ b/osgi.service.scr/play.bndrun
@@ -0,0 +1,19 @@
+
+-runrequires: \
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}-tests',\
+ bnd.identity;id='org.eclipse.osgi-technology.command.${project.artifactId}',\
+ bnd.identity;id='org.eclipse.osgi-technology.console.plain',\
+ bnd.identity;id=junit-platform-engine
+
+-runfw: org.apache.felix.framework
+-runbundles: \
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ org.apache.felix.gogo.runtime;version='[1.1.6,1.1.7)',\
+ org.eclipse.osgi-technology.command.util;version='[0.0.1,0.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.eclipse.osgi-technology.console.plain;version='[0.0.1,0.0.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.scr;version='[0.0.1,0.0.2)',\
+ org.eclipse.osgi-technology.command.osgi.service.scr-tests;version='[0.0.1,0.0.2)'
+-runee: JavaSE-17
\ No newline at end of file
diff --git a/osgi.service.scr/pom.xml b/osgi.service.scr/pom.xml
new file mode 100644
index 0000000..44dfda5
--- /dev/null
+++ b/osgi.service.scr/pom.xml
@@ -0,0 +1,109 @@
+
+
+
+ 4.0.0
+
+ org.eclipse.osgi-technology.command
+ command
+ 0.0.1-SNAPSHOT
+
+ osgi.service.scr
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.osgi
+ org.osgi.service.http.whiteboard
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.util.tracker
+
+
+ org.apache.felix
+ org.apache.felix.gogo.runtime
+ provided
+
+
+ org.osgi
+ org.osgi.resource
+
+
+ org.osgi
+ org.osgi.dto
+ 1.1.1
+
+
+ org.osgi
+ org.osgi.framework
+
+
+
+ org.osgi
+ org.osgi.annotation.bundle
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ 1.1.2
+
+
+ org.eclipse.osgi-technology.command
+ util
+ 0.0.1-SNAPSHOT
+
+
+ org.eclipse.osgi-technology.console
+ plain
+ 0.0.1-SNAPSHOT
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.1
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.jetty
+ 5.1.32
+ test
+
+
+ org.apache.felix
+ org.apache.felix.http.servlet-api
+ 3.0.0
+ test
+
+
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+
+
+
diff --git a/osgi.service.scr/src/main/java/org/eclipse/osgi/technology/command/osgi/service/scr/Activator.java b/osgi.service.scr/src/main/java/org/eclipse/osgi/technology/command/osgi/service/scr/Activator.java
new file mode 100644
index 0000000..07de893
--- /dev/null
+++ b/osgi.service.scr/src/main/java/org/eclipse/osgi/technology/command/osgi/service/scr/Activator.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.scr;
+
+import java.util.Map;
+
+import org.eclipse.osgi.technology.command.util.AbstractActivator;
+import org.eclipse.osgi.technology.command.util.BaseDTOFormatterConverter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.osgi.annotation.bundle.Header;
+import org.osgi.framework.Constants;
+
+@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@org.osgi.annotation.bundle.Capability(namespace = "org.apache.felix.gogo", name = "command.implementation", version = "1.0.0")
+@org.osgi.annotation.bundle.Requirement(effective = "active", namespace = "org.apache.felix.gogo", name = "runtime.implementation", version = "1.0.0")
+public class Activator extends AbstractActivator {
+
+ @Override
+ protected void init() {
+ registerCommandService(new ScrCommands(context, formatter), "tech", Map.of());
+ registerConverterService(new HttpWhiteboardConverter(formatter));
+ }
+
+ private static class HttpWhiteboardConverter extends BaseDTOFormatterConverter {
+
+ public HttpWhiteboardConverter(DTOFormatter formatter) {
+ super(formatter);
+ }
+ }
+}
diff --git a/osgi.service.scr/src/main/java/org/eclipse/osgi/technology/command/osgi/service/scr/ScrCommands.java b/osgi.service.scr/src/main/java/org/eclipse/osgi/technology/command/osgi/service/scr/ScrCommands.java
new file mode 100644
index 0000000..94f42d7
--- /dev/null
+++ b/osgi.service.scr/src/main/java/org/eclipse/osgi/technology/command/osgi/service/scr/ScrCommands.java
@@ -0,0 +1,336 @@
+/**
+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Stefan Bischof - initial
+ */
+package org.eclipse.osgi.technology.command.osgi.service.scr;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.felix.service.command.Descriptor;
+import org.apache.felix.service.command.Parameter;
+import org.eclipse.osgi.technology.command.util.DisplayUtil;
+import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter;
+import org.eclipse.osgi.technology.command.util.dtoformatter.Wrapper;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.dto.ServiceReferenceDTO;
+import org.osgi.service.component.runtime.ServiceComponentRuntime;
+import org.osgi.service.component.runtime.dto.ComponentConfigurationDTO;
+import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
+import org.osgi.service.component.runtime.dto.ReferenceDTO;
+import org.osgi.service.component.runtime.dto.SatisfiedReferenceDTO;
+import org.osgi.service.component.runtime.dto.UnsatisfiedReferenceDTO;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class ScrCommands implements Closeable {
+
+ final ServiceTracker scr;
+ final BundleContext context;
+ final AtomicLong componentDescriptionNextIndex = new AtomicLong(-1);
+ final Map descriptionToIndex = new HashMap<>();
+
+ public ScrCommands(BundleContext context, DTOFormatter formatter) {
+ this.context = context;
+ dtos(formatter);
+ scr = new ServiceTracker<>(context, ServiceComponentRuntime.class, null);
+ scr.open();
+ }
+
+ @Descriptor("Show the list of available components")
+ public Object ds(
+ @Parameter(names = { "-c", "--config" }, presentValue = "true", absentValue = "false") boolean configs,
+ @Parameter(names = { "-b", "--bundle" }, absentValue = "0") Bundle bs[]) {
+
+ if (bs == null || bs.length == 1 && bs[0].getBundleId() == 0) {
+ bs = new Bundle[0];
+ }
+
+ if (configs) {
+ return getScr().getComponentDescriptionDTOs(bs);
+ } else {
+ return getScr().getComponentDescriptionDTOs(bs).stream()
+ .flatMap(d -> getScr().getComponentConfigurationDTOs(d).stream()).collect(Collectors.toList());
+ }
+ }
+
+ @Descriptor("Show the list of available components")
+ public Object ds() {
+
+ return getScr().getComponentDescriptionDTOs(context.getBundles()).stream()
+ .flatMap(d -> getScr().getComponentConfigurationDTOs(d).stream()).collect(Collectors.toList());
+ }
+
+ //
+ // The SCR overrides the formatter
+ //
+ @Descriptor("Show ds components")
+ public Wrapper ds(long component) {
+ if (component >= 0) {
+ return new Wrapper(dsConfig(component));
+ } else {
+ return new Wrapper(getDescription(component));
+ }
+ }
+
+ private ComponentConfigurationDTO dsConfig(long component) {
+ return getScr().getComponentDescriptionDTOs().stream()
+ .flatMap(d -> getScr().getComponentConfigurationDTOs(d).stream()).filter(c -> component == c.id)
+ .findFirst().orElse(null);
+ }
+
+ public static class Why {
+ public ComponentDescriptionDTO description;
+ public ComponentConfigurationDTO configuration;
+ public String reason;
+ public List references = new ArrayList<>();
+ }
+
+ public static class WhyReference {
+ public WhyReference(String name, List candidates) {
+ this.name = name;
+ this.candidates = candidates;
+ }
+
+ public String name;
+ public List candidates;
+ }
+
+ @Descriptor("show a dependency tree if not satisfied")
+ public List why(long component) {
+ var ds = dsConfig(component);
+ if (ds == null) {
+ return null;
+ }
+
+ var why = why(ds, new HashSet<>());
+ return why.references;
+ }
+
+ Why why(ComponentConfigurationDTO ds, Set