FELIX-1914: Add dev:show-tree to represent bundle dependencies
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@887233 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/assembly/pom.xml b/karaf/assembly/pom.xml
index 23f0497..cdf13cc 100644
--- a/karaf/assembly/pom.xml
+++ b/karaf/assembly/pom.xml
@@ -124,6 +124,10 @@
<artifactId>org.apache.felix.karaf.shell.ssh</artifactId>
</dependency>
<dependency>
+ <groupId>org.apache.felix.karaf.shell</groupId>
+ <artifactId>org.apache.felix.karaf.shell.dev</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.apache.felix.karaf.jaas</groupId>
<artifactId>org.apache.felix.karaf.jaas.boot</artifactId>
</dependency>
diff --git a/karaf/assembly/src/main/descriptors/unix-bin.xml b/karaf/assembly/src/main/descriptors/unix-bin.xml
index 9f16dcf..b38ab74 100644
--- a/karaf/assembly/src/main/descriptors/unix-bin.xml
+++ b/karaf/assembly/src/main/descriptors/unix-bin.xml
@@ -219,6 +219,7 @@
<outputFileNameMapping>org/apache/felix/karaf/shell/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}</outputFileNameMapping>
<includes>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.console</include>
+ <include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.dev</include>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.osgi</include>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.log</include>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.config</include>
diff --git a/karaf/assembly/src/main/descriptors/windows-bin.xml b/karaf/assembly/src/main/descriptors/windows-bin.xml
index 726e3f2..b5db894 100644
--- a/karaf/assembly/src/main/descriptors/windows-bin.xml
+++ b/karaf/assembly/src/main/descriptors/windows-bin.xml
@@ -211,6 +211,7 @@
<outputFileNameMapping>org/apache/felix/karaf/shell/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}</outputFileNameMapping>
<includes>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.console</include>
+ <include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.dev</include>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.osgi</include>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.log</include>
<include>org.apache.felix.karaf.shell:org.apache.felix.karaf.shell.config</include>
diff --git a/karaf/assembly/src/main/filtered-resources/etc/startup.properties b/karaf/assembly/src/main/filtered-resources/etc/startup.properties
index 1a79a4d..cc120f1 100644
--- a/karaf/assembly/src/main/filtered-resources/etc/startup.properties
+++ b/karaf/assembly/src/main/filtered-resources/etc/startup.properties
@@ -46,6 +46,7 @@
org/apache/felix/karaf/shell/org.apache.felix.karaf.shell.config/${pom.version}/org.apache.felix.karaf.shell.config-${pom.version}.jar=30
org/apache/felix/karaf/shell/org.apache.felix.karaf.shell.packages/${pom.version}/org.apache.felix.karaf.shell.packages-${pom.version}.jar=30
org/apache/felix/karaf/shell/org.apache.felix.karaf.shell.commands/${pom.version}/org.apache.felix.karaf.shell.commands-${pom.version}.jar=30
+org/apache/felix/karaf/shell/org.apache.felix.karaf.shell.dev/${pom.version}/org.apache.felix.karaf.shell.dev-${pom.version}.jar=30
org/apache/felix/karaf/jaas/org.apache.felix.karaf.jaas.config/${pom.version}/org.apache.felix.karaf.jaas.config-${pom.version}.jar=30
org/apache/felix/karaf/jaas/org.apache.felix.karaf.jaas.modules/${pom.version}/org.apache.felix.karaf.jaas.modules-${pom.version}.jar=30
org/apache/felix/karaf/admin/org.apache.felix.karaf.admin.core/${pom.version}/org.apache.felix.karaf.admin.core-${pom.version}.jar=30
diff --git a/karaf/pom.xml b/karaf/pom.xml
index d1e61b3..bf7be79 100644
--- a/karaf/pom.xml
+++ b/karaf/pom.xml
@@ -266,6 +266,11 @@
<version>${pom.version}</version>
</dependency>
<dependency>
+ <groupId>org.apache.felix.karaf.shell</groupId>
+ <artifactId>org.apache.felix.karaf.shell.dev</artifactId>
+ <version>${pom.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.felix.karaf.jaas</groupId>
<artifactId>org.apache.felix.karaf.jaas.boot</artifactId>
<version>${pom.version}</version>
diff --git a/karaf/shell/dev/pom.xml b/karaf/shell/dev/pom.xml
new file mode 100644
index 0000000..ce90495
--- /dev/null
+++ b/karaf/shell/dev/pom.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+ <parent>
+ <artifactId>shell</artifactId>
+ <groupId>org.apache.felix.karaf.shell</groupId>
+ <version>1.3.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>org.apache.felix.karaf.shell.dev</artifactId>
+ <packaging>bundle</packaging>
+ <name>Apache Felix Karaf :: Shell Development Commands</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix.karaf.shell</groupId>
+ <artifactId>org.apache.felix.karaf.shell.console</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${artifactId}</Bundle-SymbolicName>
+ <Export-Package>${pom.artifactId}*;version=${project.version}</Export-Package>
+ <Import-Package>
+ !${pom.artifactId}*,
+ org.osgi.service.command,
+ org.apache.felix.gogo.commands,
+ org.apache.felix.karaf.shell.console,
+ *
+ </Import-Package>
+ <Private-Package>!*</Private-Package>
+ <_versionpolicy>${bnd.version.policy}</_versionpolicy>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/ShowBundleTree.java b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/ShowBundleTree.java
new file mode 100644
index 0000000..feee325
--- /dev/null
+++ b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/ShowBundleTree.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev;
+
+import static java.lang.String.format;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.gogo.commands.Argument;
+import org.apache.felix.gogo.commands.Command;
+import org.apache.felix.karaf.shell.console.OsgiCommandSupport;
+import org.apache.felix.karaf.shell.dev.util.Bundles;
+import org.apache.felix.karaf.shell.dev.util.Import;
+import org.apache.felix.karaf.shell.dev.util.Node;
+import org.apache.felix.karaf.shell.dev.util.Tree;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.packageadmin.ExportedPackage;
+import org.osgi.service.packageadmin.PackageAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Command for showing the full tree of bundles that have been used to resolve
+ * a given bundle.
+ */
+@Command(scope = "dev", name = "bundle-tree",
+ description = "Show the tree of bundles based on the wiring information")
+public class ShowBundleTree extends OsgiCommandSupport {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ShowBundleTree.class);
+
+ @Argument(index = 0, name = "id", description = "The ID of the bundle to check", required = true)
+ Long id;
+
+ // the package admin service reference
+ private PackageAdmin admin;
+
+ // a cache of all exported packages
+ private ExportedPackage[] allExportedPackages;
+
+ @Override
+ protected Object doExecute() throws Exception {
+ // Get package admin service.
+ ServiceReference ref = getBundleContext().getServiceReference(PackageAdmin.class.getName());
+ if (ref == null) {
+ System.out.println("PackageAdmin service is unavailable.");
+ return null;
+ }
+
+ // using the getService call ensures that the reference will be released at the end
+ admin = getService(PackageAdmin.class, ref);
+
+
+ Bundle bundle = getBundleContext().getBundle(id);
+ if (bundle == null) {
+ System.err.println("Bundle ID" + id + " is invalid");
+ return null;
+ }
+
+ // let's do the real work here
+ printHeader(bundle);
+ Tree<Bundle> tree = createTree(bundle);
+ printTree(tree);
+ printDuplicatePackages(tree);
+ return null;
+ }
+
+ /*
+ * Print the header
+ */
+ private void printHeader(Bundle bundle) {
+ System.out.printf("Bundle %s [%s] is currently %s%n",
+ bundle.getSymbolicName(),
+ bundle.getBundleId(),
+ Bundles.toString(bundle.getState()));
+ }
+
+ /*
+ * Print the dependency tree
+ */
+ private void printTree(Tree<Bundle> tree) {
+ System.out.printf("%n");
+ tree.write(System.out);
+ }
+
+ /*
+ * Check for bundles in the tree exporting the same package
+ * (possible cause for 'Unresolved constraint...' on a uses-conflict
+ */
+ private void printDuplicatePackages(Tree<Bundle> tree) {
+ Set<Bundle> bundles = tree.flatten();
+ Map<String, Set<Bundle>> exports = new HashMap<String, Set<Bundle>>();
+
+ for (Bundle bundle : bundles) {
+ ExportedPackage[] packages = admin.getExportedPackages(bundle);
+ if (packages != null) {
+ for (ExportedPackage p : packages) {
+ if (exports.get(p.getName()) == null) {
+ exports.put(p.getName(), new HashSet<Bundle>());
+ }
+ exports.get(p.getName()).add(bundle);
+ }
+ }
+ }
+
+ for (String pkg : exports.keySet()) {
+ if (exports.get(pkg).size() > 1) {
+ System.out.printf("%n");
+ System.out.printf("WARNING: multiple bundles are exporting package %s%n", pkg);
+ for (Bundle bundle : exports.get(pkg)) {
+ System.out.printf("- %s%n", bundle);
+ }
+ }
+ }
+ }
+
+ /*
+ * Creates the bundle tree
+ */
+ protected Tree<Bundle> createTree(Bundle bundle) {
+ Tree<Bundle> tree = new Tree<Bundle>(bundle);
+ Set<Bundle> trail = new HashSet<Bundle>();
+ if (bundle.getState() >= Bundle.RESOLVED) {
+ createNode(tree, trail);
+ } else {
+ for (Import i : Import.parse(String.valueOf(bundle.getHeaders().get("Import-Package")))) {
+ for (ExportedPackage ep : admin.getExportedPackages(i.getPackage())) {
+ if (ep.getVersion().compareTo(i.getVersion()) >= 0) {
+ if (!bundle.equals(ep.getExportingBundle())) {
+ Node child = tree.addChild(ep.getExportingBundle());
+ System.out.printf("- using %s to resolve import %s%n", ep.getExportingBundle(), i);
+ createNode(child, trail);
+ }
+ }
+ }
+ }
+ }
+ return tree;
+ }
+
+ /*
+ * Creates a node in the bundle tree
+ */
+ private void createNode(Node<Bundle> node, Set<Bundle> trail) {
+ Bundle bundle = node.getValue();
+ Set<Bundle> exporters = getWiredBundles(bundle);
+
+ for (Bundle exporter : exporters) {
+ if (trail.contains(exporter)) {
+ LOGGER.debug(format("Skipping %s because it already exists in the current tree branch", exporter));
+ } else {
+ trail.add(exporter);
+ Node child = node.addChild(exporter);
+ LOGGER.debug(format("Adding %s as a dependency for %s", exporter, bundle));
+ createNode(child, trail);
+ trail.remove(exporter);
+ }
+ }
+ }
+
+ /*
+ * Get the list of bundles from which the given bundle imports packages
+ */
+ private Set<Bundle> getWiredBundles(Bundle bundle) {
+ // the set of bundles from which the bundle imports packages
+ Set<Bundle> exporters = new LinkedHashSet<Bundle>();
+
+ for (ExportedPackage pkg : getAllExportedPackages()) {
+ Bundle[] bundles = pkg.getImportingBundles();
+ if (bundles != null) {
+ for (Bundle importingBundle : bundles) {
+ if (bundle.equals(importingBundle)
+ && !(pkg.getExportingBundle().getBundleId() == 0)
+ && !(pkg.getExportingBundle().equals(bundle))) {
+ exporters.add(pkg.getExportingBundle());
+ }
+ }
+ }
+ }
+ return exporters;
+ }
+
+ /*
+ * Get the full list of package exports from PackageAdmin
+ */
+ private ExportedPackage[] getAllExportedPackages() {
+ if (allExportedPackages == null) {
+ allExportedPackages = admin.getExportedPackages((Bundle) null);
+ }
+ return allExportedPackages;
+ }
+}
diff --git a/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Bundles.java b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Bundles.java
new file mode 100644
index 0000000..acf70eb
--- /dev/null
+++ b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Bundles.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import org.osgi.framework.Bundle;
+
+/**
+ * A set of utility methods for working with {@link org.osgi.framework.Bundle}s
+ */
+public class Bundles {
+
+ /**
+ * Return a String representation of a bundle state
+ */
+ public static String toString(int state) {
+ switch (state) {
+ case Bundle.UNINSTALLED : return "UNINSTALLED";
+ case Bundle.INSTALLED : return "INSTALLED";
+ case Bundle.RESOLVED: return "RESOLVED";
+ case Bundle.STARTING : return "STARTING";
+ case Bundle.STOPPING : return "STOPPING";
+ case Bundle.ACTIVE : return "ACTIVE";
+ default : return "UNKNOWN";
+ }
+ }
+}
diff --git a/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Import.java b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Import.java
new file mode 100644
index 0000000..bea30f1
--- /dev/null
+++ b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Import.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.osgi.framework.Version;
+
+/**
+ * Simple class to model an OSGi Import-Package
+ */
+public class Import {
+
+ private final String packageName;
+ private final Version version;
+ private final String value;
+
+ /**
+ * Create a new import based on the string value found in MANIFEST.MF
+ *
+ * @param value the MANIFEST.MF value
+ */
+ protected Import(String value) {
+ super();
+ this.value = value;
+ if (value.contains(";")) {
+ this.packageName = value.split(";")[0];
+ } else {
+ this.packageName = value;
+ }
+ if (value.contains("version=")) {
+ this.version = extractVersion(value);
+ } else {
+ this.version = Version.emptyVersion;
+ }
+ }
+
+ /*
+ * Extract the version from the string
+ */
+ private Version extractVersion(String value) {
+ int begin = value.indexOf("version=") + 8;
+ int end = value.indexOf(";", begin);
+ if (end < 0) {
+ return Version.parseVersion(unquote(value.substring(begin)));
+ } else {
+ return Version.parseVersion(unquote(value.substring(begin, end)));
+ }
+ }
+
+ /*
+ * Remove leading/trailing quotes
+ */
+ private String unquote(String string) {
+ return string.replace("\"", "");
+ }
+
+ public String getPackage() {
+ return packageName;
+ }
+
+ public Version getVersion() {
+ return version;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ /**
+ * Parse the value of an Import-Package META-INF header and return
+ * a list of Import instances
+ */
+ public static List<Import> parse(String value) {
+ LinkedList<Import> imports = new LinkedList<Import>();
+ for (String imp : value.split(",")) {
+ imports.add(new Import(imp));
+ }
+ return imports;
+ }
+}
diff --git a/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Node.java b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Node.java
new file mode 100644
index 0000000..450828d
--- /dev/null
+++ b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Node.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Represents a node in a {@link org.apache.felix.karaf.shell.dev.util.Tree}
+ */
+public class Node<T> {
+
+ private final T value;
+ private Node<T> parent;
+ private List<Node<T>> children = new LinkedList<Node<T>>();
+
+ /**
+ * Creates a new node. Only meant for internal use,
+ * new nodes should be added using the {@link #addChild(Object)} method
+ *
+ * @param value the node value
+ */
+ protected Node(T value) {
+ super();
+ this.value = value;
+ }
+
+ /**
+ * Creates a new node. Only meant for internal use,
+ * new nodes should be added using the {@link #addChild(Object)} method
+ *
+ * @param value the node value
+ */
+ protected Node(T value, Node<T> parent) {
+ this(value);
+ this.parent = parent;
+ }
+
+ /**
+ * Access the node's value
+ */
+ public T getValue() {
+ return value;
+ }
+
+ /**
+ * Access the node's child nodes
+ */
+ public List<Node<T>> getChildren() {
+ return children;
+ }
+
+ /**
+ * Adds a child to this node
+ *
+ * @param value the child's value
+ * @return the child node
+ */
+ public Node addChild(T value) {
+ Node node = new Node(value, this);
+ children.add(node);
+ return node;
+ }
+
+ /**
+ * Give a set of values in the tree.
+ *
+ * @return
+ */
+ public Set<T> flatten() {
+ Set<T> result = new HashSet<T>();
+ result.add(getValue());
+ for (Node<T> child : getChildren()) {
+ result.addAll(child.flatten());
+ }
+ return result;
+ }
+
+ /*
+ * Write this node to the PrintWriter. It should be indented one step
+ * further for every element in the indents array. If an element in the
+ * array is <code>true</code>, there should be a | to connect to the next
+ * sibling.
+ */
+ protected void write(PrintWriter writer, boolean... indents) {
+ for (boolean indent : indents) {
+ writer.printf("%-3s", indent ? "|" : "");
+ }
+ writer.printf("+- %s%n", value);
+ for (Node<T> child : getChildren()) {
+ child.write(writer, concat(indents, hasNextSibling()));
+ }
+ }
+
+ /*
+ * Is this node the last child node for its parent
+ * or is there a next sibling?
+ */
+ private boolean hasNextSibling() {
+ if (parent == null) {
+ return false;
+ } else {
+ return parent.getChildren().size() > 1
+ && parent.getChildren().indexOf(this) < parent.getChildren().size() - 1;
+ }
+ }
+
+ /*
+ * Add an element to the end of the array
+ */
+ private boolean[] concat(boolean[] array, boolean element) {
+ boolean[] result = new boolean[array.length + 1];
+ for (int i = 0 ; i < array.length ; i++) {
+ result[i] = array[i];
+ }
+ result[array.length] = element;
+ return result;
+ }
+}
diff --git a/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Tree.java b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Tree.java
new file mode 100644
index 0000000..ae61eed5
--- /dev/null
+++ b/karaf/shell/dev/src/main/java/org/apache/felix/karaf/shell/dev/util/Tree.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents a tree that can be written to the console.
+ *
+ * The output will look like this:
+ * <pre>
+ * root
+ * +- child1
+ * | +- grandchild
+ * +- child2
+ * </pre>
+ */
+public class Tree<T> extends Node<T> {
+
+ /**
+ * Creates a new tree with the given root node
+ *
+ * @param root the root node
+ */
+ public Tree(T root) {
+ super(root);
+ }
+
+ /**
+ * Write the tree to a PrintStream
+ * @param stream
+ */
+ public void write(PrintStream stream) {
+ write(new PrintWriter(stream));
+ }
+
+ /**
+ * Write the tree to a PrintWriter
+ * @param writer
+ */
+ public void write(PrintWriter writer) {
+ writer.printf("%s%n", getValue());
+ for (Node<T> child : getChildren()) {
+ child.write(writer);
+ }
+ writer.flush();
+ }
+}
diff --git a/karaf/shell/dev/src/main/resources/OSGI-INF/blueprint/shell-dev.xml b/karaf/shell/dev/src/main/resources/OSGI-INF/blueprint/shell-dev.xml
new file mode 100644
index 0000000..a695b7a
--- /dev/null
+++ b/karaf/shell/dev/src/main/resources/OSGI-INF/blueprint/shell-dev.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+
+ <command-bundle xmlns="http://felix.apache.org/karaf/xmlns/shell/v1.0.0">
+ <command name="dev/show-tree">
+ <action class="org.apache.felix.karaf.shell.dev.ShowBundleTree"/>
+ </command>
+ </command-bundle>
+
+</blueprint>
diff --git a/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/BundlesTest.java b/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/BundlesTest.java
new file mode 100644
index 0000000..94c28a6
--- /dev/null
+++ b/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/BundlesTest.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+
+/**
+ * Test cases for {@link org.apache.felix.karaf.shell.dev.util.Bundles}
+ */
+public class BundlesTest {
+
+ @Test
+ public void testToString() {
+ assertEquals("UNINSTALLED", Bundles.toString(Bundle.UNINSTALLED));
+ assertEquals("INSTALLED", Bundles.toString(Bundle.INSTALLED));
+ assertEquals("RESOLVED", Bundles.toString(Bundle.RESOLVED));
+ assertEquals("STARTING", Bundles.toString(Bundle.STARTING));
+ assertEquals("STOPPING", Bundles.toString(Bundle.STOPPING));
+ assertEquals("ACTIVE", Bundles.toString(Bundle.ACTIVE));
+ }
+}
diff --git a/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/ImportTest.java b/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/ImportTest.java
new file mode 100644
index 0000000..cd9543a
--- /dev/null
+++ b/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/ImportTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import java.util.List;
+
+import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import org.junit.Test;
+import org.osgi.framework.Version;
+
+/**
+ * Test cases for {@link org.apache.felix.karaf.shell.dev.util.Import}
+ */
+public class ImportTest {
+
+ @Test
+ public void createWithPackageName() {
+ Import i = new Import("org.wip.foo");
+ assertEquals("org.wip.foo", i.getPackage());
+ }
+
+ @Test
+ public void createWithPackageNameAndVersion() {
+ Import i = new Import("org.wip.bar;version=\"2.0.0\"");
+ assertEquals("org.wip.bar", i.getPackage());
+ assertEquals(new Version("2.0.0"), i.getVersion());
+ }
+
+ @Test
+ public void createListOfImports() {
+ List<Import> imports = Import.parse("org.wip.bar;version=\"2.0.0\",org.wip.foo");
+ assertNotNull(imports);
+ assertEquals(2, imports.size());
+ assertEquals("org.wip.bar", imports.get(0).getPackage());
+ assertEquals("org.wip.foo", imports.get(1).getPackage());
+ }
+}
diff --git a/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/TreeTest.java b/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/TreeTest.java
new file mode 100644
index 0000000..2132687
--- /dev/null
+++ b/karaf/shell/dev/src/test/java/org/apache/felix/karaf/shell/dev/util/TreeTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.shell.dev.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+
+/**
+ * Test cases for {@link org.apache.felix.karaf.shell.dev.util.Tree}
+ * and {@link org.apache.felix.karaf.shell.dev.util.Node}
+ */
+public class TreeTest {
+
+ @Test
+ public void writeTreeWithOneChild() throws IOException {
+ Tree<String> tree = new Tree<String>("root");
+ tree.addChild("child");
+
+ BufferedReader reader = read(tree);
+
+ assertEquals("root" , reader.readLine());
+ assertEquals("+- child" , reader.readLine());
+ }
+
+ @Test
+ public void writeTreeWithChildAndGrandChild() throws IOException {
+ Tree<String> tree = new Tree<String>("root");
+ Node<String> node = tree.addChild("child");
+ node.addChild("grandchild");
+
+ BufferedReader reader = read(tree);
+
+ assertEquals("root" , reader.readLine());
+ assertEquals("+- child" , reader.readLine());
+ assertEquals(" +- grandchild", reader.readLine());
+ }
+
+ @Test
+ public void writeTreeWithTwoChildrenAndOneGrandchild() throws IOException {
+ Tree<String> tree = new Tree<String>("root");
+ Node<String> child = tree.addChild("child1");
+ child.addChild("grandchild");
+ tree.addChild("child2");
+
+ BufferedReader reader = read(tree);
+
+ assertEquals("root" , reader.readLine());
+ assertEquals("+- child1" , reader.readLine());
+ assertEquals("| +- grandchild", reader.readLine());
+ assertEquals("+- child2" , reader.readLine());
+ }
+
+ @Test
+ public void flattenTree() throws IOException {
+ Tree<String> tree = new Tree<String>("root");
+ Node<String> child1 = tree.addChild("child1");
+ child1.addChild("grandchild");
+ Node child2 = tree.addChild("child2");
+ child2.addChild("grandchild");
+
+ Set<String> elements = tree.flatten();
+ assertNotNull(elements);
+ assertEquals(4, elements.size());
+ assertTrue(elements.contains("root"));
+ assertTrue(elements.contains("child1"));
+ assertTrue(elements.contains("child2"));
+ assertTrue(elements.contains("grandchild"));
+ }
+
+ private BufferedReader read(Tree<String> tree) {
+ StringWriter writer = new StringWriter();
+ tree.write(new PrintWriter(writer));
+
+ BufferedReader reader = new BufferedReader(new StringReader(writer.getBuffer().toString()));
+ return reader;
+ }
+}
diff --git a/karaf/shell/pom.xml b/karaf/shell/pom.xml
index c50ccee..5a6aa2d 100644
--- a/karaf/shell/pom.xml
+++ b/karaf/shell/pom.xml
@@ -42,6 +42,7 @@
<module>packages</module>
<module>ssh</module>
<module>wrapper</module>
+ <module>dev</module>
</modules>
</project>