/*******************************************************************************
* Copyright (c) 2014 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.fusesource.ide.generator;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.transform.stream.StreamSource;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.apache.camel.CamelContext;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.model.DescriptionDefinition;
import org.apache.camel.model.OtherwiseDefinition;
import org.apache.camel.model.ToDefinition;
import org.apache.camel.model.WhenDefinition;
import org.apache.commons.io.IOUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.fusesource.ide.generator.model.Node;
import org.fusesource.ide.generator.model.Nodeset;
import org.fusesource.ide.generator.velocity.Slf4jLogChute;
import org.fusesource.scalate.introspector.BeanProperty;
import org.fusesource.scalate.introspector.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Generator {
public static Logger LOG = LoggerFactory.getLogger(Generator.class);
public static String defaultSourceDir = "src/main/webapp/stencilsets/camel";
public static String defaultOutputDir = defaultSourceDir;
public static boolean eclipseMode = false;
private static String SEPARATOR = "\n";
public static Class[] XML_ANNOTATIONS = new Class[] {
XmlAttribute.class, XmlElement.class, XmlElementRef.class, XmlElements.class
};
public static Set<Class<?>> ignoreClasses = new HashSet<Class<?>>(Arrays.asList(ToDefinition.class));
private String outputDir = defaultOutputDir;
private String sourceDir = defaultSourceDir;
public Set<NodeDefinition<?>> nodeDefinitions = loadModelTypes();
public Set<NodeDefinition<?>> baseClassAndNestedClasses = loadBaseClassAndNestedClasses();
public Set<NodeDefinition<?>> nodeDefinitionsAndBaseClasses = new HashSet<>();
private boolean debug = false;
private VelocityEngine engine;
private CamelContext camelContext = new DefaultCamelContext();
private Map<String, NodeDefinition<?>> nodeDefinitionMap = new HashMap<>();
private Map<Class<?>, NodeDefinition<?>> nodeDefinitionClassMap = new HashMap<>();
private NodeDefinition toDefinition = createNodeDefinition("ToDefinition");
private File dir = new File(outputDir);
private File srcDir = new File(sourceDir);
private String[] imageExtensions = new String[] { "png", "gif", "jpg", "jpeg" };
private String eclipseIconDir = "../../../../../../../core/plugins/org.fusesource.ide.camel.model/icons/";
private Nodeset xmlModel;
public Generator(String outputDir, String sourceDir) {
this.outputDir = outputDir;
this.sourceDir = sourceDir;
for (NodeDefinition<?> nd : nodeDefinitions) {
nodeDefinitionMap.put(nd.getId(), nd);
nodeDefinitionClassMap.put(nd.getClazz(), nd);
}
nodeDefinitionsAndBaseClasses.addAll(nodeDefinitions);
nodeDefinitionsAndBaseClasses.addAll(baseClassAndNestedClasses);
engine = new VelocityEngine();
engine.setProperty(VelocityEngine.INPUT_ENCODING, "UTF-8");
engine.setProperty(VelocityEngine.OUTPUT_ENCODING, "UTF-8");
engine.setProperty(VelocityEngine.RUNTIME_LOG_LOGSYSTEM_CLASS, Slf4jLogChute.class.getName());
engine.setProperty(VelocityEngine.RESOURCE_LOADER, "class");
engine.setProperty("class.resource.loader.class", ClasspathResourceLoader.class.getName());
engine.init();
}
public void generateCamelDescriptionElements(File file) {
String text = findDescriptionNodes();
file.getParentFile().mkdirs();
writeText(file, text);
}
public void generateEclipseModel(String outputDir) throws IOException {
eclipseMode = true;
Thread.currentThread().setContextClassLoader(classLoader());
LOG.info("Generating files to {}", outputDir);
new File(outputDir).mkdirs();
int[] codes = new int[SEPARATOR.length()];
int idx = 0;
for (char c : SEPARATOR.toCharArray()) {
codes[idx++] = (int) c;
}
LOG.info("Separator used is length " + SEPARATOR.length() + " codes: " + Arrays.asList(codes));
String srcDir = "org/fusesource/ide/generator/eclipse";
render("ComplexProperties.txt", outputDir, srcDir);
render("tooltips.properties", new File(outputDir + "/../l10n").getCanonicalPath(), srcDir);
render("Tooltips.java", outputDir, srcDir);
render("messages.properties", new File(outputDir + "/../l10n").getCanonicalPath(), srcDir);
render("Messages.java", outputDir, srcDir);
render("NodeFactory.java", outputDir, srcDir);
// lets load our templates up first to test any errors
String uri = srcDir + "/ModelBean.java.vm";
engine.getTemplate(uri);
List<String> errors = new LinkedList<>();
for (NodeDefinition<?> node: nodeDefinitions) {
if (debug) {
LOG.info(node.getDefinitionName());
for (Property<?> p: node.simpleProperties()) {
LOG.info(" simple: " + p.label() + " " + javaScriptType(p));
}
for (Property<?> p: node.complexProperties()) {
LOG.info(" complex: " + p.label() + " " + p.propertyType().getName());
}
}
try {
VelocityContext context = new VelocityContext();
context.put("generator", this);
context.put("node", node);
if (node.getName().equals("RouteDefinition")) {
context.put("isRoute", Boolean.TRUE);
context.put("baseClass", "RouteSupport");
} else {
context.put("isRoute", Boolean.FALSE);
context.put("baseClass", "AbstractNode");
}
Set<String> propertyTypes = new LinkedHashSet<>();
for (Property<?> p : node.beanProperties()) {
String pn = p.propertyType().getCanonicalName();
if (!pn.startsWith("java.lang") && pn.contains(".")) {
propertyTypes.add(pn);
}
}
propertyTypes.add("org.apache.camel.model." + node.getName());
propertyTypes.add("org.apache.camel.model.language.ExpressionDefinition");
context.put("importedTypes", propertyTypes);
StringWriter sw = new StringWriter();
engine.getTemplate(uri).merge(context, sw);
String answer = sw.toString();
String outFile = outputDir + "/" + node.getDefinitionName() + ".java";
LOG.info("Generating file: {}", outFile);
writeText(outFile, answer);
}
catch (Exception e) {
LOG.error("Failed to compile " + uri + ": " + e.getMessage(), e);
}
}
if (!errors.isEmpty()) {
LOG.warn("add to NodeDefinition.documentationFile method:");
for (String e: errors) {
LOG.warn(" - " + e);
}
}
}
public void generateEclipseEditor(String outputDir) {
eclipseMode = true;
Thread.currentThread().setContextClassLoader(classLoader());
LOG.info("Generating files to {}", outputDir);
new File(outputDir).mkdirs();
String srcDir = "org/fusesource/ide/generator/eclipse/editor";
String[] templates = new String[] {
"provider/generated/ProviderHelper.java",
"provider/generated/AddNodeMenuFactory.java",
"l10n/messages.properties",
"Messages.java"
};
for (String t: templates) {
render(t, outputDir, srcDir);
}
}
public void run() {
Thread.currentThread().setContextClassLoader(classLoader());
if (debug) {
for (NodeDefinition<?> n: nodeDefinitions) {
LOG.debug(n.getName());
for (Property<?> p: n.simpleProperties()) {
LOG.info(" simple: " + p.label() + " " + javaScriptType(p));
}
for (Property<?> p: n.complexProperties()) {
LOG.info(" complex: " + p.label() + " " + p.propertyType().getName());
}
}
}
render("Dimensions.scala", "src/main/scala/org/fusesource/ide/generator");
String[] uris = new String[] { "camel.json" };
LOG.info("Generating files to {}", outputDir);
new File(outputDir).mkdirs();
for (String u: uris) {
render(u, outputDir);
}
}
public void generateHawtIO(String outputDir) throws FileNotFoundException {
String[] uris = new String[] { "camelModel.json" };
LOG.info("Generating files to {}", outputDir);
new File(outputDir).mkdirs();
for (String u: uris) {
render(u, outputDir, "src/main/resources/org/fusesource/ide/generator/hawtio");
// now lets parse and pretty print the JSON
File file = new File(outputDir, u);
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
JsonParser jp = new JsonParser();
JsonElement je = jp.parse(new FileReader(file));
String prettyJsonString = gson.toJson(je);
writeText(new File(outputDir, "camelModel.js"), "var _apacheCamelModel = " + prettyJsonString + ";");
LOG.info("Pretty printed JSON {}", file);
}
}
private void render(String u, String outputDir) {
render(u, outputDir, "src/main/resources/org/fusesource/ide/generator");
}
private void render(String u, String outputDir, String srcDir) {
render(u, outputDir, srcDir, ".vm");
}
private void render(String u, String outputDir, String srcDir, String extension) {
VelocityContext context = new VelocityContext();
context.put("generator", this);
String uri = srcDir + "/" + u + extension;
LOG.info("rendering {}", uri);
try {
// lets make sure we don't have a compile error
Template template = engine.getTemplate(uri);
StringWriter sw = new StringWriter();
template.merge(context, sw);
String answer = sw.toString();
String outFile = outputDir + "/" + u;
LOG.info("Generating file: {}", outFile);
writeText(outFile, answer);
}
catch (Exception e) {
LOG.error("Failed to compile " + uri + " " + e.getMessage(), e);
/*
for (err <- e.errors) {
println(err.message + " at " + err.pos + " " + err.original.message)
}
*/
}
}
protected Set<NodeDefinition<?>> loadBaseClassAndNestedClasses() {
Set<Class<?>> classes = new LinkedHashSet<>();
classes.add(DescriptionDefinition.class);
Set<String> ignoredTypeNames = new HashSet<>(Arrays.asList(
"java.util.List",
"java.lang.String",
"org.apache.camel.model.language.Expression"
));
for (NodeDefinition<?> node: nodeDefinitions) {
for (Property<?> prop: node.beanProperties()) {
Class<?> propType = prop.propertyType();
if (!propType.isPrimitive() && node.isSimplePropertyType(prop) &&
!ignoredTypeNames.contains(propType.getName())) {
classes.add(propType);
}
Class<?> aClass = elementTypeClass(prop);
if (aClass != null) {
classes.add(aClass);
}
XmlElement[] elements = node.xmlElements(prop);
for (XmlElement el: elements) {
classes.add(el.type());
}
}
}
Set<NodeDefinition<?>> result = new LinkedHashSet<>();
for (Class<?> c : classes) {
result.add(new NodeDefinition<>(c.getName(), c, this));
}
return result;
}
private Class<?> elementTypeClass(Property<?> prop) {
if (isJavaCollection(prop)) {
if (prop instanceof FieldProperty) {
FieldProperty fieldProp = (FieldProperty)prop;
Field fld = fieldProp.getField();
return elementTypeClass(fld.getGenericType());
}
if (prop instanceof BeanProperty) {
BeanProperty beanProp = (BeanProperty)prop;
Method readMethod = beanProp.descriptor().getReadMethod();
if (readMethod != null) {
return elementTypeClass(readMethod.getGenericReturnType());
}
}
}
return null;
}
private Class<?> elementTypeClass(Type retType) {
if (retType != null && retType instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType)retType;
Type[] arguments = paramType.getActualTypeArguments();
if (arguments != null && arguments.length > 0) {
if (arguments[0] instanceof Class) {
return (Class<?>) arguments[0];
} else {
return null;
}
}
}
return null;
}
protected Set<NodeDefinition<?>> loadModelTypes() {
String[] classNames = loadStrings("org/apache/camel/model/jaxb.index");
Set<NodeDefinition<?>> set = new LinkedHashSet<>();
try {
for (String className : classNames) {
NodeDefinition<?> n = createNodeDefinition(className);
if (n.isProcessor()) {
set.add(n);
}
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
return set;
}
protected String[] loadStrings(String uri) {
URL url = classLoader().getResource(uri);
if (url == null) {
throw new IllegalArgumentException("Cannot find resource " + uri + "!");
}
List<String> result = new LinkedList<>();
StringWriter writer = new StringWriter();
try {
InputStream is = url.openStream();
IOUtils.copy(is, writer);
IOUtils.closeQuietly(is);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
String text = writer.toString();
for (String line : text.split("\n")) {
if (line.length() > 0 && !line.startsWith("#")) {
result.add(line);
}
}
return result.toArray(new String[result.size()]);
}
protected ClassLoader classLoader() {
return getClass().getClassLoader();
}
public String findDescriptionNodes() {
StringBuilder builder = new StringBuilder();
for (NodeDefinition<?> node: nodeDefinitions) {
if (node.getIntrospector().propertyMap().contains("description") && node.getElementName() != null) {
builder.append(node.getElementName());
builder.append(" ");
}
}
return builder.toString().trim();
}
private void writeText(String outFile, String answer) {
writeText(new File(outFile), answer);
}
private void writeText(File outFile, String answer) {
try {
FileOutputStream os = new FileOutputStream(outFile);
IOUtils.write(convertNewlines(answer), os);
IOUtils.closeQuietly(os);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
protected String convertNewlines(String answer) {
String[] lines = answer.split("\n");
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line).append(SEPARATOR);
}
return sb.toString();
}
protected NodeDefinition<?> createNodeDefinition(String n) {
try {
return new NodeDefinition<>(n, classLoader().loadClass("org.apache.camel.model." + n), this);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public Map<Class<?>, NodeDefinition<?>> getNodeDefinitionClassMap() {
return nodeDefinitionClassMap;
}
public Set<NodeDefinition<?>> nodeDefinitions() {
return nodeDefinitions;
}
/**
* Returns the JavaScript type we should use for editing the property
* @param prop
* @return
*/
private String javaScriptType(Property<?> prop) {
if (prop.propertyType().isArray() || isJavaCollection(prop)) {
return "array";
}
String number = "number";
String string = "string";
String bool = "bool";
switch (prop.propertyType().getName()) {
case "java.lang.Byte": return number;
case "java.lang.Short": return number;
case "java.lang.Integer": return number;
case "java.lang.Long": return number;
case "java.lang.Float": return number;
case "java.lang.Double": return number;
case "byte": return number;
case "short": return number;
case "int": return number;
case "long": return number;
case "float": return number;
case "double": return number;
case "java.util.Date": return string;
case "java.lang.String": return string;
case "boolean": return bool;
case "java.lang.Boolean": return bool;
default:
return prop.propertyType().getName();
}
}
private boolean isJavaCollection(Property prop) {
switch (prop.propertyType().getName()) {
case "java.util.List":
return true;
case "java.util.Set":
return true;
case "java.util.Collection":
return true;
case "java.lang.Iterable":
return true;
default:
return false;
}
}
public Nodeset getXmlModel() {
if (xmlModel == null) {
try {
JAXBContext context = JAXBContext.newInstance(Nodeset.class);
xmlModel = (Nodeset) context.createUnmarshaller().unmarshal(new StreamSource(getClass().getResourceAsStream("/org/fusesource/ide/generator/model.xml")));
} catch (JAXBException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
return xmlModel;
}
/* For Velocity */
public boolean isValid(NodeDefinition<?> n, Property<?> p) {
return !WhenDefinition.class.isAssignableFrom(p.propertyType()) && !OtherwiseDefinition.class.isAssignableFrom(p.propertyType()) && !n.isBeanProperty(p);
}
// XML helpers
// public static String childElemText(Node on, String name, String defaultValue) {
// if (on == null) {
// return defaultValue;
// } else {
// String t = n\name
// if (t.isEmpty)
// defaultValue
// else
// t.text
// }
// }
public static Node findElemById(List<Node> nodes, String id) {
for (Node n : nodes) {
if (n.id != null && n.id.equals(id)) {
return n;
}
}
return null;
}
public String findIconFileOrElse(String childDir, String name, String elseName) throws IOException {
return findIconFileOrElse(childDir, name, imageExtensions, elseName);
}
/**
* Returns the name if the file exists inside the childDir directory of the outputDir otherwise
* return the elseName
* @param childDir
* @param name
* @param extensions
* @param elseName
* @return
*/
public String findIconFileOrElse(String childDir, String name, String[] extensions, String elseName) throws IOException {
File subDir = new File(new File(srcDir, childDir).getCanonicalPath());
if (!subDir.exists()) {
LOG.warn("Icon dir " + subDir.getCanonicalPath() + " does not exist!");
}
for (String e : extensions) {
File f = new File(subDir, name + "." + e);
if (f.exists()) {
return f.getName();
}
}
return elseName;
}
public String getEclipseIconDir() {
return eclipseIconDir;
}
}
/*
object Generator {
//val separator = System.getProperty("line.separator", "\r\n")
private val singleton = new Generator
def nodeDefinition(value: AnyRef) = {
val clazz = value.getClass
new NodeDefinition(clazz.getSimpleName, clazz, singleton)
}
}
import Generator._
import Reflections._
object Dimension {
val doesNotExist = Dimension("doesNotExist.svg", 58.5, 36)
}
class Generator() extends Logging {
/ * *
* Make it easy to add a comma between values while iterating
* /
def comma[T](iter: Iterable[T])(fn: T => Any): Unit = {
var first = true
for (t <- iter) {
if (first)
first = false
else
RenderContext() << ", "
fn(t)
}
}
def toId(name: String): String = {
val idx = name.lastIndexOf("Definition")
val definitionName = if (idx > 0) name.substring(0, idx) else name
return (new Strings()).decapitalize(definitionName)
}
def isProcessor(clazz: Class[_]) = !Modifier.isAbstract(clazz.getModifiers) &&
classOf[ProcessorDefinition[_]].isAssignableFrom(clazz) &&
!ignoreClasses.contains(clazz)
def elementTypeId(prop: Property[_]): Option[String] = {
elementTypeClass(prop) match {
case Some(clazz) => Some(if (isProcessor(clazz)) {
toId(clazz.getSimpleName)
} else {
clazz.getName
})
case _ => None
}
}
def elementType(prop: Property[_]): Option[String] = {
return elementTypeClass(prop) match {
case Some(aClass) => Some(aClass.getName)
case _ => None
}
}
def isExpression(prop: Property[_]) = classOf[ExpressionDefinition].isAssignableFrom(prop.propertyType)
def wrapLines(prop: Property[_]) = prop.name == "description"
def findNodeDimensions: Seq[Dimension] = {
new File(srcDir, "view").listFiles.filter(_.getName.matches("""node\..+\.svg""")).map{
f =>
val doc = XML.loadFile(f)
var width = -1.0
var height = -1.0
for (r <- doc \\ "rect") {
width = max(width, attributeDoubleValue(r, "width"))
height = max(height, attributeDoubleValue(r, "height"))
}
val d = Dimension(f.getName, width, height)
println("found: " + d)
d
}
}
/ * *
* Returns the double value of the given attribute or return the default value if none is provided
* /
def attributeDoubleValue(e: Node, name: String, defaultValue: Double = -1): Double = {
e.attribute(name) match {
case Some(s) =>
if (s.isEmpty) {
defaultValue
}
else {
s.head.text.toDouble
}
case _ => defaultValue
}
}
}
*/