/*
* 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.brooklyn.cli;
import io.airlift.command.Command;
import io.airlift.command.Option;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.api.catalog.Catalog;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.ImplementedBy;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationResolver;
import org.apache.brooklyn.api.objs.BrooklynObject;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.sensor.Enricher;
import org.apache.brooklyn.cli.lister.ClassFinder;
import org.apache.brooklyn.cli.lister.ItemDescriptors;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.text.TemplateProcessor;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.text.Strings;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
public class ItemLister {
private static final Logger LOG = LoggerFactory.getLogger(ItemLister.class);
private static final String BASE = "brooklyn/item-lister";
private static final String BASE_TEMPLATES = BASE+"/"+"templates";
private static final String BASE_STATICS = BASE+"/"+"statics";
@Command(name = "list-objects", description = "List Brooklyn objects (Entities, Policies, Enrichers and Locations)")
public static class ListAllCommand extends AbstractMain.BrooklynCommandCollectingArgs {
@Option(name = { "--jars" }, title = "Jars", description = "Jars to scan. If a file (not a url) pointing at a directory, will include all files in that directory")
public List<String> jars = Lists.newLinkedList();
@Option(name = { "--type-regex" }, title = "Regex for types to list")
public String typeRegex;
@Option(name = { "--catalog-only" }, title = "Whether to only list items annotated with @Catalog")
public boolean catalogOnly = true;
@Option(name = { "--ignore-impls" }, title = "Ignore Entity implementations, where there is an Entity interface with @ImplementedBy")
public boolean ignoreImpls = false;
@Option(name = { "--headings-only" }, title = "Whether to only show name/type, and not config keys etc")
public boolean headingsOnly = false;
@Option(name = { "--output-folder" }, title = "Folder to save output")
public String outputFolder;
@SuppressWarnings("unchecked")
@Override
public Void call() throws Exception {
List<URL> urls = getUrls();
LOG.info("Retrieving objects from "+urls);
// TODO Remove duplication from separate ListPolicyCommand etc
List<Class<? extends Entity>> entityTypes = getTypes(urls, Entity.class);
List<Class<? extends Policy>> policyTypes = getTypes(urls, Policy.class);
List<Class<? extends Enricher>> enricherTypes = getTypes(urls, Enricher.class);
List<Class<? extends Location>> locationTypes = getTypes(urls, Location.class, Boolean.FALSE);
Map<String, Object> result = ImmutableMap.<String, Object>builder()
.put("entities", ItemDescriptors.toItemDescriptors(entityTypes, headingsOnly, "name"))
.put("policies", ItemDescriptors.toItemDescriptors(policyTypes, headingsOnly, "name"))
.put("enrichers", ItemDescriptors.toItemDescriptors(enricherTypes, headingsOnly, "name"))
.put("locations", ItemDescriptors.toItemDescriptors(locationTypes, headingsOnly, "type"))
.put("locationResolvers", ItemDescriptors.toItemDescriptors(ImmutableList.copyOf(ServiceLoader.load(LocationResolver.class)), true))
.build();
String json = toJson(result);
if (outputFolder == null) {
System.out.println(json);
} else {
LOG.info("Outputting item list (size "+itemCount+") to " + outputFolder);
String outputPath = Os.mergePaths(outputFolder, "index.html");
String parentDir = (new File(outputPath).getParentFile()).getAbsolutePath();
mkdir(parentDir, "entities");
mkdir(parentDir, "policies");
mkdir(parentDir, "enrichers");
mkdir(parentDir, "locations");
mkdir(parentDir, "locationResolvers"); //TODO nothing written here yet...
mkdir(parentDir, "style");
mkdir(Os.mergePaths(parentDir, "style"), "js");
mkdir(Os.mergePaths(parentDir, "style", "js"), "catalog");
Files.write("var items = " + json, new File(Os.mergePaths(outputFolder, "items.js")), Charsets.UTF_8);
ResourceUtils resourceUtils = ResourceUtils.create(this);
// root - just loads the above JSON
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "brooklyn-object-list.html", "index.html");
// statics - structure mirrors docs (not for any real reason however... the json is usually enough for our docs)
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "common.js");
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "items.css");
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "style/js/underscore-min.js");
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "style/js/underscore-min.map");
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, "style/js/catalog/typeahead.js");
// now make pages for each item
List<Map<String, Object>> entities = (List<Map<String, Object>>) result.get("entities");
String entityTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "entity.html"));
for (Map<String, Object> entity : entities) {
String type = (String) entity.get("type");
String name = (String) entity.get("name");
String entityHtml = TemplateProcessor.processTemplateContents(entityTemplateHtml, ImmutableMap.of("type", type, "name", name));
Files.write(entityHtml, new File(Os.mergePaths(outputFolder, "entities", type + ".html")), Charsets.UTF_8);
}
List<Map<String, Object>> policies = (List<Map<String, Object>>) result.get("policies");
String policyTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "policy.html"));
for (Map<String, Object> policy : policies) {
String type = (String) policy.get("type");
String name = (String) policy.get("name");
String policyHtml = TemplateProcessor.processTemplateContents(policyTemplateHtml, ImmutableMap.of("type", type, "name", name));
Files.write(policyHtml, new File(Os.mergePaths(outputFolder, "policies", type + ".html")), Charsets.UTF_8);
}
List<Map<String, Object>> enrichers = (List<Map<String, Object>>) result.get("enrichers");
String enricherTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "enricher.html"));
for (Map<String, Object> enricher : enrichers) {
String type = (String) enricher.get("type");
String name = (String) enricher.get("name");
String enricherHtml = TemplateProcessor.processTemplateContents(enricherTemplateHtml, ImmutableMap.of("type", type, "name", name));
Files.write(enricherHtml, new File(Os.mergePaths(outputFolder, "enrichers", type + ".html")), Charsets.UTF_8);
}
List<Map<String, Object>> locations = (List<Map<String, Object>>) result.get("locations");
String locationTemplateHtml = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_TEMPLATES, "location.html"));
for (Map<String, Object> location : locations) {
String type = (String) location.get("type");
String locationHtml = TemplateProcessor.processTemplateContents(locationTemplateHtml, ImmutableMap.of("type", type));
Files.write(locationHtml, new File(Os.mergePaths(outputFolder, "locations", type + ".html")), Charsets.UTF_8);
}
LOG.info("Finished outputting item list to " + outputFolder);
}
return null;
}
private void copyFromItemListerClasspathBaseStaticsToOutputDir(ResourceUtils resourceUtils, String item) throws IOException {
copyFromItemListerClasspathBaseStaticsToOutputDir(resourceUtils, item, item);
}
private void copyFromItemListerClasspathBaseStaticsToOutputDir(ResourceUtils resourceUtils, String item, String dest) throws IOException {
String js = resourceUtils.getResourceAsString(Urls.mergePaths(BASE_STATICS, item));
Files.write(js, new File(Os.mergePaths(outputFolder, dest)), Charsets.UTF_8);
}
private void mkdir(String rootDir, String dirName) {
(new File(Os.mergePaths(rootDir, dirName))).mkdirs();
}
protected List<URL> getUrls() throws MalformedURLException {
List<URL> urls = Lists.newArrayList();
if (jars.isEmpty()) {
String classpath = System.getenv("INITIAL_CLASSPATH");
if (Strings.isNonBlank(classpath)) {
List<String> entries = Splitter.on(":").omitEmptyStrings().trimResults().splitToList(classpath);
for (String entry : entries) {
if (entry.endsWith(".jar") || entry.endsWith("/*")) {
urls.addAll(ClassFinder.toJarUrls(entry.replace("/*", "")));
}
}
} else {
throw new IllegalArgumentException("No Jars to process");
}
} else {
for (String jar : jars) {
List<URL> expanded = ClassFinder.toJarUrls(jar);
if (expanded.isEmpty())
LOG.warn("No jars found at: "+jar);
urls.addAll(expanded);
}
}
return urls;
}
private <T extends BrooklynObject> List<Class<? extends T>> getTypes(List<URL> urls, Class<T> type) {
return getTypes(urls, type, null);
}
int itemCount = 0;
private <T extends BrooklynObject> List<Class<? extends T>> getTypes(List<URL> urls, Class<T> type, Boolean catalogOnlyOverride) {
FluentIterable<Class<? extends T>> fluent = FluentIterable.from(ClassFinder.findClasses(urls, type));
if (typeRegex != null) {
fluent = fluent.filter(ClassFinder.withClassNameMatching(typeRegex));
}
if (catalogOnlyOverride == null ? catalogOnly : catalogOnlyOverride) {
fluent = fluent.filter(ClassFinder.withAnnotation(Catalog.class));
}
List<Class<? extends T>> filtered = fluent.toList();
Collection<Class<? extends T>> result;
if (ignoreImpls) {
result = MutableSet.copyOf(filtered);
for (Class<? extends T> clazz : filtered) {
ImplementedBy implementedBy = clazz.getAnnotation(ImplementedBy.class);
if (implementedBy != null) {
result.remove(implementedBy.value());
}
}
} else {
result = filtered;
}
itemCount += result.size();
return ImmutableList.copyOf(result);
}
private String toJson(Object obj) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
.enable(SerializationFeature.INDENT_OUTPUT)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.setSerializationInclusion(JsonInclude.Include.ALWAYS)
// Only serialise annotated fields
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return objectMapper.writeValueAsString(obj);
}
}
}