diff --git a/.gitignore b/.gitignore index a98da61..d4e260f 100644 --- a/.gitignore +++ b/.gitignore @@ -240,4 +240,7 @@ gradle-app.setting # JDT-specific (Eclipse Java Development Tools) .classpath +# Generated Graphs from lx graph +src/main/java/io/openliberty/explore/GraphDisplay/Graph.svg + # End of https://www.toptal.com/developers/gitignore/api/macos,intellij,gradle,java,vim,visualstudiocode diff --git a/gradle/wrapper/gradle-wrapper.jar b/.gradle-wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to .gradle-wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/.gradle-wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to .gradle-wrapper/gradle-wrapper.properties diff --git a/build.gradle b/build.gradle index 3cccba4..25b0a75 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ ext { } tasks.named('wrapper') { - jarFile = rootProject.file('.gradle/gradle-wrapper.jar') + jarFile = rootProject.file('.gradle-wrapper/gradle-wrapper.jar') } compileJava { @@ -27,10 +27,13 @@ repositories { } dependencies { + implementation "guru.nidi:graphviz-java:0.18.1" implementation "org.jgrapht:jgrapht-core:1.5.1" implementation "org.jgrapht:jgrapht-io:1.5.1" implementation "info.picocli:picocli:4.6.3" implementation "org.barfuin.texttree:text-tree:2.1.2" implementation "org.osgi:osgi.core:8.0.0" implementation "org.apache.commons:commons-collections4:4.4" + implementation "commons-io:commons-io:2.6" + implementation "org.slf4j:slf4j-nop:1.7.36" } diff --git a/gradlew b/gradlew index 744e882..dfc9e2a 100755 --- a/gradlew +++ b/gradlew @@ -80,7 +80,7 @@ case "`uname`" in ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH=$APP_HOME/.gradle-wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..1a5d6d5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -67,7 +67,7 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH=%APP_HOME%\.gradle-wrapper\gradle-wrapper.jar @rem Execute Gradle diff --git a/src/main/java/io/openliberty/explore/DescribeCommand.java b/src/main/java/io/openliberty/explore/DescribeCommand.java new file mode 100644 index 0000000..1c15ef4 --- /dev/null +++ b/src/main/java/io/openliberty/explore/DescribeCommand.java @@ -0,0 +1,33 @@ +/* + * ============================================================================= + * Copyright (c) 2022 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * ============================================================================= + */ +package io.openliberty.explore; + +import picocli.CommandLine.Command; +import static picocli.CommandLine.Help.Ansi.Style.fg_red; + +@Command( + name = "describe", + description = "Display description for matching features" +) +public class DescribeCommand extends QueryCommand { + + DescribeCommand() { + super(DisplayOption.normal, true); + } + + void execute() { + explorer().allResults().stream().map(e -> "" + fg_red.on() + this.displayName(e) + ":" + fg_red.off() + "\n" + e.description() + "\n"). + sorted(). + forEach(System.out::println); + } +} diff --git a/src/main/java/io/openliberty/explore/GraphCommand.java b/src/main/java/io/openliberty/explore/GraphCommand.java index 31bf505..4d29031 100644 --- a/src/main/java/io/openliberty/explore/GraphCommand.java +++ b/src/main/java/io/openliberty/explore/GraphCommand.java @@ -12,21 +12,35 @@ */ package io.openliberty.explore; +import guru.nidi.graphviz.engine.Format; +import guru.nidi.graphviz.engine.Graphviz; +import guru.nidi.graphviz.model.MutableGraph; +import guru.nidi.graphviz.parse.Parser; import io.openliberty.inspect.Bundle; import io.openliberty.inspect.Element; import io.openliberty.inspect.feature.Feature; +import org.apache.commons.io.FileUtils; import org.jgrapht.graph.DefaultEdge; import org.jgrapht.nio.Attribute; import org.jgrapht.nio.dot.DOTExporter; import picocli.CommandLine.Command; +import java.awt.Desktop; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; import java.io.StringWriter; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.jgrapht.nio.DefaultAttribute.createAttribute; +import static picocli.CommandLine.Help.Ansi.Style.fg_blue; +import static picocli.CommandLine.Help.Ansi.Style.underline; @Command( name = "graph", @@ -44,16 +58,28 @@ String displayName(Element e) { + '"'; } + URI generateSVGGraph(String graphDotCode) throws IOException { + MutableGraph g = new Parser().read(graphDotCode); + File svgFile = new File("src/main/java/io/openliberty/explore/GraphDisplay/Graph.svg"); + Graphviz.fromGraph(g).render(Format.SVG).toFile(svgFile); + return svgFile.toURI(); + } + void execute() { var exporter = new DOTExporter(this::displayName); exporter.setVertexAttributeProvider(this::getDotAttributes); var writer = new StringWriter(); exporter.exportGraph(explorer().subgraph(), writer); System.out.println(writer); + try { + System.out.println("SVG Graph: " + underline.on() + fg_blue.on() + generateSVGGraph(writer.toString()) + underline.off() + fg_blue.off()); + } catch (IOException e) { + throw new RuntimeException(e); + } } private static Attribute shape(Element element) { - if (element instanceof Feature) switch(element.visibility()) { + if (element instanceof Feature) switch (element.visibility()) { case PUBLIC: return createAttribute("tripleoctagon"); case PROTECTED: return createAttribute("doubleoctagon"); case PRIVATE: return createAttribute("octagon"); diff --git a/src/main/java/io/openliberty/explore/LibertyExplorer.java b/src/main/java/io/openliberty/explore/LibertyExplorer.java index a60d957..142b720 100644 --- a/src/main/java/io/openliberty/explore/LibertyExplorer.java +++ b/src/main/java/io/openliberty/explore/LibertyExplorer.java @@ -49,7 +49,7 @@ name = "lx", description = "Liberty installation eXplorer", version = "Liberty installation eXplorer 0.5", - subcommands = {ListCommand.class, GraphCommand.class, TreeCommand.class, HelpCommand.class}, + subcommands = {DescribeCommand.class, ListCommand.class, GraphCommand.class, TreeCommand.class, HelpCommand.class}, defaultValueProvider = PropertiesDefaultProvider.class ) public class LibertyExplorer { diff --git a/src/main/java/io/openliberty/inspect/Bundle.java b/src/main/java/io/openliberty/inspect/Bundle.java index ee383cb..43f729b 100644 --- a/src/main/java/io/openliberty/inspect/Bundle.java +++ b/src/main/java/io/openliberty/inspect/Bundle.java @@ -51,13 +51,18 @@ public final class Bundle implements Element { @Override public Path path() { return path; } @Override - public String symbolicName() { - return symbolicName; - } + public String symbolicName() { return symbolicName; } + @Override + public String name() { return symbolicName() + "_" + version; } @Override - public String name() { - return symbolicName() + "_" + version; + public String description() { + String description = manifest.getMainAttributes().getValue("Bundle-Description"); + if (description != null) { + return description; + } + return "No Description found"; } + @Override public Version version() { return version; } @Override diff --git a/src/main/java/io/openliberty/inspect/Element.java b/src/main/java/io/openliberty/inspect/Element.java index 74866c5..cbfed8d 100644 --- a/src/main/java/io/openliberty/inspect/Element.java +++ b/src/main/java/io/openliberty/inspect/Element.java @@ -27,6 +27,7 @@ public interface Element extends Comparable { default String fileName() { return path().getFileName().toString(); } default String pathName() { return path().toString(); } String name(); + default String description() { return "No Description found"; } Version version(); /** Returns a stream of other names for this element */ Stream aka(); diff --git a/src/main/java/io/openliberty/inspect/feature/Feature.java b/src/main/java/io/openliberty/inspect/feature/Feature.java index ccd7a69..a37cc47 100644 --- a/src/main/java/io/openliberty/inspect/feature/Feature.java +++ b/src/main/java/io/openliberty/inspect/feature/Feature.java @@ -20,15 +20,18 @@ import java.io.IOError; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.stream.Stream; +import static io.openliberty.inspect.Visibility.UNKNOWN; import static java.util.stream.Collectors.toUnmodifiableList; public final class Feature implements Element { @@ -39,6 +42,7 @@ public final class Feature implements Element { private final Version version; private final Visibility visibility; private final List contents; + private final Manifest manifest; private final boolean isAutoFeature; public Feature(Path path) { @@ -52,11 +56,7 @@ public Feature(Path path) { Optional symbolicName = ManifestKey.SUBSYSTEM_SYMBOLICNAME.parseValues(attributes).findFirst(); this.fullName = symbolicName.orElseThrow(Error::new).id; this.shortName = ManifestKey.IBM_SHORTNAME.get(attributes).orElse(null); - this.visibility = symbolicName - .map(v -> v.getQualifier("visibility")) - .map(String::toUpperCase) - .map(Visibility::valueOf) - .orElse(Visibility.UNKNOWN); + this.visibility = symbolicName.map(Feature::getVisibility).orElse(UNKNOWN); this.name = visibility == Visibility.PUBLIC ? shortName().orElse(fullName) : fullName; this.contents = ManifestKey.SUBSYSTEM_CONTENT.parseValues(attributes) .map(Feature::createSpec) @@ -65,32 +65,51 @@ public Feature(Path path) { .collect(toUnmodifiableList()); this.isAutoFeature = ManifestKey.IBM_PROVISION_CAPABILITY.isPresent(attributes); this.version = ManifestKey.SUBSYSTEM_VERSION.get(attributes).map(Version::new).orElse(Version.emptyVersion); + try { + this.manifest = new Manifest(path.toUri().toURL().openStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Visibility getVisibility(ManifestValueEntry symbolicName) { + String vis = symbolicName.getQualifier("visibility").toUpperCase(); + try { + return Visibility.valueOf(vis); + } catch (IllegalArgumentException e) { + return null; + } } - @Override public Path path() { return path; } - @Override public String symbolicName() { return fullName; } public Optional shortName() { return Optional.ofNullable(shortName); } - @Override public Visibility visibility() { return this.visibility; } - @Override public String name() { return name; } - @Override + public String description() { + if (visibility() == Visibility.PUBLIC) { + try { + return getPublicFeatureDescription(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + Attributes attributes = manifest.getMainAttributes(); + String symbolicNameAttr = attributes.getValue("Subsystem-SymbolicName").substring(symbolicName().length() + 2); + if(isAutoFeature()) return "" + symbolicNameAttr + "\n" + attributes.getValue("IBM-Provision-Capability"); + if(!symbolicNameAttr.isEmpty()) return symbolicNameAttr; + return "No Description found"; + } public Version version() { return version; } - @Override public Stream aka() { return Stream.of(shortName); } - @Override public boolean isAutoFeature() { return isAutoFeature; } - @Override public Stream findDependencies(Collection elements) { return contents.stream() .map(spec -> spec.findBestMatch(elements)) .flatMap(Optional::stream); } - @Override public int compareTo(Element other) { if (!(other instanceof Feature)) return -1; // Features sort before other element types Feature that = (Feature) other; @@ -104,25 +123,48 @@ public int compareTo(Element other) { public boolean equals(Object other) { if (this == other) return true; if (other == null) return false; - if (! (other instanceof Feature)) return false; + if (!(other instanceof Feature)) return false; Feature that = (Feature) other; return this.fullName.equals(that.fullName); } @Override - public int hashCode() { return Objects.hash(fullName); } + public int hashCode() { + return Objects.hash(fullName); + } @Override - public String toString() { return symbolicName(); } + public String toString() { + return symbolicName(); + } static Optional createSpec(ManifestValueEntry ve) { String type = ve.getQualifierOrDefault("type", "bundle"); switch (type) { - case "osgi.subsystem.feature": return Optional.of(ve).map(FeatureSpec::new); - case "bundle": return Optional.of(ve).map(BundleSpec::new); - case "file": return Optional.empty(); - case "jar": return Optional.empty(); - default: throw new IllegalStateException("Unknown content type: " + type); + case "osgi.subsystem.feature": + return Optional.of(ve).map(FeatureSpec::new); + case "bundle": + return Optional.of(ve).map(BundleSpec::new); + case "file": + return Optional.empty(); + case "jar": + return Optional.empty(); + default: + throw new IllegalStateException("Unknown content type: " + type); } } + + private String getPublicFeatureDescription() throws IOException { + Path featuresRoot = path.getParent(); + Path propertiesFile = validate(featuresRoot.resolve("l10n/" + symbolicName() + ".properties")); + Properties prop = new Properties(); + prop.load(new FileInputStream(propertiesFile.toString())); + return prop.getProperty("description"); + } + + private static Path validate(Path path) { + if (Files.exists(path)) return path; + throw new Error("No properties file found: " + path.toFile().getAbsolutePath()); + } + }