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 set) { + if (!set.add(ds)) { + return null; + } + + var why = new Why(); + why.description = ds.description; + why.configuration = ds; + + for (UnsatisfiedReferenceDTO unsatisfiedReference : ds.unsatisfiedReferences) { + var ref = getReference(ds.description, unsatisfiedReference.name); + why.references.add(new WhyReference(ref.name, candidates(ref, set))); + } + return why; + } + + private List candidates(ReferenceDTO ref, Set set) { + List candidates = new ArrayList<>(); + + List collect = getScr().getComponentDescriptionDTOs().stream() + .filter(cd -> Stream.of(cd.serviceInterfaces).filter(ref.interfaceName::equals).findAny().isPresent()) + .collect(Collectors.toList()); + + for (ComponentDescriptionDTO d : collect) { + + if ("REQUIRE".equals(d.configurationPolicy)) { + var required = new Why(); + required.description = d; + required.reason = "may require configuration"; + candidates.add(required); + } + + for (ComponentConfigurationDTO c : getScr().getComponentConfigurationDTOs(d)) { + var why = why(c, set); + if (why == null) { + var cycle = new Why(); + cycle.description = d; + cycle.configuration = c; + cycle.reason = "cycle"; + candidates.add(cycle); + } else { + candidates.add(why); + } + } + } + return candidates; + } + + private ReferenceDTO getReference(ComponentDescriptionDTO description, String name) { + return Stream.of(description.references).filter(r -> r.name.equals(name)).findFirst() + .orElseThrow(() -> new IllegalStateException( + "the description " + description + " does not have the reference named " + name)); + } + + private ServiceComponentRuntime getScr() { + return scr.getService(); + } + + @Override + public void close() throws IOException { + scr.close(); + } + + void dtos(DTOFormatter formatter) { + formatter.build(ComponentDescriptionDTO.class).inspect().fields("*").line().format("id", this::getId) + .format("bundle", cd -> cd.bundle.id).field("name").field("defaultEnabled").field("immediate") + .field("activate").field("modified").field("deactivate").field("configurationPolicy") + .field("configurationPid").field("references").count().part() + .as(cdd -> String.format("[%s] %s", cdd.bundle.id, cdd.name)); + + formatter.build(ComponentConfigurationDTO.class).inspect().format("state", this::state).fields("*").line() + .field("id").format("state", this::state) + .format("service", c -> DisplayUtil.objectClass(c.description.serviceInterfaces)).fields("*") + .remove("properties").remove("description").format("unsatisfiedReferences", this::unsatisfied).part() + .as(c -> "<" + c.id + "> " + c.description.name); + + formatter.build(SatisfiedReferenceDTO.class).inspect().fields("*").line().fields("*") + .format("boundServices", u -> shortend(u.boundServices)).part().as(sr -> sr.name); + + formatter.build(ServiceReferenceDTO.class).inspect().fields("*").line().format("id", srd -> srd.id + "") + .format("service", srd -> DisplayUtil.objectClass(srd.properties)).part() + .as(sr -> "(" + sr.id + ")" + DisplayUtil.objectClass(sr.properties)); + + formatter.build(UnsatisfiedReferenceDTO.class).inspect().fields("*").line().fields("*").part() + .as(sr -> sr.name); + + formatter.build(ReferenceDTO.class).inspect().fields("*").line().fields("*").part().field("name"); + + formatter.build(Unsatisfied.class).inspect().fields("*").line().fields("*").part().as(u -> u.name); + + formatter.build(Why.class).inspect().fields("*").line().format("description", w -> getId(w.description)) + .format("configuration", w -> w.configuration == null ? "" : w.configuration.id).field("reason") + .field("references").part().as(w -> "<<" + getId(w.description) + ">>"); + + formatter.build(WhyReference.class).inspect().fields("*").line().field("name").field("candidates").part() + .field("name"); + } + + static class ComponentDescriptionId { + private final String name; + private final Bundle bundle; + + ComponentDescriptionId(String name, Bundle bundle) { + this.name = name; + this.bundle = bundle; + } + + @Override + public int hashCode() { + return Objects.hash(bundle, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + var other = (ComponentDescriptionId) obj; + return Objects.equals(bundle, other.bundle) && Objects.equals(name, other.name); + } + + } + + long getId(ComponentDescriptionDTO description) { + return descriptionToIndex.computeIfAbsent( + new ComponentDescriptionId(description.name, context.getBundle(description.bundle.id)), + d -> componentDescriptionNextIndex.getAndDecrement()); + } + + ComponentDescriptionDTO getDescription(long id) { + return descriptionToIndex.entrySet().stream().filter(e -> e.getValue() == id).map(Entry::getKey).findFirst() + .map(cdi -> getScr().getComponentDescriptionDTO(cdi.bundle, cdi.name)).orElse(null); + } + + public static class Unsatisfied { + public String name; + public String interfaceName; + public String cardinality; + public String target; + public List candidates; + public String remark; + } + + private List unsatisfied(ComponentConfigurationDTO c) { + return Stream.of(c.unsatisfiedReferences).map(ur -> unsatisfied(c.description, ur)) + .collect(Collectors.toList()); + } + + private Unsatisfied unsatisfied(ComponentDescriptionDTO description, UnsatisfiedReferenceDTO r) { + var u = new Unsatisfied(); + try { + var ref = find(description, r.name); + u.name = r.name; + u.target = r.target; + u.interfaceName = DisplayUtil.shorten(ref.interfaceName); + u.cardinality = ref.cardinality; + + u.candidates = getServiceCandidates(r, ref); + + } catch (InvalidSyntaxException e) { + u.remark = e.toString(); + } + + return u; + + } + + private List getServiceCandidates(UnsatisfiedReferenceDTO r, ReferenceDTO ref) throws InvalidSyntaxException { + var serviceReferences = context.getServiceReferences(ref.interfaceName, r.target); + if (serviceReferences == null) { + serviceReferences = new ServiceReference[0]; + } + + return Stream.of(serviceReferences).map(x -> (Long) x.getProperty(Constants.SERVICE_ID)) + .collect(Collectors.toList()); + } + + private ReferenceDTO find(ComponentDescriptionDTO description, String name) { + for (ReferenceDTO r : description.references) { + if (r.name.equals(name)) { + return r; + } + } + throw new IllegalStateException("Not supposed to happen"); + } + + private List shortend(ServiceReferenceDTO[] refs) { + return Stream.of(refs).map(r -> "<" + r.id + "> " + DisplayUtil.objectClass(r.properties)) + .collect(Collectors.toList()); + } + + private String state(ComponentConfigurationDTO c) { + return switch (c.state) { + case ComponentConfigurationDTO.ACTIVE -> "ACTIVE"; + case ComponentConfigurationDTO.UNSATISFIED_CONFIGURATION -> "CONFG?"; + case ComponentConfigurationDTO.UNSATISFIED_REFERENCE -> "REFRN?"; + case ComponentConfigurationDTO.SATISFIED -> "SATSFD"; + default -> c.state + "?"; + }; + } +} diff --git a/osgi.service.scr/src/test/java/org/eclipse/osgi/technology/command/osgi/service/scr/Test.java b/osgi.service.scr/src/test/java/org/eclipse/osgi/technology/command/osgi/service/scr/Test.java new file mode 100644 index 0000000..123f68a --- /dev/null +++ b/osgi.service.scr/src/test/java/org/eclipse/osgi/technology/command/osgi/service/scr/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.scr; + +public class Test { + + @org.junit.jupiter.api.Test + void testName() throws Exception { + + } +} diff --git a/osgi.service.servlet/.gitignore b/osgi.service.servlet/.gitignore new file mode 100644 index 0000000..651f810 --- /dev/null +++ b/osgi.service.servlet/.gitignore @@ -0,0 +1,2 @@ +/target/ +/generated/ \ No newline at end of file diff --git a/osgi.service.servlet/bnd.bnd b/osgi.service.servlet/bnd.bnd new file mode 100644 index 0000000..700e527 --- /dev/null +++ b/osgi.service.servlet/bnd.bnd @@ -0,0 +1,4 @@ +Import-Package: \ + org.osgi.service.servlet.runtime;'resolution:'=optional,\ + org.osgi.service.servlet.runtime.dto;'resolution:'=optional,\ + * \ No newline at end of file diff --git a/osgi.service.servlet/play.bndrun b/osgi.service.servlet/play.bndrun new file mode 100644 index 0000000..9d77f9b --- /dev/null +++ b/osgi.service.servlet/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.servlet;version='[0.0.1,0.0.2)',\ + org.eclipse.osgi-technology.command.osgi.service.servlet-tests;version='[0.0.1,0.0.2)' +-runee: JavaSE-17 \ No newline at end of file diff --git a/osgi.service.servlet/pom.xml b/osgi.service.servlet/pom.xml new file mode 100644 index 0000000..b931fd1 --- /dev/null +++ b/osgi.service.servlet/pom.xml @@ -0,0 +1,109 @@ + + + + 4.0.0 + + org.eclipse.osgi-technology.command + command + 0.0.1-SNAPSHOT + + osgi.service.servlet + + + + org.slf4j + slf4j-api + + + org.osgi + org.osgi.service.servlet + 2.0.0 + + + 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.servlet/src/main/java/org/eclipse/osgi/technology/command/osgi/service/servlet/Activator.java b/osgi.service.servlet/src/main/java/org/eclipse/osgi/technology/command/osgi/service/servlet/Activator.java new file mode 100644 index 0000000..a10ca27 --- /dev/null +++ b/osgi.service.servlet/src/main/java/org/eclipse/osgi/technology/command/osgi/service/servlet/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.servlet; + +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 ServletWhiteboardCommand(context, formatter), "tech", Map.of()); + registerConverterService(new ServletWhiteboardConverter(formatter)); + } + + private static class ServletWhiteboardConverter extends BaseDTOFormatterConverter { + + public ServletWhiteboardConverter(DTOFormatter formatter) { + super(formatter); + } + } +} diff --git a/osgi.service.servlet/src/main/java/org/eclipse/osgi/technology/command/osgi/service/servlet/ServletWhiteboardCommand.java b/osgi.service.servlet/src/main/java/org/eclipse/osgi/technology/command/osgi/service/servlet/ServletWhiteboardCommand.java new file mode 100644 index 0000000..d6dd0d3 --- /dev/null +++ b/osgi.service.servlet/src/main/java/org/eclipse/osgi/technology/command/osgi/service/servlet/ServletWhiteboardCommand.java @@ -0,0 +1,203 @@ +/** + * 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.servlet; + +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.servlet.runtime.HttpServiceRuntime; +import org.osgi.service.servlet.runtime.dto.DTOConstants; +import org.osgi.service.servlet.runtime.dto.ErrorPageDTO; +import org.osgi.service.servlet.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.servlet.runtime.dto.FailedFilterDTO; +import org.osgi.service.servlet.runtime.dto.FailedListenerDTO; +import org.osgi.service.servlet.runtime.dto.FailedPreprocessorDTO; +import org.osgi.service.servlet.runtime.dto.FailedResourceDTO; +import org.osgi.service.servlet.runtime.dto.FailedServletContextDTO; +import org.osgi.service.servlet.runtime.dto.FailedServletDTO; +import org.osgi.service.servlet.runtime.dto.FilterDTO; +import org.osgi.service.servlet.runtime.dto.ListenerDTO; +import org.osgi.service.servlet.runtime.dto.PreprocessorDTO; +import org.osgi.service.servlet.runtime.dto.RequestInfoDTO; +import org.osgi.service.servlet.runtime.dto.ResourceDTO; +import org.osgi.service.servlet.runtime.dto.RuntimeDTO; +import org.osgi.service.servlet.runtime.dto.ServletContextDTO; +import org.osgi.service.servlet.runtime.dto.ServletDTO; +import org.osgi.util.tracker.ServiceTracker; + +public class ServletWhiteboardCommand { + + final ServiceTracker tracker; + final BundleContext context; + + public ServletWhiteboardCommand(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 static String port(ServiceReferenceDTO srDTO) { + var map = srDTO.properties; + var p = map.get("org.osgi.service.http.port").toString(); + return p; + } + + 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 servletRts() throws InterruptedException { + + return serviceRuntimes().stream().map(HttpServiceRuntime::getRuntimeDTO).collect(Collectors.toList()); + } + + @Descriptor("Show the RuntimeDTO of the HttpServiceRuntime") + public RuntimeDTO servletRt(@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 servletRi(@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) { + var map = runtimeDTO.serviceDTO.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.servlet/src/test/java/org/eclipse/osgi/technology/command/osgi/service/http/Test.java b/osgi.service.servlet/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.servlet/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/pom.xml b/pom.xml new file mode 100644 index 0000000..d22e25e --- /dev/null +++ b/pom.xml @@ -0,0 +1,395 @@ + + + + 4.0.0 + + + org.eclipse.osgi-technology + org.eclipse.osgi-technology.pom.parent + 0.0.1-SNAPSHOT + + + org.eclipse.osgi-technology.command + command + pom + + Reactor for command + https://github.com/eclipse-osgi-technology/command + + + util + + converter.file + converter.bundle + diagnostics + osgi.framework + osgi.framework.modify + osgi.service.scr + osgi.service.http + osgi.service.servlet + osgi.service.jaxrs + osgi.service.jakartars + fs.navigate + mxbeans + help + system.runtime + + + + + + central-snapshots + Central Snapshot + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + apache-snapshots + Apache Snapshots + https://repository.apache.org/snapshots/ + + false + + + true + + + + + + + StefanBischof + Stefan Bischof + stbischof@bipolis.org + + + + + + 17 + + + false + ${osgi.dependency.allowed} + + + 8.0.0 + 1.6.1 + 1.5.1 + 1.8.0 + 1.0.1 + 1.5.4 + 1.0.9 + 4.0.0-beta-4 + 2.0.1 + 2.0.11 + + + 7.0.5 + 1.9.26 + 2.2.2 + + + 1.3.0 + 5.11.1 + 1.11.1 + 5.14.1 + 1.3.7 + 2.0.1 + 1.2 + + + 3.8.0 + 1.2.1 + 3.4.0 + 3.3.1 + 3.13.0 + 3.3.0 + 3.4.2 + 3.1.2 + 3.1.2 + 3.12.1 + 3.6.1 + 3.6.0 + + + true + false + + + ${project.build.directory}/m2Repo + + + + + + org.osgi + osgi.annotation + ${osgi.annotation.version} + provided + + + org.osgi + org.osgi.framework + ${osgi.framework.version} + provided + + + org.osgi + org.osgi.util.tracker + ${osgi.util.tracker.version} + provided + + + org.osgi + org.osgi.resource + ${osgi.resource.version} + provided + + + org.osgi + org.osgi.service.cm + ${osgi.cm.version} + + + org.osgi + org.osgi.service.component + ${osgi.ds.version} + + + org.osgi + org.osgi.service.component.annotations + ${osgi.ds.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + + org.apache.felix + org.apache.felix.framework + ${felix.framework.version} + runtime + + + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.platform + junit-platform-commons + ${junit-platform.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + + org.osgi + org.osgi.test.junit5 + ${osgi.test.version} + test + + + org.osgi + org.osgi.test.junit5.cm + ${osgi.test.version} + test + + + org.osgi + org.osgi.test.common + ${osgi.test.version} + test + + + org.osgi + org.osgi.test.assertj.framework + ${osgi.test.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.platform + junit-platform-launcher + ${junit-platform.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + + + org.apache.felix + org.apache.felix.configadmin + ${felix.configadmin.version} + test + + + + + org.apache.felix + org.apache.felix.scr + ${felix.scr.version} + test + + + + commons-logging + commons-logging + ${commons.logging.version} + test + + + + + + + + org.junit.jupiter + junit-jupiter-api + + + org.junit.platform + junit-platform-commons + + + org.junit.jupiter + junit-jupiter-params + + + org.osgi + org.osgi.test.junit5 + + + org.osgi + org.osgi.test.junit5.cm + + + org.osgi + org.osgi.test.common + + + org.osgi + org.osgi.test.assertj.framework + + + org.junit.jupiter + junit-jupiter-engine + + + org.junit.platform + junit-platform-launcher + + + org.mockito + mockito-junit-jupiter + + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + + + + resolve-test + pre-integration-test + + resolve + + + + *.bndrun + + + + ${project.build.directory}/${project.build.finalName}-tests.jar + + false + true + false + + compile + runtime + test + + + + + + + biz.aQute.bnd + bnd-testing-maven-plugin + + + + testing + + testing + + + + test.bndrun + + false + true + false + + compile + runtime + test + + + + + + + + + + \ No newline at end of file diff --git a/system.runtime/.gitignore b/system.runtime/.gitignore new file mode 100644 index 0000000..651f810 --- /dev/null +++ b/system.runtime/.gitignore @@ -0,0 +1,2 @@ +/target/ +/generated/ \ No newline at end of file diff --git a/system.runtime/play.bndrun b/system.runtime/play.bndrun new file mode 100644 index 0000000..f5ebf08 --- /dev/null +++ b/system.runtime/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.system.runtime;version='[0.0.1,0.0.2)',\ + org.eclipse.osgi-technology.command.system.runtime-tests;version='[0.0.1,0.0.2)' +-runee: JavaSE-17 \ No newline at end of file diff --git a/system.runtime/pom.xml b/system.runtime/pom.xml new file mode 100644 index 0000000..d3884c3 --- /dev/null +++ b/system.runtime/pom.xml @@ -0,0 +1,93 @@ + + + + 4.0.0 + + org.eclipse.osgi-technology.command + command + 0.0.1-SNAPSHOT + + system.runtime + + + + + 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/system.runtime/src/main/java/org/eclipse/osgi/technology/command/system/runtime/Activator.java b/system.runtime/src/main/java/org/eclipse/osgi/technology/command/system/runtime/Activator.java new file mode 100644 index 0000000..1da1165 --- /dev/null +++ b/system.runtime/src/main/java/org/eclipse/osgi/technology/command/system/runtime/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.system.runtime; + +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 RuntimeCommands(formatter), "tech", Map.of()); + registerConverterService(new FrameworkConverter(formatter)); + } + + private static class FrameworkConverter extends BaseDTOFormatterConverter { + + public FrameworkConverter(DTOFormatter formatter) { + super(formatter); + } + } +} diff --git a/system.runtime/src/main/java/org/eclipse/osgi/technology/command/system/runtime/RuntimeCommands.java b/system.runtime/src/main/java/org/eclipse/osgi/technology/command/system/runtime/RuntimeCommands.java new file mode 100644 index 0000000..3086e1d --- /dev/null +++ b/system.runtime/src/main/java/org/eclipse/osgi/technology/command/system/runtime/RuntimeCommands.java @@ -0,0 +1,98 @@ +/** + * 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.system.runtime; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +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.dtoformatter.DTOFormatter; + +public class RuntimeCommands { + + public RuntimeCommands(DTOFormatter formatter) { + } + + @Descriptor("Show the amount of free memory") + public long freeMemory( + @Descriptor("Run a gc first") @Parameter(absentValue = "false", presentValue = "true", names = { "-g", + "--gc" }) boolean gc) { + if (gc) { + System.gc(); + } + return Runtime.getRuntime().freeMemory(); + } + + @Descriptor("Show memory info similar to /proc/meminfo") + public void meminfo() { + + long max = Runtime.getRuntime().maxMemory(); + long total = Runtime.getRuntime().totalMemory(); + long free = Runtime.getRuntime().freeMemory(); + long used = total - free; + + System.out.printf("MemMax: %d kB\n", max / 1024); + System.out.printf("MemTotal: %d kB\n", total / 1024); + System.out.printf("MemFree: %d kB\n", free / 1024); + System.out.printf("MemUsed: %d kB\n", used / 1024); + } + + @Descriptor("Show cpu info similar to /proc/cuinfo") + public String cpuinfo() { + + StringBuilder sb = new StringBuilder(); + + long processors = Runtime.getRuntime().availableProcessors(); + sb.append("ProcessorsAvailable: " + processors + " \n"); + sb.append("Architecture: " + System.getProperty("os.arch") + " \n"); + + return sb.toString(); + + } + + @Descriptor("Show cpu info similar to /proc/cuinfo") + public String jvminfo() { + StringBuilder sb = new StringBuilder(); + String version = Runtime.version().toString(); + sb.append("RuntimeVersion: " + version + " \n"); + sb.append("Architecture: " + System.getProperty("os.arch") + " \n"); + sb.append("OS Name: " + System.getProperty("os.name") + " \n"); + sb.append("OS Version: " + System.getProperty("os.version") + " \n"); + sb.append("Java Version: " + System.getProperty("java.version") + " \n"); + sb.append("Java Vendor: " + System.getProperty("java.vendor") + " \n"); + + return sb.toString(); + + } + + @Descriptor("showns current user") + public String whoami() { + return System.getProperty("user.name"); + } + + @Descriptor("Runs a gc") + public void gc() { + System.gc(); + } + + @Descriptor("Shows the current datetime") + public String date() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy"); + String dateOutput = LocalDateTime.now().format(formatter); + return dateOutput; + } + + +} diff --git a/system.runtime/src/test/java/org/eclipse/osgi/technology/command/system/runtime/Test.java b/system.runtime/src/test/java/org/eclipse/osgi/technology/command/system/runtime/Test.java new file mode 100644 index 0000000..4754652 --- /dev/null +++ b/system.runtime/src/test/java/org/eclipse/osgi/technology/command/system/runtime/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.system.runtime; + +public class Test { + + @org.junit.jupiter.api.Test + void testName() throws Exception { + + } +} diff --git a/util/.gitignore b/util/.gitignore new file mode 100644 index 0000000..651f810 --- /dev/null +++ b/util/.gitignore @@ -0,0 +1,2 @@ +/target/ +/generated/ \ No newline at end of file diff --git a/util/pom.xml b/util/pom.xml new file mode 100644 index 0000000..19ba04c --- /dev/null +++ b/util/pom.xml @@ -0,0 +1,121 @@ + + + + 4.0.0 + + org.eclipse.osgi-technology.command + command + 0.0.1-SNAPSHOT + + util + + + org.slf4j + slf4j-api + + + org.osgi + org.osgi.service.servlet + 2.0.0 + + + org.osgi + org.osgi.service.http.whiteboard + 1.1.1 + + + org.osgi + org.osgi.service.component + + + org.osgi + org.osgi.service.feature + 1.0.0 + + + org.osgi + org.osgi.service.log + 1.5.0 + + + org.osgi + org.osgi.util.tracker + + + + org.osgi + org.osgi.service.jakartars + 2.0.0 + + + + org.osgi + org.osgi.service.jaxrs + 1.0.0 + + + 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.osgi + org.osgi.framework + + + + org.osgi + org.osgi.service.component.annotations + 1.5.1 + + + + + + + + biz.aQute.bnd + bnd-maven-plugin + + + + + \ No newline at end of file diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/AbstractActivator.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/AbstractActivator.java new file mode 100644 index 0000000..04f2f7c --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/AbstractActivator.java @@ -0,0 +1,121 @@ +/** + * 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.util; + +import java.io.Closeable; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Descriptor; +import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; + +@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 abstract class AbstractActivator implements BundleActivator { + + protected final Set closeables = new HashSet<>(); + protected BundleContext context; + protected DTOFormatter formatter = new DTOFormatter(); + + @Override + public void start(BundleContext context) throws Exception { + this.context = context; + init(); + } + + protected abstract void init(); + + protected void registerConverterService(Converter converterService) { + registerConverterService(converterService, null); + + } + + protected void registerConverterService(Converter converterService, Map properties) { + var minimalProperties = new Hashtable(); + minimalProperties.put(Constants.SERVICE_RANKING, 10000); + + if (properties != null) { + minimalProperties.putAll(properties); + } + + ServiceRegistration registration = context.registerService(Converter.class, converterService, + minimalProperties); + closeables.add(() -> { + registration.unregister(); + }); + } + + protected void registerCommandService(Object service, String scope) throws Exception { + registerCommandService(service, scope, null); + } + + protected void registerCommandService(Object service, String scope, Map properties) { + try { + + var minimalProperties = new Hashtable(); + minimalProperties.put(CommandProcessor.COMMAND_SCOPE, scope); + if (properties != null) { + minimalProperties.putAll(properties); + } + + Set commands = new TreeSet<>(); + for (Method m : service.getClass().getMethods()) { + var d = m.getAnnotation(Descriptor.class); + + if (d != null) { + commands.add(m.getName().toLowerCase()); + } + } + + var functions = commands.stream().map(name -> name.startsWith("_") ? name.substring(1) : name) + .toArray(String[]::new); + + minimalProperties.put(CommandProcessor.COMMAND_FUNCTION, functions); + + ServiceRegistration registration = context.registerService(Object.class, service, + minimalProperties); + + closeables.add(() -> { + registration.unregister(); + if (service instanceof Closeable) { + ((Closeable) service).close(); + } + }); + } catch (Throwable e) { + // ignore + } + } + + @Override + public void stop(BundleContext context) throws Exception { + closeables.forEach(c -> { + try { + c.close(); + } catch (Exception e) { + // ignore + } + }); + } + +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/BaseDTOFormatterConverter.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/BaseDTOFormatterConverter.java new file mode 100644 index 0000000..c0fee59 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/BaseDTOFormatterConverter.java @@ -0,0 +1,71 @@ +/** + * 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.util; + +import java.io.File; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Objects; +import java.util.function.Function; + +import org.apache.felix.gogo.runtime.Closure; +import org.apache.felix.service.command.Converter; +import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter; + +public abstract class BaseDTOFormatterConverter implements Converter { + + private final DTOFormatter formatter; + + protected BaseDTOFormatterConverter(DTOFormatter formatter) { + this.formatter = formatter; + } + + @Override + public CharSequence format(Object from, int level, Converter backup) throws Exception { + try { + if (from instanceof Enumeration) { + from = Collections.list((Enumeration) from); + } + if (from instanceof File file) { + return file.getPath(); + } + if (from instanceof Function) { + if (from.getClass() == Closure.class) { + return " { " + from.toString() + " } "; + } + return "a Function"; + } + var formatted = formatter.format(from, level, (o, l, f) -> { + try { + return backup.format(o, l, null); + } catch (Exception e) { + return Objects.toString(o); + } + }); + + if (formatted != null) { + return formatted; + } + return formatted; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Override + public Object convert(Class desiredType, Object in) throws Exception { + return null; + } +} \ No newline at end of file diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/DisplayUtil.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/DisplayUtil.java new file mode 100644 index 0000000..0d5ab97 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/DisplayUtil.java @@ -0,0 +1,95 @@ +/** + * 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.util; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class DisplayUtil { + static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("kk:ss.S").withZone(ZoneId.of("UTC")); + + public static String objectClass(Map map) { + var object = (String[]) map.get(Constants.OBJECTCLASS); + + return objectClass(object); + } + + public static String objectClass(String[] object) { + return Stream.of(object).map(DisplayUtil::shorten).collect(Collectors.joining("\n")); + } + + public static String shorten(String className) { + var split = className.split("\\."); + var sb = new StringBuilder(); + sb.append(split[split.length - 1]); + return sb.toString(); + + } + + public static String dateTime(long time) { + if (time == 0) { + return "0"; + } else { + return Instant.ofEpochMilli(time).toString(); + } + } + + public static String lastModified(long time) { + if (time == 0) { + return "?"; + } + + var now = Instant.now(); + var modified = Instant.ofEpochMilli(time); + var d = Duration.between(modified, now); + var millis = d.toMillis(); + if (millis < 300_000L) { + return (millis + 500L) / 1000L + " secs ago"; + } + if (millis < 60L * 300_000L) { + return (millis + 500L) / 60_000L + " mins ago"; + } + if (millis < 60L * 60L * 300_000L) { + return (millis + 500L) / (60L * 60_000L) + " hrs ago"; + } + if (millis < 24L * 60L * 60L * 300_000L) { + return (millis + 500L) / (24L * 60L * 60_000L) + " days ago"; + } + return dateTime(time); + } + + public static Map toMap(ServiceReference ref) { + Map map = new HashMap<>(); + for (String key : ref.getPropertyKeys()) { + map.put(key, ref.getProperty(key)); + } + return map; + } + + public static String toTime(long time) { + var timeInstant = Instant.ofEpochMilli(time); + return dtf.format(timeInstant); + } + +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/GlobFilter.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/GlobFilter.java new file mode 100644 index 0000000..5b56ec0 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/GlobFilter.java @@ -0,0 +1,171 @@ +/** + * 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.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GlobFilter { + + enum State { + SIMPLE, CURLIES, BRACKETS, QUOTED + } + + public static final GlobFilter ALL = new GlobFilter("*"); + + private final Pattern pattern; + + public GlobFilter(String globString) { + this(globString, 0); + } + + public GlobFilter(String globString, int flags) { + this(globString, toPattern(globString, flags)); + } + + protected GlobFilter(String globString, Pattern pattern) { + this.pattern = pattern; + } + + public Pattern pattern() { + return pattern; + } + + public static Pattern toPattern(String line) { + return toPattern(line, 0); + } + + public static Pattern toPattern(String line, int flags) { + line = line.trim(); + var strLen = line.length(); + var sb = new StringBuilder(strLen << 2); + var curlyLevel = 0; + var state = State.SIMPLE; + + char previousChar = 0; + for (var i = 0; i < strLen; i++) { + var currentChar = line.charAt(i); + switch (currentChar) { + case '*': + if ((state == State.SIMPLE || state == State.CURLIES) && !isEnd(previousChar)) { + sb.append('.'); + } + sb.append(currentChar); + break; + case '?': + if ((state == State.SIMPLE || state == State.CURLIES) && !isStart(previousChar) + && !isEnd(previousChar)) { + sb.append('.'); + } else { + sb.append(currentChar); + } + break; + case '+': + if ((state == State.SIMPLE || state == State.CURLIES) && !isEnd(previousChar)) { + sb.append('\\'); + } + sb.append(currentChar); + break; + case '\\': + sb.append(currentChar); + if (i + 1 < strLen) { + var nextChar = line.charAt(++i); + if (state == State.SIMPLE && nextChar == 'Q') { + state = State.QUOTED; + } else if (state == State.QUOTED && nextChar == 'E') { + state = State.SIMPLE; + } + sb.append(nextChar); + } + break; + case '[': + if (state == State.SIMPLE) { + state = State.BRACKETS; + } + sb.append(currentChar); + break; + case ']': + if (state == State.BRACKETS) { + state = State.SIMPLE; + } + sb.append(currentChar); + break; + + case '{': + if ((state == State.SIMPLE || state == State.CURLIES) && !isEnd(previousChar)) { + state = State.CURLIES; + sb.append("(?:"); + curlyLevel++; + } else { + sb.append(currentChar); + } + break; + case '}': + if (state == State.CURLIES && curlyLevel > 0) { + sb.append(')'); + currentChar = ')'; + curlyLevel--; + if (curlyLevel == 0) { + state = State.SIMPLE; + } + } else { + sb.append(currentChar); + } + break; + case ',': + if (state == State.CURLIES) { + sb.append('|'); + } else { + sb.append(currentChar); + } + break; + case '^': + case '.': + case '$': + case '@': + case '%': + if (state == State.SIMPLE || state == State.CURLIES) { + sb.append('\\'); + } + + // FALL THROUGH + default: + sb.append(currentChar); + break; + } + previousChar = currentChar; + } + return Pattern.compile(sb.toString(), flags); + } + + private static boolean isStart(char c) { + return c == '('; + } + + private static boolean isEnd(char c) { + return c == ')' || c == ']'; + } + + public Matcher createMatcher(CharSequence input) { + return pattern.matcher(input); + } + + public boolean matches(String s) { + return matches((CharSequence) s); + } + + public boolean matches(CharSequence s) { + return createMatcher(s).matches(); + } +} \ No newline at end of file diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Canvas.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Canvas.java new file mode 100644 index 0000000..945d1fa --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Canvas.java @@ -0,0 +1,426 @@ +/** + * 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.util.dtoformatter; + +import java.util.Arrays; + +public class Canvas { + // n + // | + // w - + - e + // | + // s + + final static char PRIVATE = 0xE000; + final static int north = PRIVATE + 0b0000_0001; + final static int bnorth = PRIVATE + 0b0000_0011; + + final static int east = PRIVATE + 0b0000_0100; + final static int beast = PRIVATE + 0b0000_1100; + + final static int south = PRIVATE + 0b0001_0000; + final static int bsouth = PRIVATE + 0b0011_0000; + + final static int west = PRIVATE + 0b0100_0000; + final static int bwest = PRIVATE + 0b1100_0000; + + static class Style { + + final int east; + final int west; + final int south; + final int north; + + Style(int east, int south, int west, int north) { + this.east = east; + this.west = west; + this.south = south; + this.north = north; + } + } + + final static Style PLAIN = new Style(east, south, west, north); + final static Style BOLD = new Style(beast, bsouth, bwest, bnorth); + + final static boolean[] extraline = new boolean[256]; + final static char[] boxchars = new char[256]; + static { + box('─', west | east); + box('━', bwest | beast); + box('│', north | south); + box('┃', bnorth | bsouth); + box('┌', east | south); + xbox('┍', beast | south); + xbox('┎', east | bsouth); + xbox('┏', beast | bsouth); + box('┐', west | south); + box('┑', bwest | south); + box('┒', west | bsouth); + box('┓', bwest | bsouth); + xbox('└', north | east); + xbox('┕', north | beast); + xbox('┖', bnorth | east); + xbox('┗', bnorth | beast); + box('┘', west | north); + box('┙', bwest | north); + box('┚', west | bnorth); + box('┛', bwest | bnorth); + xbox('├', north | south | east); + xbox('┝', north | south | beast); + xbox('┞', bnorth | south | east); + xbox('┟', north | bsouth | east); + xbox('┠', bnorth | bsouth | east); + xbox('┡', bnorth | south | beast); + xbox('┢', north | bsouth | beast); + xbox('┣', bnorth | bsouth | beast); + box('┤', west | north | south); + box('┥', bwest | north | south); + box('┦', west | bnorth | south); + box('┧', west | north | bsouth); + box('┨', west | bnorth | bsouth); + box('┩', bwest | bnorth | south); + box('┪', bwest | north | bsouth); + box('┫', bwest | bnorth | bsouth); + box('┬', west | south | east); + box('┭', bwest | south | east); + box('┮', west | south | beast); + box('┯', bwest | beast | south); + box('┰', west | bsouth | east); + box('┱', bwest | bsouth | east); + box('┲', west | bsouth | beast); + box('┳', bwest | bsouth | beast); + box('┴', west | north | east); + box('┵', bwest | north | east); + box('┶', west | north | beast); + box('┷', bwest | north | beast); + box('┸', west | bnorth | east); + box('┹', bwest | bnorth | east); + box('┺', west | bnorth | beast); + box('┻', bwest | bnorth | beast); + box('┼', north | west | south | east); + box('┽', north | bwest | south | east); + box('┾', north | west | south | beast); + box('┿', north | bwest | south | beast); + box('╀', bnorth | west | south | east); + box('╁', north | west | bsouth | east); + box('╂', bnorth | west | bsouth | east); + box('╃', bnorth | bwest | south | east); + box('╄', bnorth | west | south | beast); + box('╅', north | bwest | bsouth | east); + box('╆', north | west | bsouth | beast); + box('╇', bnorth | bwest | south | beast); + box('╈', north | bwest | bsouth | beast); + box('╉', bnorth | bwest | bsouth | east); + box('╊', bnorth | west | bsouth | beast); + box('╋', bnorth | bwest | bsouth | beast); + box('╴', west); + box('╵', north); + box('╶', east); + box('╷', south); + box('╸', bwest); + box('╹', bnorth); + box('╺', beast); + box('╻', bsouth); + box('╼', west | beast); + box('╽', north | bsouth); + box('╾', bwest | east); + box('╿', bnorth | south); + } + + final static int none = 0; + + final char[] buffer; + final int width; + + public Canvas(int width, int height) { + this.buffer = new char[width * height]; + this.width = width; + clear(); + } + + private static void box(char c, int i) { + i = i - PRIVATE; + assert boxchars[i] == 0 : "Double usage " + c + " " + i; + assert c != 0; + boxchars[i] = c; + } + + private static void xbox(char c, int i) { + box(c, i); + i = i - PRIVATE; + extraline[i] = true; + } + + public Canvas clear() { + return clear(' '); + } + + public Canvas clear(char c) { + Arrays.fill(buffer, c); + return this; + } + + /** + * Remove boxes + */ + public Canvas removeBoxes() { + var w = width(); + var h = height(); + + var columns = new boolean[w]; + var rows = new boolean[h]; + + for (var y = 0; y < height(); y++) { + for (var x = 0; x < width(); x++) { + var c = get(x, y); + + if (isPrivate(c) || c == ' ') { + if (x == 0 || x == width() - 1) { + + } else { + columns[x] = true; + } + } else { + columns[x] = true; + rows[y] = true; + } + + } + } + for (boolean column : columns) { + if (!column) { + w--; + } + } + for (boolean row : rows) { + if (!row) { + h--; + } + } + + var result = new Canvas(w, h); + + var oy = 0; + for (var y = 0; y < height(); y++) { + if (rows[y]) { + var ox = 0; + for (var x = 0; x < width(); x++) { + if (columns[x]) { + var c = get(x, y); + if (isPrivate(c)) { + c = ' '; + } + result.set(ox, oy, c); + ox++; + } + } + oy++; + } + } + return result; + } + + public Canvas box(int x, int y, int w, int h) { + return box(x, y, w, h, PLAIN); + } + + public Canvas box(int x, int y, int w, int h, Style style) { + if (w == 0 || h == 0) { + return this; + } + bounds(x, y); + w--; + h--; + bounds(x + w, y + h); + line(x + 1, y, x + w - 1, y, style.east | style.west); // top horizontal + line(x + w, y + 1, x + w, y + h - 1, style.north | style.south); // right + // vertical + line(x + 1, y + h, x + w - 1, y + h, style.east | style.west); // left + // bottom + line(x, y + 1, x, y + h - 1, style.north | style.south); // left + // vertical + merge(x, y, style.east | style.south); + merge(x + w, y, style.west | style.south); + merge(x, y + h, style.east | style.north); + merge(x + w, y + h, style.north | style.west); + return this; + } + + public Canvas line(int x1, int y1, int x2, int y2, int what) { + return line(x1, y1, x2, y2, (char) what); + } + + public Canvas line(int x1, int y1, int x2, int y2, char what) { + var d = 0; + + var dx = Math.abs(x2 - x1); + var dy = Math.abs(y2 - y1); + + var dx2 = 2 * dx; // slope scaling factors to + var dy2 = 2 * dy; // avoid floating point + + var ix = x1 < x2 ? 1 : -1; // increment direction + var iy = y1 < y2 ? 1 : -1; + + var x = x1; + var y = y1; + + if (dx >= dy) { + while (true) { + merge(x, y, what); + if (x == x2) { + break; + } + x += ix; + d += dy2; + if (d > dx) { + y += iy; + d -= dx2; + } + } + } else { + while (true) { + merge(x, y, what); + if (y == y2) { + break; + } + y += iy; + d += dx2; + if (d > dy) { + x += ix; + d -= dy2; + } + } + } + return this; + } + + public Canvas merge(int x, int y, int what) { + return merge(x, y, (char) what); + } + + public Canvas merge(int x, int y, char what) { + var c = get(x, y); + + if (!isPrivate(what) || !isPrivate(c)) { + set(x, y, what); + } else { + set(x, y, (char) (what | c)); + } + return this; + } + + private boolean isPrivate(char c) { + return c >= PRIVATE && c < PRIVATE + 256; + } + + public char set(int x, int y, char what) { + bounds(x, y); + var index = y * width + x; + var old = buffer[index]; + buffer[index] = what; + return old; + } + + public char get(int x, int y) { + bounds(x, y); + return buffer[y * width + x]; + } + + private void bounds(int x, int y) { + if (x < 0) { + throw new IllegalArgumentException(" x < 0 " + x); + } + if (y < 0) { + throw new IllegalArgumentException(" y < 0 " + y); + } + if (x >= width) { + throw new IllegalArgumentException("x " + x + " is too high, max " + width); + } + if (y >= height()) { + throw new IllegalArgumentException("y " + y + " is too high, max " + height()); + } + } + + public int width() { + return width; + } + + public int height() { + if (width == 0) { + return buffer.length; + } + return buffer.length / width; + } + + @Override + public String toString() { + var sb = new StringBuilder(); + toString(sb); + return sb.toString(); + } + + public void toString(StringBuilder sb) { + for (var y = 0; y < height(); y++) { + var t = get(0, y); + + for (var x = 0; x < width; x++) { + var c = get(x, y); + if (c == 0) { + System.out.println("null " + x + "," + y); + } + if (isPrivate(c)) { + c = boxchar(c); + } + sb.append(c); + } + sb.append("\n"); + } + } + + char boxchar(char c) { + return boxchars[c - PRIVATE]; + } + + public Canvas box() { + box(0, 0, width(), height(), PLAIN); + return this; + } + + public Canvas box(Style style) { + box(0, 0, width(), height(), style); + return this; + } + + public Canvas merge(Canvas secondary, int dx, int dy) { + for (var y = 0; y < secondary.height() && dy + y < height(); y++) { + for (var x = 0; x < secondary.width() && dx + x < width(); x++) { + if (dy + y >= 0 && dx + x >= 0) { + var c = secondary.get(x, y); + merge(dx + x, dy + y, c); + } + } + } + return this; + } + + public void set(int x, int y, String message) { + for (var i = 0; i < message.length(); i++) { + if (x + i < width() && y < height()) { + set(x + i, y, message.charAt(i)); + } + } + + } +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Cell.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Cell.java new file mode 100644 index 0000000..034e0f4 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Cell.java @@ -0,0 +1,48 @@ +/** + * 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.util.dtoformatter; + +public interface Cell { + + /** + * Width including its borders on left and right + */ + int width(); + + /** + * Height including its borders on top and bottom + */ + int height(); + + /** + * Render including borders + * + * @param w + * @param h + * @return Canvas + */ + default Canvas render(int w, int h) { + var canvas = new Canvas(w, h); + canvas.box(); + var lines = this.toString().split("\r?\n"); + for (var y = 0; y < lines.length && y < h - 2; y++) { + for (var x = 0; x < w - 2 && x < lines[y].length(); x++) { + canvas.set(x + 1, y + 1, lines[y].charAt(x)); + } + } + return canvas; + } + + Object original(); +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/DTODescription.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/DTODescription.java new file mode 100644 index 0000000..c0b2f1b --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/DTODescription.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.util.dtoformatter; + +public class DTODescription { + Class clazz; + String label; + GroupDescription inspect = new GroupDescription(); + GroupDescription line = new GroupDescription(); + GroupDescription part = new GroupDescription(); + +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/DTOFormatter.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/DTOFormatter.java new file mode 100644 index 0000000..06104aa --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/DTOFormatter.java @@ -0,0 +1,713 @@ +/** + * 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.util.dtoformatter; + +import java.beans.Introspector; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.osgi.technology.command.util.GlobFilter; +import org.osgi.dto.DTO; + +public class DTOFormatter implements ObjectFormatter { + + private static final Cell NULL_CELL = Table.EMPTY; + final Map, DTODescription> descriptors = new LinkedHashMap<>(); + public static boolean boxes = true; + + public interface ItemBuilder extends GroupBuilder { + + ItemDescription zitem(); + + default ItemBuilder method(Method readMethod) { + if (readMethod == null) { + System.out.println("?"); + return null; + } + zitem().member = o -> { + try { + return readMethod.invoke(o); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + return this; + } + + default ItemBuilder field(Field field) { + zitem().member = o -> { + try { + return field.get(o); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + return this; + } + + default ItemBuilder minWidth(int w) { + zitem().minWidth = w; + return this; + } + + default ItemBuilder maxWidth(int w) { + zitem().maxWidth = w; + return this; + } + + default ItemBuilder width(int w) { + zitem().maxWidth = w; + zitem().minWidth = w; + return this; + } + + default ItemBuilder label(String label) { + zitem().label = label.toUpperCase(); + return this; + } + + default DTOFormatterBuilder count() { + zitem().format = base -> { + + var o = zitem().member.apply(base); + + if (o instanceof Collection) { + return "" + ((Collection) o).size(); + } else if (o.getClass().isArray()) { + return "" + Array.getLength(o); + } + + return "?"; + }; + return this; + } + + } + + public interface GroupBuilder extends DTOFormatterBuilder { + GroupDescription zgroup(); + + default GroupBuilder title(String title) { + zgroup().title = title; + return this; + } + + default ItemBuilder item(String label) { + var g = zgroup(); + var d = zdto(); + var i = g.items.computeIfAbsent(label, ItemDescription::new); + + ItemBuilder itemBuilder = new ItemBuilder<>() { + + @Override + public GroupDescription zgroup() { + return g; + } + + @Override + public DTODescription zdto() { + return d; + } + + @Override + public ItemDescription zitem() { + return i; + } + + }; + + return itemBuilder; + } + + default ItemBuilder field(String field) { + try { + var b = item(field); + var f = zdto().clazz.getField(field); + + b.zitem().member = o -> { + try { + return f.get(o); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + return b; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + default ItemBuilder optionalField(String field) { + try { + return field(field); + } catch (Exception e) { + var b = item(field); + zgroup().items.remove(field); + return b; + } + } + + default ItemBuilder optionalMethod(String method) { + try { + return method(method); + } catch (Exception e) { + var b = item(method); + zgroup().items.remove(method); + return b; + } + } + + default ItemBuilder method(String method) { + try { + var b = item(method); + var properties = Introspector.getBeanInfo(zdto().clazz).getPropertyDescriptors(); + + Stream.of(properties).filter(property -> property.getName().equals(method)) + .filter(property -> property.getReadMethod() != null).forEach(property -> { + b.method(property.getReadMethod()).label(property.getDisplayName()); + }); + return b; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + default GroupBuilder separator(String separator) { + zgroup().separator = separator; + return this; + } + + default GroupBuilder fields(String string) { + var fields = zdto().clazz.getFields(); + Stream.of(fields).filter(field -> !Modifier.isStatic(field.getModifiers())) + .filter(field -> new GlobFilter(string).matches(field.getName())).forEach(field -> { + item(field.getName()).field(field); + }); + return this; + } + + default GroupBuilder methods(String string) { + try { + var properties = Introspector.getBeanInfo(zdto().clazz).getPropertyDescriptors(); + Stream.of(properties).filter(property -> new GlobFilter(string).matches(property.getName())) + .filter(property -> property.getReadMethod() != null).forEach(property -> { + item(property.getDisplayName()).method(property.getReadMethod()); + }); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + default GroupBuilder as(Function map) { + zgroup().format = (Function) map; + return this; + } + + @SuppressWarnings("unchecked") + default ItemBuilder format(String label, Function format) { + var b = item(label); + var item = b.zitem(); + item.format = (Function) format; + return b; + } + + default GroupBuilder remove(String string) { + zgroup().items.remove(string); + return this; + } + } + + public interface DTOFormatterBuilder { + + DTODescription zdto(); + + default GroupBuilder inspect() { + var d = zdto(); + + return new GroupBuilder<>() { + + @Override + public DTODescription zdto() { + return d; + } + + @Override + public GroupDescription zgroup() { + return zdto().inspect; + } + + }; + } + + default GroupBuilder line() { + var d = zdto(); + + return new GroupBuilder<>() { + + @Override + public DTODescription zdto() { + return d; + } + + @Override + public GroupDescription zgroup() { + return zdto().line; + } + + }; + } + + default GroupBuilder part() { + var d = zdto(); + + return new GroupBuilder<>() { + + @Override + public DTODescription zdto() { + return d; + } + + @Override + public GroupDescription zgroup() { + return zdto().part; + } + + }; + } + } + + public DTOFormatterBuilder build(Class clazz) { + var dto = getDescriptor(clazz, new DTODescription()); + + dto.clazz = clazz; + descriptors.put(clazz, dto); + + return () -> dto; + } + + /*********************************************************************************************/ + + private Cell inspect(Object object, DTODescription descriptor, ObjectFormatter formatter) { + + var table = new Table(descriptor.inspect.items.size(), 2, 0); + var row = 0; + + for (ItemDescription item : descriptor.inspect.items.values()) { + var o = getValue(object, item); + var cell = cell(o, formatter); + table.set(row, 0, item.label); + table.set(row, 1, cell); + row++; + } + return table; + } + + private Object getValue(Object object, ItemDescription item) { + if (item.format != null) { + return item.format.apply(object); + } + + var target = object; + if (!item.self) { + if (item.member == null) { + System.out.println("? item " + item.label); + return "? " + item.label; + } + target = item.member.apply(object); + } + + return target; + } + + @SuppressWarnings("unchecked") + public Cell cell(Object o, ObjectFormatter formatter) { + if (o == null) { + return NULL_CELL; + } + + try { + if (o instanceof Collection) { + return list((Collection) o, formatter); + } else if (o.getClass().isArray()) { + List list = new ArrayList<>(); + for (var i = 0; i < Array.getLength(o); i++) { + list.add(Array.get(o, i)); + } + return list(list, formatter); + } else if (o instanceof Map) { + return map((Map) o, formatter); + } else if (o instanceof Dictionary) { + Map map = new HashMap<>(); + var dictionary = (Dictionary) o; + var e = dictionary.keys(); + while (e.hasMoreElements()) { + var key = e.nextElement(); + var value = dictionary.get(key); + map.put(key, value); + } + return map(map, formatter); + } else { + var descriptor = getDescriptor(o.getClass()); + if (descriptor == null) { + var string = formatter.format(o, ObjectFormatter.PART, formatter); + if (string == null) { + string = Objects.toString(o); + } + + if (string.length() > 25) { + String formatted = string.toString(); + + String[] sarr = formatted.split(","); + if (sarr.length > 5) { + formatted = String.join("," + System.lineSeparator(), sarr); + } + string = formatted; + } + + return new StringCell(string.toString(), o); + } + return part(o, descriptor, formatter); + } + } catch (Exception e) { + e.printStackTrace(); + return new StringCell(e.toString(), e); + } + } + + private Cell map(Map map, ObjectFormatter formatter) { + var table = new Table(map.size(), 2, 0); + var row = 0; + var tm = new TreeMap(Comparator.comparing(Object::toString)); + tm.putAll(map); + for (Map.Entry e : tm.entrySet()) { + + var key = cell(e.getKey(), formatter); + var value = cell(e.getValue(), formatter); + table.set(row, 0, key); + table.set(row, 1, value); + row++; + } + return table; + } + + private Cell list(Collection list, ObjectFormatter formatter) { + + Class type = null; + var maxwidth = 0; + for (Object o : list) { + if (o == null) { + continue; + } + type = commonType(o.getClass(), type); + if (type == String.class) { + maxwidth = Math.max(o.toString().length(), maxwidth); + } + } + + if (type == null) { + return new StringCell("", null); + } + + var descriptor = getDescriptor(type); + if (descriptor == null) { + + if (type == String.class && maxwidth < 100) { + + return new StringCell(list.toArray(new String[0]), list); + } + + if (Number.class.isAssignableFrom(type) || type.isPrimitive() || type == Character.class + || type == Boolean.class) { + + var s = list.stream().map(Object::toString).collect(Collectors.joining(", ")); + return new StringCell(s, list); + } + + var table = new Table(list.size(), 1, 0); + var r = 0; + for (Object o : list) { + var c = cell(o, ObjectFormatter.LINE, formatter); + if (c == null) { + c = new StringCell(formatter.format(o, ObjectFormatter.LINE, null).toString(), o); + } + table.set(r, 0, c); + r++; + } + return table; + } + + Table table; + var group = descriptor.line; + if (group.format != null) { + table = new Table(list.size(), 1, 0); + var r = 0; + for (Object o : list) { + Cell c = new StringCell(group.format.apply(o), 0); + table.set(r, 0, c); + r++; + } + } else { + var cols = group.items.size(); + if (cols == 0) { + // no columns defined + var row = 0; + table = new Table(list.size(), 1, 0); + for (Object member : list) { + table.set(row, 0, member.toString()); + } + } else { + var col = 0; + table = new Table(list.size() + 1, cols, 1); + for (ItemDescription item : group.items.values()) { + table.set(0, col, item.label); + col++; + } + var row = 1; + for (Object member : list) { + col = 0; + for (ItemDescription item : group.items.values()) { + var o = getValue(member, item); + var cell = cell(o, formatter); + table.set(row, col, cell); + col++; + } + row++; + } + } + } + return table; + } + + private Class commonType(Class class1, Class type) { + if ((type == null) || class1.isAssignableFrom(type)) { + return class1; + } + + if (type.isAssignableFrom(class1)) { + return type; + } + + return Object.class; + } + + private Cell line(Object object, DTODescription description, ObjectFormatter formatter) { + var table = new Table(1, description.line.items.size(), 0); + line(object, description, 0, table, formatter); + return table; + } + + private void line(Object object, DTODescription description, int row, Table table, ObjectFormatter formatter) { + var col = 0; + for (ItemDescription item : description.line.items.values()) { + var o = getValue(object, item); + var cell = cell(o, formatter); + table.set(row, col, cell); + col++; + } + } + + private Cell part(Object object, DTODescription descriptor, ObjectFormatter formatter) { + if (descriptor == null) { + var string = formatter.format(object, ObjectFormatter.PART, null).toString(); + return new StringCell(string, object); + } + + var col = 0; + if (descriptor.part.format != null) { + return new StringCell(descriptor.part.format.apply(object), object); + } + var sb = new StringBuilder(); + sb.append(descriptor.part.prefix); + var del = ""; + for (ItemDescription item : descriptor.part.items.values()) { + var o = getValue(object, item); + var cell = cell(o, formatter); + sb.append(del).append(cell); + del = descriptor.part.separator; + } + sb.append(descriptor.part.suffix); + return new StringCell(sb.toString(), object); + } + + @Override + public CharSequence format(Object o, int level, ObjectFormatter formatter) { + while (o instanceof Wrapper) { + o = ((Wrapper) o).whatever; + } + + var c = cell(o, level, formatter); + + return toString(c); + } + + Cell cell(Object o, int level, ObjectFormatter formatter) { + + if (o == null) { + return new StringCell("null", null); + } + + if (isSpecial(o)) { + return cell(o, formatter); + } + + var descriptor = getDescriptor(o.getClass()); + if (descriptor == null) { + if (!(o instanceof DTO)) { + return null; + } + try { + return switch (level) { + case ObjectFormatter.INSPECT -> inspect(o, formatter); + case ObjectFormatter.LINE -> line(o, formatter); + case ObjectFormatter.PART -> part(o, formatter); + default -> null; + }; + } catch (Exception e) { + // TODO LOG + return null; + } + } + + return switch (level) { + case ObjectFormatter.INSPECT -> inspect(o, descriptor, formatter); + case ObjectFormatter.LINE -> line(o, descriptor, formatter); + case ObjectFormatter.PART -> part(o, descriptor, formatter); + default -> null; + }; + } + + final static String[] IDNAMES = { "id", "key", "name", "title" }; + + private Cell part(Object o, ObjectFormatter formatter) throws Exception { + Field primary = null; + var priority = IDNAMES.length; + + for (Field f : o.getClass().getFields()) { + if (isDTOField(f, o)) { + continue; + } + + for (var i = 0; i < priority; i++) { + if (IDNAMES[i].equalsIgnoreCase(f.getName())) { + priority = i; + primary = f; + } + } + } + if (primary != null) { + var v = primary.get(o); + return cell(v, PART, formatter); + } + return null; + } + + private Cell line(Object o, ObjectFormatter formatter) throws IllegalAccessException { + var form = getCells(o, formatter, ObjectFormatter.PART); + var table = new Table(1, form.size(), 0); + var c = 0; + for (Entry e : form.entrySet()) { + table.set(0, c++, e.getValue()); + } + + return table; + } + + private Cell inspect(Object o, ObjectFormatter formatter) throws Exception { + var form = getCells(o, formatter, ObjectFormatter.LINE); + + var table = new Table(form.size(), 2, 0); + var r = 0; + for (Entry e : form.entrySet()) { + table.set(r, 0, new StringCell(e.getKey(), e.getKey())); + table.set(r++, 1, e.getValue()); + } + + return table; + } + + private Map getCells(Object o, ObjectFormatter formatter, int level) throws IllegalAccessException { + Map form = new TreeMap<>(); + for (Field f : o.getClass().getFields()) { + if (!isDTOField(f, o)) { + continue; + } + + var value = f.get(o); + var cell = cell(value, level, formatter); + if (cell == null) { + cell = new StringCell(Objects.toString(value), value); + } + form.put(f.getName(), cell); + } + return form; + } + + private boolean isDTOField(Field f, Object o) { + return Modifier.isStatic(f.getModifiers()) || f.isSynthetic() || !f.canAccess(o) || f.isEnumConstant(); + } + + private CharSequence toString(Cell cell) { + if (cell == null) { + return null; + } + + var render = cell.render(cell.width(), cell.height()); + if (!boxes) { + render = render.removeBoxes(); + } + return render.toString(); + } + + private boolean isSpecial(Object o) { + return o instanceof Map || o instanceof Dictionary || o instanceof Collection || o.getClass().isArray(); + } + + private DTODescription getDescriptor(Class clazz, DTODescription defaultDescriptor) { + var descriptor = getDescriptor(clazz); + if (descriptor != null) { + return descriptor; + } + return defaultDescriptor; + } + + private DTODescription getDescriptor(Class clazz) { + var description = descriptors.get(clazz); + if (description != null) { + return description; + } + + description = descriptors.entrySet().stream().filter(e -> e.getKey().isAssignableFrom(clazz)) + .map(Map.Entry::getValue).findAny().orElse(null); + + return description; + } +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/GroupDescription.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/GroupDescription.java new file mode 100644 index 0000000..8c3b48e --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/GroupDescription.java @@ -0,0 +1,27 @@ +/** + * 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.util.dtoformatter; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +public class GroupDescription { + String title; + final Map items = new LinkedHashMap<>(); + String separator = ","; + String prefix = "["; + String suffix = "]"; + public Function format; +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/ItemDescription.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/ItemDescription.java new file mode 100644 index 0000000..50c0972 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/ItemDescription.java @@ -0,0 +1,29 @@ +/** + * 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.util.dtoformatter; + +import java.util.function.Function; + +public class ItemDescription { + ItemDescription(String name) { + this.label = name; + } + + Function member; + String label; + int maxWidth = Integer.MAX_VALUE; + int minWidth = 0; + boolean self; + Function format; +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Justif.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Justif.java new file mode 100644 index 0000000..9ab56da --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Justif.java @@ -0,0 +1,253 @@ +/** + * 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.util.dtoformatter; + +import java.util.ArrayList; +import java.util.Formatter; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** + * Formatter. This formatter allows you to build up an input string and then + * wraps the text. The following markup is available + *
    + *
  • $- - Line over the remaining width + *
  • \\t[0-9] - Go to tab position, and set indent to that position + *
  • \\f - Newlin + *
+ */ +public class Justif { + final int[] tabs; + final int width; + StringBuilder sb = new StringBuilder(); + Formatter f = new Formatter(sb); + + public Justif(int width, int... tabs) { + this.tabs = tabs == null || tabs.length == 0 ? new int[] { 30, 40, 50, 60, 70 } : tabs; + this.width = width == 0 ? 73 : width; + } + + public Justif() { + this(0); + } + + /** + * Routine to wrap a stringbuffer. Basically adds line endings but has the + * following control characters: + *
    + *
  • Space at the beginnng of a line is repeated when wrapped for indent.
  • + *
  • A tab will mark the current position and wrapping will return to that + * position
  • + *
  • A form feed in a tabbed colum will break but stay in the column
  • + *
+ * + * @param sb + */ + public void wrap(StringBuilder sb) { + List indents = new ArrayList<>(); + + var indent = 0; + var linelength = 0; + var lastSpace = 0; + var r = 0; + var begin = true; + + while (r < sb.length()) { + switch (sb.charAt(r++)) { + case '\r': + indents.clear(); + sb.setCharAt(r - 1, '\n'); + // FALL THROUGH + + case '\n': + indent = indents.isEmpty() ? 0 : indents.remove(0); + linelength = 0; + begin = true; + lastSpace = 0; + break; + + case ' ': + if (begin) { + indent++; + } else { + while (r < sb.length() && sb.charAt(r) == ' ') { + sb.delete(r, r + 1); + } + } + lastSpace = r - 1; + linelength++; + break; + + case '\t': + sb.deleteCharAt(--r); + indents.add(indent); + if (r < sb.length()) { + var digit = sb.charAt(r); + if (Character.isDigit(digit)) { + sb.deleteCharAt(r); + + var column = (digit - '0'); + if (column < tabs.length) { + indent = tabs[column]; + } else { + indent = column * 8; + } + + var diff = indent - linelength; + if (diff > 0) { + for (var i = 0; i < diff; i++) { + sb.insert(r, ' '); + } + r += diff; + linelength += diff; + } + } else { + System.err.println("missing digit after \t"); + } + } + break; + + case '\f': + sb.setCharAt(r - 1, '\n'); + for (var i = 0; i < indent; i++) { + sb.insert(r, ' '); + } + r += indent; + while (r < sb.length() && sb.charAt(r) == ' ') { + sb.delete(r, r + 1); + } + linelength = 0; + lastSpace = 0; + break; + + case '$': + if (sb.length() > r) { + var c = sb.charAt(r); + if (c == '-' || c == '_' || c == '\u2014') { + sb.delete(r - 1, r); // remove $ + begin = false; + linelength++; + while (linelength < width - 1) { + sb.insert(r++, c); + linelength++; + } + break; + } + } + + case '\u00A0': // non breaking space + sb.setCharAt(r - 1, ' '); // Turn it into a space + + // fall through + + default: + linelength++; + begin = false; + if (linelength > width) { + if (lastSpace == 0) { + lastSpace = r - 1; + sb.insert(lastSpace, ' '); + r++; + } + sb.setCharAt(lastSpace, '\n'); + linelength = r - lastSpace - 1; + + for (var i = 0; i < indent; i++) { + sb.insert(lastSpace + 1, ' '); + linelength++; + } + r += indent; + lastSpace = 0; + } + } + } + } + + public String wrap() { + wrap(sb); + return sb.toString(); + } + + public Formatter formatter() { + return f; + } + + @Override + public String toString() { + wrap(sb); + return sb.toString(); + } + + public void indent(int indent, String string) { + for (var i = 0; i < string.length(); i++) { + var c = string.charAt(i); + if (i == 0) { + for (var j = 0; j < indent; j++) { + sb.append(' '); + } + } else { + sb.append(c); + if (c == '\n') { + for (var j = 0; j < indent; j++) { + sb.append(' '); + } + } + } + } + } + + // TODO not working yet + + public void entry(String key, String separator, Object value) { + sb.append(key); + sb.append("\t1"); + sb.append(separator); + sb.append("\t2"); + if (value instanceof Iterable iterable) { + Iterator it = iterable.iterator(); + var hadone = false; + var del = ""; + while (it.hasNext()) { + sb.append(del).append(it.next() + ""); + sb.append("\r"); + hadone = true; + del = "\t2"; + } + if (!hadone) { + sb.append("\r"); + } + } else { + sb.append(value + ""); + sb.append("\r"); + } + } + + public void table(Map table, String separator) { + TreeMap map = new TreeMap<>(table); + for (Entry e : map.entrySet()) { + entry(e.getKey().toString(), separator, e.getValue()); + } + } + + public String toString(Object o) { + var s = "" + o; + if (s.length() > 50) { + return s.replace(",", ", \f"); + } + return s; + } +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/ObjectFormatter.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/ObjectFormatter.java new file mode 100644 index 0000000..cd22c2a --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/ObjectFormatter.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.util.dtoformatter; + +public interface ObjectFormatter { + int INSPECT = 0; + int LINE = 1; + int PART = 2; + + CharSequence format(Object o, int level, ObjectFormatter formatter); +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/StringCell.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/StringCell.java new file mode 100644 index 0000000..ab2fdbd --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/StringCell.java @@ -0,0 +1,67 @@ +/** + * 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.util.dtoformatter; + +public class StringCell implements Cell { + static Justif j = new Justif(80); + + public final String[] value; + public final int width; + final Object original; + + public StringCell(String label, Object original) { + this.original = original; + this.value = label.split("\\s*\r?\n"); + var w = 0; + for (String l : value) { + if (l.length() > w) { + w = l.length(); + } + } + this.width = w; + } + + public StringCell(String[] array, Object original) { + this.value = array; + this.original = original; + var w = 0; + for (String l : value) { + if (l.length() > w) { + w = l.length(); + } + } + this.width = w; + } + + @Override + public int width() { + return width + 2; + } + + @Override + public int height() { + return value.length + 2; + } + + @Override + public String toString() { + return String.join("\n", value); + } + + @Override + public Object original() { + return original; + } + +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Table.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Table.java new file mode 100644 index 0000000..fc77e60 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Table.java @@ -0,0 +1,427 @@ +/** + * 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.util.dtoformatter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A table consists of rows and columns of {@link Cell} objects. + */ +public class Table implements Cell { + + public final int rows; + public final int cols; + public final Cell[][] cells; + final public int headers; + Canvas.Style style = Canvas.PLAIN; + Object original; + + public static final Cell EMPTY = new Cell() { + + @Override + public int width() { + return 3; + } + + @Override + public int height() { + return 3; + } + + @Override + public String toString() { + return " "; + } + + @Override + public Object original() { + return null; + } + }; + + public Table(int rows, int cols, int headers) { + this.rows = rows; + this.cols = cols; + this.headers = headers; + cells = new Cell[rows][cols]; + for (var r = 0; r < rows; r++) { + for (var c = 0; c < cols; c++) { + cells[r][c] = EMPTY; + } + } + } + + public Table(List> matrix, int headers) { + this(matrix.size(), maxWidth(matrix), headers); + var r = headers; + + for (List row : matrix) { + var c = 0; + for (String col : row) { + this.set(r, c, col); + c++; + } + r++; + } + } + + private static int maxWidth(List> matrix) { + var maxWidth = 0; + for (List row : matrix) { + maxWidth = Math.max(maxWidth, row.size()); + } + return maxWidth; + } + + /** + * Width including borders + */ + @Override + public int width() { + if (rows == 0 || cols == 0) { + return 2; + } + + var w = 0; + for (var c = 0; c < cols; c++) { + w += width(c) - 1; // remove right border width because we overlap + } + return Math.max(w, 0) + 1; // add right border + } + + /** + * Height including borders + */ + @Override + public int height() { + if (rows == 0 || cols == 0) { + return 2; + } + + var h = 0; + for (var r = 0; r < rows; r++) { + var height = height(r); + if (height > 0) { + h += height - 1;// remove bottom border width because we overlap + } + } + return Math.max(h, 0) + 1; // add right border + } + + public void set(int r, int c, Object label) { + cells[r][c] = new StringCell("" + label, label); + } + + public void set(int r, int c, Cell table) { + if (table == null || table.width() == 0 || table.height() == 0) { + table = Table.EMPTY; + } + + cells[r][c] = table; + } + + public Cell[] row(int row) { + return cells[row]; + } + + @Override + public String toString() { + return toString(null); + } + + @Override + public Canvas render(int width, int height) { + return render(width, height, 0, 0, 0, 0); + } + + public Canvas render() { + return render(width(), height(), 0, 0, 0, 0); + } + + public Canvas render(int width, int height, int left, int top, int right, int bottom) { + + var canvas = new Canvas(width + left + right, height + top + bottom); + canvas.box(left, top, width, height, style); + + var y = top; + + for (var r = 0; r < rows; r++) { + var ch = height(r); + var x = left; + for (var c = 0; c < cols; c++) { + int cw; + if (c == cols - 1) { + // adjust last column width + cw = width - x - left; + } else { + cw = width(c); + } + var cell = cells[r][c]; + var foo = cell.render(cw, ch); + canvas.merge(foo, x, y); + x += cw - 1; + } + y += ch - 1; + } + return canvas; + } + + /** + * Width of a column without borders + * + * @param col + * @return + */ + private int width(int col) { + var w = 0; + for (var r = 0; r < rows; r++) { + var cell = cells[r][col]; + var width = cell.width(); + if (width > w) { + w = width; + } + } + return w; + } + + public int height(int row) { + var h = 0; + for (var c = 0; c < cols; c++) { + var cell = cells[row][c]; + var height = cell.height(); + if (height > h) { + h = height; + } + } + return h; + } + + public Table transpose(int headers) { + var transposed = new Table(cols, rows, headers); + for (var row = 0; row < rows; row++) { + for (var col = 0; col < cols; col++) { + var c = this.get(row, col); + transposed.set(col, row, c); + } + } + return transposed; + } + + public Cell get(int row, int col) { + return cells[row][col]; + } + + public String toString(String message) { + if (message == null) { + message = ""; + } + + if (rows == 0 || cols == 0) { + return "☒" + message; + } + var render = render(width(), height(), 0, 0, message.length(), 0); + render.set(width(), 0, message); + return render.toString(); + } + + public Table addColum(int col) { + var t = new Table(rows, cols + 1, headers); + if (col > 0) { + copyTo(t, 0, 0, 0, 0, rows, col); + } + + return copyTo(t, 0, col, 0, col + 1, rows, cols - col); + } + + public Table setColumn(int col, Object cell) { + for (var r = 0; r < rows; r++) { + set(r, col, cell); + } + + return this; + } + + public Table copyTo(Table dest, int sourceRow, int sourceCol, int destRow, int destCol, int rows, int cols) { + for (var i = 0; i < rows; i++) { + for (var j = 0; j < cols; j++) { + var cell = get(sourceRow + i, sourceCol + j); + dest.set(destRow + i, destCol + j, cell); + } + } + return dest; + } + + public void copyColumn(Table src, int from, int to) { + copyTo(src, 0, from, 0, to, rows, 1); + } + + public void setBold() { + style = Canvas.BOLD; + } + + public Table select(List columns) { + var dst = new Table(rows, columns.size(), headers); + for (var toColumn = 0; toColumn < columns.size(); toColumn++) { + var srcColumn = findHeader(columns.get(toColumn)); + if (srcColumn >= 0) { + this.copyColumn(dst, srcColumn, toColumn); + } else { + throw new IllegalArgumentException("No such column " + columns.get(toColumn)); + } + } + return dst; + } + + /** + * Select matching rows. Each row is translated to a map and then run against + * the given predicate. If the predicate matches the row is included in the + * output. + * + * @param predicate + * @return a new table with only the matching rows + */ + public Table select(Predicate> predicate) { + String colNames[] = new String[cols]; + + if (headers == 0) { + for (var i = 0; i < colNames.length; i++) { + colNames[i] = "" + i; + } + } else { + for (var i = 0; i < colNames.length; i++) { + colNames[i] = get(0, i).toString(); + } + } + + List selectedRows = new ArrayList<>(); + + Map map = new HashMap<>(); + for (var r = headers; r < rows; r++) { + for (var c = 0; c < cols; c++) { + var cell = get(r, c); + Object value; + if (cell instanceof Table) { + value = ((Table) cell).toList(); + } else { + var s = cell.toString(); + try { + value = Long.parseLong(s); + } catch (Exception e) { + try { + value = Double.parseDouble(s); + } catch (Exception ee) { + value = s; + } + } + } + map.put(colNames[c], value); + } + if (predicate.test(map)) { + selectedRows.add(r); + } + } + + var result = new Table(selectedRows.size() + headers, cols, headers); + + copyTo(result, 0, 0, 0, 0, headers, cols); + + for (var r = 0; r < selectedRows.size(); r++) { + copyTo(result, selectedRows.get(r), 0, r + result.headers, 0, 1, cols); + } + return result; + } + + public List toList() { + List list = new ArrayList<>(); + for (var r = headers; r < rows; r++) { + list.add(toString(r)); + } + return list; + } + + public String toString(int row) { + return Stream.of(cells[row]).map(Object::toString).collect(Collectors.joining(",")); + } + + public int findHeader(String name) { + for (var h = 0; h < headers; h++) { + for (var c = 0; c < cols; c++) { + if (Integer.toString(c).equals(name)) { + return c; + } + + var header = get(h, c).toString(); + if (header.matches(name)) { + return c; + } + } + } + return -1; + } + + public void sort(String sort, boolean reverse) { + var col = findHeader(sort); + if (col < 0) { + return; + } + Comparator cmp = (var a, var b) -> { + + var aa = a[col].original(); + var bb = b[col].original(); + if (aa == bb) { + return 0; + } + + if (aa == null) { + return -1; + } + if (bb == null) { + return 1; + } + + var aaa = aa.toString(); + var bbb = bb.toString(); + + try { + var la = Long.parseLong(aaa); + var lb = Long.parseLong(bbb); + return Long.compare(la, lb); + } catch (Exception e) { + try { + double la = Long.parseLong(aaa); + double lb = Long.parseLong(bbb); + return Double.compare(la, lb); + } catch (Exception ee) { + return aaa.compareTo(bbb); + } + } + }; + + Arrays.sort(cells, headers, rows, reverse ? cmp.reversed() : cmp); + } + + @Override + public Object original() { + return original; + } + +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Wrapper.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Wrapper.java new file mode 100644 index 0000000..a6db153 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/Wrapper.java @@ -0,0 +1,28 @@ +/** + * 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.util.dtoformatter; + +public class Wrapper { + public Wrapper(Object o) { + this.whatever = o; + } + + public Object whatever; + + @Override + public String toString() { + return "[" + whatever + "]"; + } + +} diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/package-info.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/package-info.java new file mode 100644 index 0000000..26c0525 --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/dtoformatter/package-info.java @@ -0,0 +1,16 @@ +/** + * 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 + */ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.osgi.technology.command.util.dtoformatter; \ No newline at end of file diff --git a/util/src/main/java/org/eclipse/osgi/technology/command/util/package-info.java b/util/src/main/java/org/eclipse/osgi/technology/command/util/package-info.java new file mode 100644 index 0000000..9bd4f4d --- /dev/null +++ b/util/src/main/java/org/eclipse/osgi/technology/command/util/package-info.java @@ -0,0 +1,16 @@ +/** + * 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 + */ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.osgi.technology.command.util; \ No newline at end of file diff --git a/util/src/test/java/org/eclipse/osgi/technology/command/util/CanvasTest.java b/util/src/test/java/org/eclipse/osgi/technology/command/util/CanvasTest.java new file mode 100644 index 0000000..255181e --- /dev/null +++ b/util/src/test/java/org/eclipse/osgi/technology/command/util/CanvasTest.java @@ -0,0 +1,80 @@ +/** + * 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.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.osgi.technology.command.util.dtoformatter.Canvas; + +public class CanvasTest { + + @org.junit.jupiter.api.Test + public void testCanvas() { + Canvas c = new Canvas(10, 10); + c.box(0, 0, 10, 10); + assertThat("" + "┌────────┐\n" + "│ │\n" + "│ │\n" + "│ │\n" + "│ │\n" + + "│ │\n" + "│ │\n" + "│ │\n" + "│ │\n" + "└────────┘\n" + "") + .isEqualTo(c.toString()); + c.box(2, 2, 6, 6); + assertThat("" + "┌────────┐\n" + "│ │\n" + "│ ┌────┐ │\n" + "│ │ │ │\n" + "│ │ │ │\n" + + "│ │ │ │\n" + "│ │ │ │\n" + "│ └────┘ │\n" + "│ │\n" + "└────────┘\n" + "") + .isEqualTo(c.toString()); + + c.box(0, 0, 6, 6); + assertThat("" + "┌────┬───┐\n" + "│ │ │\n" + "│ ┌──┼─┐ │\n" + "│ │ │ │ │\n" + "│ │ │ │ │\n" + + "├─┼──┘ │ │\n" + "│ │ │ │\n" + "│ └────┘ │\n" + "│ │\n" + "└────────┘\n" + "") + .isEqualTo(c.toString()); + c.box(6, 6, 4, 4); + assertThat("" + "┌────┬───┐\n" + "│ │ │\n" + "│ ┌──┼─┐ │\n" + "│ │ │ │ │\n" + "│ │ │ │ │\n" + + "├─┼──┘ │ │\n" + "│ │ ┌┼─┤\n" + "│ └───┼┘ │\n" + "│ │ │\n" + "└─────┴──┘\n" + "") + .isEqualTo(c.toString()); + } + + @org.junit.jupiter.api.Test + public void testCanvasRemoveBox() { + Canvas c = new Canvas(10, 10); + Canvas box = c.box(0, 0, 10, 10); + Canvas removeBoxes = box.removeBoxes(); + System.out.println(removeBoxes); + } + + @org.junit.jupiter.api.Test + public void testMerge() { + Canvas c1 = new Canvas(10, 10); + c1.box(); + Canvas c2 = new Canvas(5, 5); + c2.box(); + c1.merge(c2, 5, 5); + assertThat("" + "┌────────┐\n" + "│ │\n" + "│ │\n" + "│ │\n" + "│ │\n" + + "│ ┌───┤\n" + "│ │ │\n" + "│ │ │\n" + "│ │ │\n" + "└────┴───┘\n" + "").isEqualTo( + c1.toString()); + c1.merge(c2, 0, 0); + c1.merge(c2, 4, 0); + assertThat("" + "┌───┬───┬┐\n" + "│ │ ││\n" + "│ │ ││\n" + "│ │ ││\n" + "├───┴───┘│\n" + + "│ ┌───┤\n" + "│ │ │\n" + "│ │ │\n" + "│ │ │\n" + "└────┴───┘\n" + "").isEqualTo( + c1.toString()); + + c1.clear().box(); + c1.merge(c2, -3, -2); + assertThat("" + " ┼───────┐\n" + " │ │\n" + "┼┘ │\n" + "│ │\n" + "│ │\n" + + "│ │\n" + "│ │\n" + "│ │\n" + "│ │\n" + "└────────┘\n" + "").isEqualTo( + c1.toString()); + c1.merge(c2, 8, 8); + assertThat("" + " ┼───────┐\n" + " │ │\n" + "┼┘ │\n" + "│ │\n" + "│ │\n" + + "│ │\n" + "│ │\n" + "│ │\n" + "│ ┌┼\n" + "└───────┼ \n" + "").isEqualTo( + c1.toString()); + } + +} \ No newline at end of file diff --git a/util/src/test/java/org/eclipse/osgi/technology/command/util/DTOFormatterTest.java b/util/src/test/java/org/eclipse/osgi/technology/command/util/DTOFormatterTest.java new file mode 100644 index 0000000..7de0a87 --- /dev/null +++ b/util/src/test/java/org/eclipse/osgi/technology/command/util/DTOFormatterTest.java @@ -0,0 +1,163 @@ +/** + * 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.util; + +import java.util.Arrays; +import java.util.HashMap; + +import org.eclipse.osgi.technology.command.util.dtoformatter.DTOFormatter; +import org.eclipse.osgi.technology.command.util.dtoformatter.ObjectFormatter; +import org.osgi.dto.DTO; +import org.osgi.framework.Bundle; +import org.osgi.framework.dto.BundleDTO; +import org.osgi.service.component.annotations.ServiceScope; +import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO; +import org.osgi.service.component.runtime.dto.ReferenceDTO; + +public class DTOFormatterTest { + DTOFormatter formatter = setup(); + ComponentDescriptionDTO dto = createComponentDescriptionDTO(); + + public static class Foo extends DTO { + public String field = "field"; + } + @org.junit.jupiter.api.Test + public void testListUnknown() { + + System.out.println(formatter.format(Arrays.asList(new Foo()), ObjectFormatter.INSPECT, (o, l, f) -> "" + o)); + } + + @org.junit.jupiter.api.Test + public void testComponentDescriptionDTOPart() { + + System.out.println(formatter.format(dto, ObjectFormatter.PART, (o, l, f) -> "" + o)); + } + + @org.junit.jupiter.api.Test + public void testComponentDescriptionDTOLine() { + + System.out.println(formatter.format(dto, ObjectFormatter.LINE, (o, l, f) -> "" + o)); + } + + @org.junit.jupiter.api.Test + public void testComponentDescriptionDTOLines() { + + System.out.println(formatter.format(Arrays.asList(dto), ObjectFormatter.LINE, (o, l, f) -> "" + o)); + } + + public static class RawDTO extends DTO { + public String id = "id"; + public long[] values = new long[] { + 1, 23, 4 + }; + public RawDTO[] children; + } + + @org.junit.jupiter.api.Test + public void testRawDTO() { + RawDTO parent = new RawDTO(); + RawDTO x = new RawDTO(); + x.children = new RawDTO[] { + new RawDTO() + }; + parent.children = new RawDTO[] { + new RawDTO(), new RawDTO(), x + }; + System.out.println(formatter.format(parent, ObjectFormatter.INSPECT, (o, l, f) -> "" + o)); + } + + @org.junit.jupiter.api.Test + public void testComponentDescriptionDTOInspect() { + + System.out.println(formatter.format(dto, ObjectFormatter.INSPECT, (o, l, f) -> "" + o)); + } + + private DTOFormatter setup() { + DTOFormatter formatter = new DTOFormatter(); + formatter.build(ComponentDescriptionDTO.class) + .inspect() + .fields("*") + .line() + .field("bundle") + .field("activate") + .field("modified") + .field("deactivate") + .field("configurationPolicy") + .field("configurationPid") + .field("references") + .count() + .part() + .as(cdd -> String.format("%s[%s]", cdd.name, cdd.bundle.id)); + + formatter.build(ReferenceDTO.class) + .inspect() + .fields("*") + .line() + .fields("*") + .part() + .field("name"); + + formatter.build(BundleDTO.class) + .inspect() + .fields("*") + .line() + .field("id") + .field("symbolicName") + .field("version") + .field("state") + .part() + .as(b -> String.format("%s[%s]", b.symbolicName, b.id)); + return formatter; + } + + private ComponentDescriptionDTO createComponentDescriptionDTO() { + ComponentDescriptionDTO dto = new ComponentDescriptionDTO(); + dto.name = "name"; + dto.bundle = new BundleDTO(); + dto.bundle.id = 10020; + dto.bundle.lastModified = System.currentTimeMillis() - 10003232; + dto.bundle.state = Bundle.ACTIVE; + dto.bundle.symbolicName = "com.example.foo.bar"; + dto.bundle.version = "1.2.3"; + + dto.factory = null; + + dto.scope = ServiceScope.BUNDLE.toString(); + dto.implementationClass = DTOFormatterTest.class.getName(); + dto.defaultEnabled = false; + dto.immediate = true; + dto.serviceInterfaces = new String[] { + "com.example.Foo", "com.example.Bar" + }; + dto.properties = new HashMap<>(); + dto.properties.put("foo", "bar"); + dto.references = new ReferenceDTO[2]; + dto.references[0] = new ReferenceDTO(); + dto.references[0].name = "r0"; + dto.references[0].cardinality = "1..1"; + dto.references[0].interfaceName = "com.example.Bar"; + dto.references[0].scope = ServiceScope.PROTOTYPE.toString(); + dto.references[1] = new ReferenceDTO(); + dto.references[1].name = "r1"; + dto.references[1].cardinality = "1..*"; + dto.references[1].interfaceName = "com.example.Foo"; + dto.references[1].scope = ServiceScope.BUNDLE.toString(); + dto.configurationPolicy = org.osgi.service.component.annotations.ConfigurationPolicy.IGNORE.toString(); + dto.configurationPid = new String[] { + "com.example.configuration.pid.one", "com.example.configuration.pid.two" + }; + return dto; + } +} \ No newline at end of file diff --git a/util/src/test/java/org/eclipse/osgi/technology/command/util/TableTest.java b/util/src/test/java/org/eclipse/osgi/technology/command/util/TableTest.java new file mode 100644 index 0000000..39918dd --- /dev/null +++ b/util/src/test/java/org/eclipse/osgi/technology/command/util/TableTest.java @@ -0,0 +1,143 @@ +package org.eclipse.osgi.technology.command.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.osgi.technology.command.util.dtoformatter.Canvas; +import org.eclipse.osgi.technology.command.util.dtoformatter.Table; + +/** + * 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 + */ +public class TableTest { + + @org.junit.jupiter.api.Test + public void testTableWithEmptyValue() { + Table table = new Table(1, 0, 0); + int w = table.width(); + int h = table.height(); + table.render(); + + } + + @org.junit.jupiter.api.Test + public void testUnfilledTable() { + Table t = new Table(1, 1, 0); + assertEquals("" + "┌─┐\n" + "│ │\n" + "└─┘\n" + "", t.toString()); + + } + + @org.junit.jupiter.api.Test + public void testEmptyTable() { + Table t = new Table(0, 0, 0); + assertEquals("" + "☒" + "", t.toString()); + + } + + @org.junit.jupiter.api.Test + public void testSimpleTable() { + Table t = new Table(2, 2, 0); + t.set(0, 0, "0x0"); + t.set(0, 1, "0x1"); + t.set(1, 0, "1x0"); + t.set(1, 1, "1x1"); + assertEquals("" + "┌───┬───┐\n" + "│0x0│0x1│\n" + "├───┼───┤\n" + "│1x0│1x1│\n" + "└───┴───┘\n" + "", + t.toString()); + } + + @org.junit.jupiter.api.Test + public void testTableWithUnequalColumns() { + Table t = new Table(2, 2, 0); + t.set(0, 0, "0x0xxxx"); + t.set(0, 1, "0x1"); + t.set(1, 0, "1x0"); + t.set(1, 1, "1x1yyyyyyyyyyyyyyyyyyy"); + assertEquals("" + "┌───────┬──────────────────────┐\n" + "│0x0xxxx│0x1 │\n" + + "├───────┼──────────────────────┤\n" + "│1x0 │1x1yyyyyyyyyyyyyyyyyyy│\n" + + "└───────┴──────────────────────┘\n" + "", t.toString()); + } + + @org.junit.jupiter.api.Test + public void testTableWithUnequalRows() { + Table t = new Table(2, 2, 0); + t.set(0, 0, "0x0\n0x0"); + t.set(0, 1, "0x1"); + t.set(1, 0, "1x0"); + t.set(1, 1, "1x1"); + assertEquals( + "" + "┌───┬───┐\n" + "│0x0│0x1│\n" + "│0x0│ │\n" + "├───┼───┤\n" + "│1x0│1x1│\n" + "└───┴───┘\n" + "", + t.toString()); + } + + @org.junit.jupiter.api.Test + public void testNestedTable() { + Table t1 = new Table(2, 2, 0); + t1.set(0, 0, "0x0/1"); + t1.set(0, 1, "0x1/1"); + t1.set(1, 1, "1x1/1"); + + Table t2 = new Table(2, 2, 0); + t2.set(0, 1, "0x1/2"); + t2.set(1, 0, "1x0/2"); + t2.set(1, 1, "1x1/2"); + + Table t3 = new Table(2, 2, 0); + t3.set(0, 0, "0x0/3"); + t3.set(0, 1, "0x1/3"); + t3.set(1, 0, "1x0/3"); + t3.set(1, 1, "1x1/3"); + + t1.set(1, 0, t2); + t2.set(0, 0, t3); + + assertEquals("" + "┌─────────────────┬─────┐\n" + "│0x0/1 │0x1/1│\n" + "├─────┬─────┬─────┼─────┤\n" + + "│0x0/3│0x1/3│0x1/2│1x1/1│\n" + "├─────┼─────┤ │ │\n" + "│1x0/3│1x1/3│ │ │\n" + + "├─────┴─────┼─────┤ │\n" + "│1x0/2 │1x1/2│ │\n" + "└───────────┴─────┴─────┘\n" + "", + t1.toString()); + + Canvas render = t3.render(); + Canvas rb = render.removeBoxes(); + System.out.println(t3); + System.out.println(rb); + } + + @org.junit.jupiter.api.Test + public void testNestedTableWithTopBold() { + Table t1 = new Table(2, 2, 0); + t1.setBold(); + t1.set(0, 0, "0x0/1"); + t1.set(0, 1, "0x1/1"); + t1.set(1, 1, "1x1/1"); + + Table t2 = new Table(2, 2, 0); + t2.set(0, 1, "0x1/2"); + t2.set(1, 0, "1x0/2"); + t2.set(1, 1, "1x1/2"); + + Table t3 = new Table(2, 2, 0); + t3.set(0, 0, "0x0/3"); + t3.set(0, 1, "0x1/3"); + t3.set(1, 0, "1x0/3"); + t3.set(1, 1, "1x1/3"); + + t1.set(1, 0, t2); + t2.set(0, 0, t3); + + assertEquals("" + "┏━━━━━━━━━━━━━━━━━┯━━━━━┓\n" + "┃0x0/1 │0x1/1┃\n" + "┠─────┬─────┬─────┼─────┨\n" + + "┃0x0/3│0x1/3│0x1/2│1x1/1┃\n" + "┠─────┼─────┤ │ ┃\n" + "┃1x0/3│1x1/3│ │ ┃\n" + + "┠─────┴─────┼─────┤ ┃\n" + "┃1x0/2 │1x1/2│ ┃\n" + "┗━━━━━━━━━━━┷━━━━━┷━━━━━┛\n" + "", + t1.toString()); + + System.out.println(t3); + System.out.println(t3.render().removeBoxes()); + } +} \ No newline at end of file diff --git a/util/src/test/java/org/eclipse/osgi/technology/command/util/Test.java b/util/src/test/java/org/eclipse/osgi/technology/command/util/Test.java new file mode 100644 index 0000000..5f09f1d --- /dev/null +++ b/util/src/test/java/org/eclipse/osgi/technology/command/util/Test.java @@ -0,0 +1,24 @@ +/** + * 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.util; + + +public class Test { + + @org.junit.jupiter.api.Test + void testName() throws Exception { + + } +}