Repository: camel Updated Branches: refs/heads/master 9ffb2549a -> 749b2d797
http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelJavaTreeParserHelper.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelJavaTreeParserHelper.java b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelJavaTreeParserHelper.java new file mode 100644 index 0000000..774831f --- /dev/null +++ b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelJavaTreeParserHelper.java @@ -0,0 +1,478 @@ +/** + * 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.camel.parser.helper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.catalog.JSonSchemaHelper; +import org.apache.camel.parser.model.CamelNodeDetails; +import org.apache.camel.parser.model.CamelNodeDetailsFactory; +import org.apache.camel.parser.roaster.StatementFieldSource; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AnonymousClassDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Block; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.BooleanLiteral; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Expression; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ExpressionStatement; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.FieldDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.InfixExpression; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MemberValuePair; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodInvocation; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.NormalAnnotation; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.NumberLiteral; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ParenthesizedExpression; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.QualifiedName; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.SimpleName; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.StringLiteral; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Type; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.VariableDeclarationStatement; +import org.jboss.forge.roaster.model.Annotation; +import org.jboss.forge.roaster.model.source.FieldSource; +import org.jboss.forge.roaster.model.source.JavaClassSource; +import org.jboss.forge.roaster.model.source.MethodSource; + +/** + * A Camel Java tree parser that only depends on the Roaster API. + * <p/> + * This implement is used for parsing the Camel routes and build a tree structure of the EIP nodes. + * + * @see CamelJavaParserHelper for parser that can discover endpoints and simple expressions + */ +public final class CamelJavaTreeParserHelper { + + private final CamelCatalog camelCatalog = new DefaultCamelCatalog(true); + + public List<CamelNodeDetails> parseCamelRouteTree(JavaClassSource clazz, String baseDir, String fullyQualifiedFileName, + MethodSource<JavaClassSource> configureMethod) { + + // find any from which is the start of the route + CamelNodeDetailsFactory nodeFactory = CamelNodeDetailsFactory.newInstance(); + + CamelNodeDetails route = nodeFactory.newNode(null, "route"); + + if (configureMethod != null) { + MethodDeclaration md = (MethodDeclaration) configureMethod.getInternal(); + Block block = md.getBody(); + if (block != null) { + for (Object statement : md.getBody().statements()) { + // must be a method call expression + if (statement instanceof ExpressionStatement) { + ExpressionStatement es = (ExpressionStatement) statement; + Expression exp = es.getExpression(); + parseExpression(nodeFactory, fullyQualifiedFileName, clazz, configureMethod, block, exp, route); + } + } + } + } + + List<CamelNodeDetails> answer = new ArrayList<>(); + + if (route.getOutputs().isEmpty()) { + // okay no routes found + return answer; + } + + // now parse the route node and build the correct model/tree structure of the EIPs + + // re-create factory as we rebuild the tree + nodeFactory = CamelNodeDetailsFactory.newInstance(); + CamelNodeDetails parent = route.getOutputs().get(0); + + for (int i = 0; i < route.getOutputs().size(); i++) { + CamelNodeDetails node = route.getOutputs().get(i); + String name = node.getName(); + + if ("from".equals(name)) { + CamelNodeDetails from = nodeFactory.copyNode(null, "from", node); + from.setFileName(fullyQualifiedFileName); + answer.add(from); + parent = from; + } else if ("routeId".equals(name)) { + // should be set on the parent + parent.setRouteId(node.getRouteId()); + } else if ("end".equals(name) || "endChoice".equals(name) || "endParent".equals(name) || "endRest".equals(name) + || "endDoTry".equals(name) || "endHystrix".equals(name)) { + // parent should be grand parent + parent = parent.getParent(); + } else if ("choice".equals(name)) { + // special for some EIPs + CamelNodeDetails output = nodeFactory.copyNode(parent, name, node); + parent.addOutput(output); + parent = output; + } else if ("when".equals(name) || "otherwise".equals(name)) { + // we are in a choice block so parent should be the first choice up the parent tree + while (!parent.getName().equals("from") && !"choice".equals(parent.getName())) { + parent = parent.getParent(); + } + } else { + boolean hasOutput = hasOutput(name); + if (hasOutput) { + // has output so add as new child node + CamelNodeDetails output = nodeFactory.copyNode(parent, name, node); + parent.addOutput(output); + parent = output; + } else { + // add straight to itself + CamelNodeDetails output = nodeFactory.copyNode(parent, name, node); + parent.addOutput(output); + } + } + } + + return answer; + } + + private boolean hasOutput(String name) { + String json = camelCatalog.modelJSonSchema(name); + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("model", json, false); + return isModelOutput(rows); + } + + private static boolean isModelOutput(List<Map<String, String>> rows) { + for (Map<String, String> row : rows) { + if (row.containsKey("output")) { + return "true".equals(row.get("output")); + } + } + return false; + } + + private boolean hasInput(String name) { + String json = camelCatalog.modelJSonSchema(name); + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("model", json, false); + return isModelInput(rows); + } + + private static boolean isModelInput(List<Map<String, String>> rows) { + for (Map<String, String> row : rows) { + if (row.containsKey("input")) { + return "true".equals(row.get("input")); + } + } + return false; + } + + private static CamelNodeDetails grandParent(CamelNodeDetails node, String parentName) { + if (node == null) { + return null; + } + if (parentName.equals(node.getName())) { + return node; + } else { + return grandParent(node.getParent(), parentName); + } + } + + private void parseExpression(CamelNodeDetailsFactory nodeFactory, String fullyQualifiedFileName, + JavaClassSource clazz, MethodSource<JavaClassSource> configureMethod, Block block, + Expression exp, CamelNodeDetails node) { + if (exp == null) { + return; + } + if (exp instanceof MethodInvocation) { + MethodInvocation mi = (MethodInvocation) exp; + node = doParseCamelModels(nodeFactory, fullyQualifiedFileName, clazz, configureMethod, block, mi, node); + // if the method was called on another method, then recursive + exp = mi.getExpression(); + parseExpression(nodeFactory, fullyQualifiedFileName, clazz, configureMethod, block, exp, node); + } + } + + private CamelNodeDetails doParseCamelModels(CamelNodeDetailsFactory nodeFactory, String fullyQualifiedFileName, + JavaClassSource clazz, MethodSource<JavaClassSource> configureMethod, Block block, + MethodInvocation mi, CamelNodeDetails node) { + String name = mi.getName().getIdentifier(); + + // special for Java DSL having some endXXX + boolean isEnd = "end".equals(name) || "endChoice".equals(name) || "endDoTry".equals(name) || "endHystrix".equals(name) || "endParent".equals(name) || "endRest".equals(name); + boolean isRoute = "route".equals(name) || "from".equals(name) || "routeId".equals(name); + // must be an eip model that has either input or output as we only want to track processors (also accept from) + boolean isEip = camelCatalog.findModelNames().contains(name) && (hasInput(name) || hasOutput(name)); + + // only include if its a known Camel model (dont include languages) + if (isEnd || isRoute || isEip) { + CamelNodeDetails newNode = nodeFactory.newNode(node, name); + + // include source code details + int pos = mi.getName().getStartPosition(); + int line = findLineNumber(fullyQualifiedFileName, pos); + if (line > -1) { + newNode.setLineNumber("" + line); + } + newNode.setFileName(fullyQualifiedFileName); + + newNode.setClassName(clazz.getQualifiedName()); + newNode.setMethodName(configureMethod.getName()); + + if ("routeId".equals(name)) { + // grab the route id + List args = mi.arguments(); + if (args != null && args.size() > 0) { + // the first argument has the route id + Expression exp = (Expression) args.get(0); + String routeId = getLiteralValue(clazz, block, exp); + if (routeId != null) { + newNode.setRouteId(routeId); + } + } + } + + node.addPreliminaryOutput(newNode); + return node; + } + + return node; + } + + @SuppressWarnings("unchecked") + private static FieldSource<JavaClassSource> getField(JavaClassSource clazz, Block block, SimpleName ref) { + String fieldName = ref.getIdentifier(); + if (fieldName != null) { + // find field in class + FieldSource field = clazz != null ? clazz.getField(fieldName) : null; + if (field == null) { + field = findFieldInBlock(clazz, block, fieldName); + } + return field; + } + return null; + } + + @SuppressWarnings("unchecked") + private static FieldSource<JavaClassSource> findFieldInBlock(JavaClassSource clazz, Block block, String fieldName) { + for (Object statement : block.statements()) { + // try local statements first in the block + if (statement instanceof VariableDeclarationStatement) { + final Type type = ((VariableDeclarationStatement) statement).getType(); + for (Object obj : ((VariableDeclarationStatement) statement).fragments()) { + if (obj instanceof VariableDeclarationFragment) { + VariableDeclarationFragment fragment = (VariableDeclarationFragment) obj; + SimpleName name = fragment.getName(); + if (name != null && fieldName.equals(name.getIdentifier())) { + return new StatementFieldSource(clazz, fragment, type); + } + } + } + } + + // okay the field may be burried inside an anonymous inner class as a field declaration + // outside the configure method, so lets go back to the parent and see what we can find + ASTNode node = block.getParent(); + if (node instanceof MethodDeclaration) { + node = node.getParent(); + } + if (node instanceof AnonymousClassDeclaration) { + List declarations = ((AnonymousClassDeclaration) node).bodyDeclarations(); + for (Object dec : declarations) { + if (dec instanceof FieldDeclaration) { + FieldDeclaration fd = (FieldDeclaration) dec; + final Type type = fd.getType(); + for (Object obj : fd.fragments()) { + if (obj instanceof VariableDeclarationFragment) { + VariableDeclarationFragment fragment = (VariableDeclarationFragment) obj; + SimpleName name = fragment.getName(); + if (name != null && fieldName.equals(name.getIdentifier())) { + return new StatementFieldSource(clazz, fragment, type); + } + } + } + } + } + } + } + return null; + } + + /** + * @deprecated currently not in use + */ + @Deprecated + public static String getLiteralValue(JavaClassSource clazz, Block block, Expression expression) { + // unwrap parenthesis + if (expression instanceof ParenthesizedExpression) { + expression = ((ParenthesizedExpression) expression).getExpression(); + } + + if (expression instanceof StringLiteral) { + return ((StringLiteral) expression).getLiteralValue(); + } else if (expression instanceof BooleanLiteral) { + return "" + ((BooleanLiteral) expression).booleanValue(); + } else if (expression instanceof NumberLiteral) { + return ((NumberLiteral) expression).getToken(); + } + + // if it a method invocation then add a dummy value assuming the method invocation will return a valid response + if (expression instanceof MethodInvocation) { + String name = ((MethodInvocation) expression).getName().getIdentifier(); + return "{{" + name + "}}"; + } + + // if its a qualified name (usually a constant field in another class) + // then add a dummy value as we cannot find the field value in other classes and maybe even outside the + // source code we have access to + if (expression instanceof QualifiedName) { + QualifiedName qn = (QualifiedName) expression; + String name = qn.getFullyQualifiedName(); + return "{{" + name + "}}"; + } + + if (expression instanceof SimpleName) { + FieldSource<JavaClassSource> field = getField(clazz, block, (SimpleName) expression); + if (field != null) { + // is the field annotated with a Camel endpoint + if (field.getAnnotations() != null) { + for (Annotation ann : field.getAnnotations()) { + boolean valid = "org.apache.camel.EndpointInject".equals(ann.getQualifiedName()) || "org.apache.camel.cdi.Uri".equals(ann.getQualifiedName()); + if (valid) { + Expression exp = (Expression) ann.getInternal(); + if (exp instanceof SingleMemberAnnotation) { + exp = ((SingleMemberAnnotation) exp).getValue(); + } else if (exp instanceof NormalAnnotation) { + List values = ((NormalAnnotation) exp).values(); + for (Object value : values) { + MemberValuePair pair = (MemberValuePair) value; + if ("uri".equals(pair.getName().toString())) { + exp = pair.getValue(); + break; + } + } + } + if (exp != null) { + return getLiteralValue(clazz, block, exp); + } + } + } + } + // is the field an org.apache.camel.Endpoint type? + if ("Endpoint".equals(field.getType().getSimpleName())) { + // then grab the uri from the first argument + VariableDeclarationFragment vdf = (VariableDeclarationFragment) field.getInternal(); + expression = vdf.getInitializer(); + if (expression instanceof MethodInvocation) { + MethodInvocation mi = (MethodInvocation) expression; + List args = mi.arguments(); + if (args != null && args.size() > 0) { + // the first argument has the endpoint uri + expression = (Expression) args.get(0); + return getLiteralValue(clazz, block, expression); + } + } + } else { + // no annotations so try its initializer + VariableDeclarationFragment vdf = (VariableDeclarationFragment) field.getInternal(); + expression = vdf.getInitializer(); + if (expression == null) { + // its a field which has no initializer, then add a dummy value assuming the field will be initialized at runtime + return "{{" + field.getName() + "}}"; + } else { + return getLiteralValue(clazz, block, expression); + } + } + } else { + // we could not find the field in this class/method, so its maybe from some other super class, so insert a dummy value + final String fieldName = ((SimpleName) expression).getIdentifier(); + return "{{" + fieldName + "}}"; + } + } else if (expression instanceof InfixExpression) { + String answer = null; + // is it a string that is concat together? + InfixExpression ie = (InfixExpression) expression; + if (InfixExpression.Operator.PLUS.equals(ie.getOperator())) { + + String val1 = getLiteralValue(clazz, block, ie.getLeftOperand()); + String val2 = getLiteralValue(clazz, block, ie.getRightOperand()); + + // if numeric then we plus the values, otherwise we string concat + boolean numeric = isNumericOperator(clazz, block, ie.getLeftOperand()) && isNumericOperator(clazz, block, ie.getRightOperand()); + if (numeric) { + Long num1 = val1 != null ? Long.valueOf(val1) : 0; + Long num2 = val2 != null ? Long.valueOf(val2) : 0; + answer = "" + (num1 + num2); + } else { + answer = (val1 != null ? val1 : "") + (val2 != null ? val2 : ""); + } + + if (!answer.isEmpty()) { + // include extended when we concat on 2 or more lines + List extended = ie.extendedOperands(); + if (extended != null) { + for (Object ext : extended) { + String val3 = getLiteralValue(clazz, block, (Expression) ext); + if (numeric) { + Long num3 = val3 != null ? Long.valueOf(val3) : 0; + Long num = Long.valueOf(answer); + answer = "" + (num + num3); + } else { + answer += val3 != null ? val3 : ""; + } + } + } + } + } + return answer; + } + + return null; + } + + private static boolean isNumericOperator(JavaClassSource clazz, Block block, Expression expression) { + if (expression instanceof NumberLiteral) { + return true; + } else if (expression instanceof SimpleName) { + FieldSource field = getField(clazz, block, (SimpleName) expression); + if (field != null) { + return field.getType().isType("int") || field.getType().isType("long") + || field.getType().isType("Integer") || field.getType().isType("Long"); + } + } + return false; + } + + private static int findLineNumber(String fullyQualifiedFileName, int position) { + int lines = 0; + + try { + int current = 0; + try (BufferedReader br = new BufferedReader(new FileReader(new File(fullyQualifiedFileName)))) { + String line; + while ((line = br.readLine()) != null) { + lines++; + current += line.length() + 1; // add 1 for line feed + if (current >= position) { + return lines; + } + } + } + } catch (Exception e) { + // ignore + return -1; + } + + return lines; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelXmlTreeParserHelper.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelXmlTreeParserHelper.java b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelXmlTreeParserHelper.java new file mode 100644 index 0000000..25396ba --- /dev/null +++ b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/CamelXmlTreeParserHelper.java @@ -0,0 +1,134 @@ +/** + * 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.camel.parser.helper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.catalog.JSonSchemaHelper; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.apache.camel.parser.model.CamelNodeDetails; +import org.apache.camel.parser.model.CamelNodeDetailsFactory; + +public final class CamelXmlTreeParserHelper { + + private final CamelCatalog camelCatalog = new DefaultCamelCatalog(true); + + public List<CamelNodeDetails> parseCamelRouteTree(Node xmlNode, String routeId, CamelNodeDetails route, + String baseDir, String fullyQualifiedFileName) { + + CamelNodeDetailsFactory nodeFactory = CamelNodeDetailsFactory.newInstance(); + List<CamelNodeDetails> answer = new ArrayList<>(); + + walkXmlTree(nodeFactory, xmlNode, route); + + // now parse the route node and build the correct model/tree structure of the EIPs + // re-create factory as we rebuild the tree + nodeFactory = CamelNodeDetailsFactory.newInstance(); + CamelNodeDetails parent = route.getOutputs().get(0); + + // we dont want the route element and only start with from + for (int i = 0; i < route.getOutputs().size(); i++) { + CamelNodeDetails node = route.getOutputs().get(i); + String name = node.getName(); + + if ("from".equals(name)) { + CamelNodeDetails from = nodeFactory.copyNode(null, "from", node); + from.setFileName(fullyQualifiedFileName); + answer.add(from); + parent = from; + } else { + // add straight to parent + parent.addOutput(node); + node.setFileName(fullyQualifiedFileName); + } + } + + return answer; + } + + private void walkXmlTree(CamelNodeDetailsFactory nodeFactory, Node node, CamelNodeDetails parent) { + CamelNodeDetails newNode = null; + + String name = node.getNodeName(); + + boolean isRoute = "route".equals(name) || "from".equals(name); + // must be an eip model that has either input or output as we only want to track processors (also accept from) + boolean isEip = camelCatalog.findModelNames().contains(name) && (hasInput(name) || hasOutput(name)); + + // only include if its a known Camel model (dont include languages) + if (isRoute || isEip) { + // skip route as we just keep from + if (!"route".equals(name)) { + String lineNumber = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER); + String lineNumberEnd = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER_END); + newNode = nodeFactory.newNode(parent, name); + newNode.setRouteId(parent.getRouteId()); + newNode.setFileName(parent.getFileName()); + newNode.setLineNumber(lineNumber); + newNode.setLineNumberEnd(lineNumberEnd); + + parent.addOutput(newNode); + } + } + + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + walkXmlTree(nodeFactory, child, newNode != null ? newNode : parent); + } + } + + } + + private boolean hasOutput(String name) { + String json = camelCatalog.modelJSonSchema(name); + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("model", json, false); + return isModelOutput(rows); + } + + private static boolean isModelOutput(List<Map<String, String>> rows) { + for (Map<String, String> row : rows) { + if (row.containsKey("output")) { + return "true".equals(row.get("output")); + } + } + return false; + } + + private boolean hasInput(String name) { + String json = camelCatalog.modelJSonSchema(name); + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("model", json, false); + return isModelInput(rows); + } + + private static boolean isModelInput(List<Map<String, String>> rows) { + for (Map<String, String> row : rows) { + if (row.containsKey("input")) { + return "true".equals(row.get("input")); + } + } + return false; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java new file mode 100644 index 0000000..2bc4bf8 --- /dev/null +++ b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/helper/RouteCoverageHelper.java @@ -0,0 +1,115 @@ +/** + * 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.camel.parser.helper; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.parser.model.CoverageData; + +/** + * Helper to provide route coverage details. + */ +public final class RouteCoverageHelper { + + private RouteCoverageHelper() { + } + + public static List<CoverageData> parseDumpRouteCoverageByRouteId(String directory, String routeId) throws Exception { + List<CoverageData> answer = new ArrayList<>(); + + File[] files = new File(directory).listFiles(f -> f.getName().endsWith(".xml")); + if (files == null) { + return answer; + } + + CamelCatalog catalog = new DefaultCamelCatalog(true); + + for (File file : files) { + try (FileInputStream fis = new FileInputStream(file)) { + Document dom = XmlLineNumberParser.parseXml(fis); + NodeList routes = dom.getElementsByTagName("route"); + for (int i = 0; i < routes.getLength(); i++) { + Node route = routes.item(i); + String id = route.getAttributes().getNamedItem("id").getNodeValue(); + // must be the target route + if (routeId.equals(id)) { + // parse each route and build a Map<String, Integer> with the no of messages processed + // where String is the EIP name + AtomicInteger counter = new AtomicInteger(); + parseRouteData(catalog, route, answer, counter); + } + } + } + } + + return answer; + } + + private static void parseRouteData(CamelCatalog catalog, Node node, List<CoverageData> data, AtomicInteger counter) { + // must be a known EIP model + String key = node.getNodeName(); + boolean valid = catalog.findModelNames().contains(key); // skip route as we use from instead + if (!valid) { + return; + } + + // only calculate for elements within the route + if (!"route".equals(key)) { + Integer count = 0; + Node total = node.getAttributes().getNamedItem("exchangesTotal"); + if (total != null) { + count = Integer.valueOf(total.getNodeValue()); + } + CoverageData holder = data.size() > counter.get() ? data.get(counter.get()) : null; + if (holder != null && holder.getNode().equals(key)) { + count += holder.getCount(); + } + if (holder == null) { + // add new + data.add(counter.get(), new CoverageData(key, count)); + } else { + // replace existing + data.set(counter.get(), new CoverageData(key, count)); + } + // advance counter + counter.incrementAndGet(); + } + + // any children + NodeList children = node.getChildNodes(); + if (children != null) { + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element) { + parseRouteData(catalog, child, data, counter); + } + } + } + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetails.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetails.java b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetails.java new file mode 100644 index 0000000..bd2a99c --- /dev/null +++ b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetails.java @@ -0,0 +1,165 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.camel.parser.model; + +import java.util.ArrayList; +import java.util.List; + +public class CamelNodeDetails { + + // source code details + private String fileName; + private String lineNumber; + private String lineNumberEnd; + + // java source code details + private String className; + private String methodName; + + // camel node details + private final CamelNodeDetails parent; + private final String name; + private final int order; + private List<CamelNodeDetails> outputs; + private String routeId; + + public CamelNodeDetails(CamelNodeDetails parent, String name, int order, CamelNodeDetails copy) { + this.parent = parent; + this.name = name; + this.order = order; + this.routeId = copy.getRouteId(); + this.fileName = copy.getFileName(); + this.lineNumber = copy.getLineNumber(); + this.lineNumberEnd = copy.getLineNumberEnd(); + this.className = copy.getClassName(); + this.methodName = copy.getMethodName(); + } + + public CamelNodeDetails(CamelNodeDetails parent, String name, int order) { + this.parent = parent; + this.name = name; + this.order = order; + } + + public void addPreliminaryOutput(CamelNodeDetails output) { + if (outputs == null) { + outputs = new ArrayList<>(); + } + // the parser walks the EIPs backwards so add from the top + outputs.add(0, output); + } + + public void addOutput(CamelNodeDetails output) { + if (outputs == null) { + outputs = new ArrayList<>(); + } + outputs.add(output); + } + + public CamelNodeDetails getParent() { + return parent; + } + + public String getName() { + return name; + } + + public int getOrder() { + return order; + } + + public List<CamelNodeDetails> getOutputs() { + return outputs; + } + + public String getRouteId() { + return routeId; + } + + public void setRouteId(String routeId) { + this.routeId = routeId; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getLineNumber() { + return lineNumber; + } + + public void setLineNumber(String lineNumber) { + this.lineNumber = lineNumber; + } + + public String getLineNumberEnd() { + return lineNumberEnd; + } + + public void setLineNumberEnd(String lineNumberEnd) { + this.lineNumberEnd = lineNumberEnd; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String toString() { + return name; + } + + public String dump(int level) { + StringBuilder sb = new StringBuilder(); + sb.append(lineNumber); + sb.append("\t"); + sb.append(padString(level)); + sb.append(name); + if (outputs != null) { + level++; + for (CamelNodeDetails child : outputs) { + String text = child.dump(level); + sb.append("\n"); + sb.append(text); + } + } + return sb.toString(); + } + + private static String padString(int level) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < level; i++) { + sb.append(" "); + } + return sb.toString(); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetailsFactory.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetailsFactory.java b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetailsFactory.java new file mode 100644 index 0000000..33b15c8 --- /dev/null +++ b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CamelNodeDetailsFactory.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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.camel.parser.model; + +public class CamelNodeDetailsFactory { + + private int order; + + private CamelNodeDetailsFactory() { + } + + public static CamelNodeDetailsFactory newInstance() { + return new CamelNodeDetailsFactory(); + } + + public CamelNodeDetails newNode(CamelNodeDetails parent, String name) { + return new CamelNodeDetails(parent, name, ++order); + } + + public CamelNodeDetails copyNode(CamelNodeDetails parent, String name, CamelNodeDetails copoy) { + return new CamelNodeDetails(parent, name, ++order, copoy); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java new file mode 100644 index 0000000..97eee37 --- /dev/null +++ b/tooling/camel-route-parser/src/main/java/org/apache/camel/parser/model/CoverageData.java @@ -0,0 +1,39 @@ +/** + * 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.camel.parser.model; + +/** + * Represents coverage data of a given node in the Camel route. + */ +public class CoverageData { + + private final String node; + private final int count; + + public CoverageData(String node, int count) { + this.count = count; + this.node = node; + } + + public String getNode() { + return node; + } + + public int getCount() { + return count; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java new file mode 100644 index 0000000..078c70b --- /dev/null +++ b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java @@ -0,0 +1,39 @@ +/** + * 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.camel.parser.java; + +import org.apache.camel.builder.RouteBuilder; + +public class MyJavaDslRouteBuilder extends RouteBuilder { + + @Override + public void configure() throws Exception { + from("direct:start").routeId("bar") + .log("I was here") + .setHeader("foo", constant("123")) + .choice() + .when(header("foo")) + .to("log:a") + .toD("log:a2") + .when().header("bar") + .toD("log:b") + .otherwise() + .log("none") + .end() + .to("mock:result"); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTest.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTest.java b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTest.java new file mode 100644 index 0000000..c6bc96b --- /dev/null +++ b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTest.java @@ -0,0 +1,77 @@ +/** + * 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.camel.parser.java; + +import java.io.File; +import java.util.List; + +import org.apache.camel.parser.RouteBuilderParser; +import org.apache.camel.parser.model.CamelNodeDetails; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.source.JavaClassSource; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RoasterJavaDslTest extends CamelTestSupport { + + private static final Logger LOG = LoggerFactory.getLogger(RoasterJavaDslTest.class); + + @Override + public boolean isDumpRouteCoverage() { + return true; + } + + @Test + public void parseTree() throws Exception { + JavaClassSource clazz = (JavaClassSource) Roaster.parse(new File("src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java")); + + List<CamelNodeDetails> list = RouteBuilderParser.parseRouteBuilderTree(clazz, ".", + "src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java",true); + assertEquals(1, list.size()); + CamelNodeDetails details = list.get(0); + assertEquals("src/test/java/org/apache/camel/parser/java/MyJavaDslRouteBuilder.java", details.getFileName()); + assertEquals("bar", details.getRouteId()); + assertEquals("configure", details.getMethodName()); + assertEquals("org.apache.camel.parser.java.MyJavaDslRouteBuilder", details.getClassName()); + + String tree = details.dump(0); + LOG.info("\n" + tree); + + assertTrue(tree.contains("25\tfrom")); + assertTrue(tree.contains("27\t setHeader")); + assertTrue(tree.contains("28\t choice")); + assertTrue(tree.contains("30\t to")); + assertTrue(tree.contains("31\t toD")); + assertTrue(tree.contains("33\t toD")); + assertTrue(tree.contains("35\t log")); + assertTrue(tree.contains("37\t to")); + } + + @Test + public void testRouteCoverage() throws Exception { + context.addRoutes(new MyJavaDslRouteBuilder()); + + getMockEndpoint("mock:result").expectedMessageCount(1); + + template.sendBody("direct:start", "Hello World"); + + assertMockEndpointsSatisfied(); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTwoRoutesTest.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTwoRoutesTest.java b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTwoRoutesTest.java new file mode 100644 index 0000000..b1b5a17 --- /dev/null +++ b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/RoasterJavaDslTwoRoutesTest.java @@ -0,0 +1,86 @@ +/** + * 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.camel.parser.java; + +import java.io.File; +import java.util.List; + +import org.apache.camel.parser.RouteBuilderParser; +import org.apache.camel.parser.model.CamelNodeDetails; +import org.apache.camel.test.junit4.CamelTestSupport; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.source.JavaClassSource; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RoasterJavaDslTwoRoutesTest extends CamelTestSupport { + + private static final Logger LOG = LoggerFactory.getLogger(RoasterJavaDslTwoRoutesTest.class); + + @Override + public boolean isDumpRouteCoverage() { + return true; + } + + @Test + public void parseTree() throws Exception { + JavaClassSource clazz = (JavaClassSource) Roaster.parse(new File("src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java")); + + List<CamelNodeDetails> list = RouteBuilderParser.parseRouteBuilderTree(clazz, ".", + "src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java",true); + assertEquals(2, list.size()); + + CamelNodeDetails details = list.get(0); + CamelNodeDetails details2 = list.get(1); + assertEquals("src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java", details.getFileName()); + assertEquals("src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java", details2.getFileName()); + + assertEquals("foo", details.getRouteId()); + assertEquals("org.apache.camel.parser.java.TwoRoutesRouteBuilder", details.getClassName()); + assertEquals("configure", details.getMethodName()); + assertEquals("bar", details2.getRouteId()); + assertEquals("configure", details2.getMethodName()); + assertEquals("org.apache.camel.parser.java.TwoRoutesRouteBuilder", details2.getClassName()); + + String tree = details.dump(0); + LOG.info("\n" + tree); + + String tree2 = details2.dump(0); + LOG.info("\n" + tree2); + + assertTrue(tree.contains("25\tfrom")); + assertTrue(tree.contains("26\t log")); + assertTrue(tree.contains("27\t to")); + + assertTrue(tree2.contains("29\tfrom")); + assertTrue(tree2.contains("30\t transform")); + assertTrue(tree2.contains("31\t to")); + } + + @Test + public void testRouteCoverage() throws Exception { + context.addRoutes(new TwoRoutesRouteBuilder()); + + getMockEndpoint("mock:foo").expectedMessageCount(1); + + template.sendBody("direct:foo", "Hello World"); + + assertMockEndpointsSatisfied(); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java new file mode 100644 index 0000000..55a8b08 --- /dev/null +++ b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/java/TwoRoutesRouteBuilder.java @@ -0,0 +1,33 @@ +/** + * 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.camel.parser.java; + +import org.apache.camel.builder.RouteBuilder; + +public class TwoRoutesRouteBuilder extends RouteBuilder { + + @Override + public void configure() throws Exception { + from("direct:foo").routeId("foo") + .log("I was here") + .to("mock:foo"); + + from("direct:bar").routeId("bar") + .transform(simple("Someone was here")) + .to("mock:bar"); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/xml/XmlParseTreeTest.java ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/xml/XmlParseTreeTest.java b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/xml/XmlParseTreeTest.java new file mode 100644 index 0000000..e4b8fe4 --- /dev/null +++ b/tooling/camel-route-parser/src/test/java/org/apache/camel/parser/xml/XmlParseTreeTest.java @@ -0,0 +1,58 @@ +/** + * 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.camel.parser.xml; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.List; + +import org.apache.camel.parser.XmlRouteParser; +import org.apache.camel.parser.model.CamelNodeDetails; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class XmlParseTreeTest { + + private static final Logger LOG = LoggerFactory.getLogger(XmlParseTreeTest.class); + + @Test + public void testXmlTree() throws Exception { + InputStream is = new FileInputStream("src/test/resources/org/apache/camel/parser/xml/mycamel.xml"); + String fqn = "src/test/resources/org/apache/camel/camel/parser/xml/mycamel.xml"; + String baseDir = "src/test/resources"; + List<CamelNodeDetails> list = XmlRouteParser.parseXmlRouteTree(is, baseDir, fqn); + + assertEquals(1, list.size()); + CamelNodeDetails details = list.get(0); + assertEquals("src/test/resources/org/apache/camel/camel/parser/xml/mycamel.xml", details.getFileName()); + assertEquals("myRoute", details.getRouteId()); + assertEquals(null, details.getMethodName()); + assertEquals(null, details.getClassName()); + + String tree = details.dump(0); + LOG.info("\n" + tree); + + assertTrue(tree.contains("32\tfrom")); + assertTrue(tree.contains("35\t transform")); + assertTrue(tree.contains("39\t to")); + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/camel-route-parser/src/test/resources/org/apache/camel/parser/xml/mycamel.xml ---------------------------------------------------------------------- diff --git a/tooling/camel-route-parser/src/test/resources/org/apache/camel/parser/xml/mycamel.xml b/tooling/camel-route-parser/src/test/resources/org/apache/camel/parser/xml/mycamel.xml index 6293639..683d78f 100644 --- a/tooling/camel-route-parser/src/test/resources/org/apache/camel/parser/xml/mycamel.xml +++ b/tooling/camel-route-parser/src/test/resources/org/apache/camel/parser/xml/mycamel.xml @@ -26,7 +26,7 @@ <!-- START SNIPPET: e1 --> <!-- camelContext is the Camel runtime, where we can host Camel routes --> <camelContext xmlns="http://camel.apache.org/schema/spring"> - <route> + <route id="myRoute"> <!-- read input from the console using the stream component --> <from uri="stream:in?promptMessage=Enter something: "/> http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java ---------------------------------------------------------------------- diff --git a/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java new file mode 100644 index 0000000..6f89638 --- /dev/null +++ b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java @@ -0,0 +1,408 @@ +/** + * 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.camel.maven; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.apache.camel.maven.helper.EndpointHelper; +import org.apache.camel.maven.model.RouteCoverageNode; +import org.apache.camel.parser.RouteBuilderParser; +import org.apache.camel.parser.XmlRouteParser; +import org.apache.camel.parser.helper.RouteCoverageHelper; +import org.apache.camel.parser.model.CamelNodeDetails; +import org.apache.camel.parser.model.CoverageData; +import org.apache.maven.model.Resource; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.project.MavenProject; +import org.codehaus.mojo.exec.AbstractExecMojo; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.source.JavaClassSource; + +/** + * Performs route coverage reports after running Camel unit tests with camel-test modules + * + * @goal route-coverage + * @threadSafe + */ +public class RouteCoverageMojo extends AbstractExecMojo { + + /** + * The maven project. + * + * @parameter property="project" + * @required + * @readonly + */ + protected MavenProject project; + + /** + * Whether to fail if a route was not fully covered + * + * @parameter property="camel.failOnError" + * default-value="false" + */ + private boolean failOnError; + + /** + * Whether to include test source code + * + * @parameter property="camel.includeTest" + * default-value="false" + */ + private boolean includeTest; + + /** + * To filter the names of java and xml files to only include files matching any of the given list of patterns (wildcard and regular expression). + * Multiple values can be separated by comma. + * + * @parameter property="camel.includes" + */ + private String includes; + + /** + * To filter the names of java and xml files to exclude files matching any of the given list of patterns (wildcard and regular expression). + * Multiple values can be separated by comma. + * + * @parameter property="camel.excludes" + */ + private String excludes; + + // CHECKSTYLE:OFF + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + + Set<File> javaFiles = new LinkedHashSet<File>(); + Set<File> xmlFiles = new LinkedHashSet<File>(); + + // find all java route builder classes + List list = project.getCompileSourceRoots(); + for (Object obj : list) { + String dir = (String) obj; + findJavaFiles(new File(dir), javaFiles); + } + // find all xml routes + list = project.getResources(); + for (Object obj : list) { + Resource dir = (Resource) obj; + findXmlFiles(new File(dir.getDirectory()), xmlFiles); + } + + if (includeTest) { + list = project.getTestCompileSourceRoots(); + for (Object obj : list) { + String dir = (String) obj; + findJavaFiles(new File(dir), javaFiles); + } + list = project.getTestResources(); + for (Object obj : list) { + Resource dir = (Resource) obj; + findXmlFiles(new File(dir.getDirectory()), xmlFiles); + } + } + + List<CamelNodeDetails> routeTrees = new ArrayList<>(); + + for (File file : javaFiles) { + if (matchFile(file)) { + try { + // parse the java source code and find Camel RouteBuilder classes + String fqn = file.getPath(); + String baseDir = "."; + JavaType out = Roaster.parse(file); + // we should only parse java classes (not interfaces and enums etc) + if (out != null && out instanceof JavaClassSource) { + JavaClassSource clazz = (JavaClassSource) out; + List<CamelNodeDetails> result = RouteBuilderParser.parseRouteBuilderTree(clazz, baseDir, fqn, true); + routeTrees.addAll(result); + } + } catch (Exception e) { + getLog().warn("Error parsing java file " + file + " code due " + e.getMessage(), e); + } + } + } + for (File file : xmlFiles) { + if (matchFile(file)) { + try { + // parse the xml files code and find Camel routes + String fqn = file.getPath(); + String baseDir = "."; + InputStream is = new FileInputStream(file); + List<CamelNodeDetails> result = XmlRouteParser.parseXmlRouteTree(is, baseDir, fqn); + routeTrees.addAll(result); + is.close(); + } catch (Exception e) { + getLog().warn("Error parsing xml file " + file + " code due " + e.getMessage(), e); + } + } + } + + getLog().info("Discovered " + routeTrees.size() + " routes"); + + // skip any routes which has no route id assigned + + long anonymous = routeTrees.stream().filter(t -> t.getRouteId() == null).count(); + if (anonymous > 0) { + getLog().warn("Discovered " + anonymous + " anonymous routes. Add route ids to these routes for route coverage support"); + } + + final AtomicInteger notCovered = new AtomicInteger(); + + routeTrees = routeTrees.stream().filter(t -> t.getRouteId() != null).collect(Collectors.toList()); + for (CamelNodeDetails t : routeTrees) { + String routeId = t.getRouteId(); + String fileName = asRelativeFile(t.getFileName()); + + // grab dump data for the route + try { + List<CoverageData> coverageData = RouteCoverageHelper.parseDumpRouteCoverageByRouteId("target/camel-route-coverage", routeId); + if (coverageData.isEmpty()) { + getLog().warn("No route coverage data found for route: " + routeId + + ". Make sure to enable route coverage in your unit tests and assign unique route ids to your routes. Also remember to run unit tests first."); + } else { + List<RouteCoverageNode> coverage = gatherRouteCoverageSummary(t, coverageData); + String out = templateCoverageData(fileName, routeId, coverage, notCovered); + getLog().info("Route coverage summary:\n\n" + out); + getLog().info(""); + } + + } catch (Exception e) { + throw new MojoExecutionException("Error during gathering route coverage data for route: " + routeId, e); + } + } + + if (failOnError && notCovered.get() > 0) { + throw new MojoExecutionException("There are " + notCovered.get() + " route(s) not fully covered!"); + } + } + // CHECKSTYLE:ON + + @SuppressWarnings("unchecked") + private String templateCoverageData(String fileName, String routeId, List<RouteCoverageNode> model, AtomicInteger notCovered) throws MojoExecutionException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream sw = new PrintStream(bos); + + if (model.get(0).getClassName() != null) { + sw.println("Class:\t" + model.get(0).getClassName()); + } else { + sw.println("File:\t" + fileName); + } + sw.println("RouteId:\t" + routeId); + sw.println(); + sw.println(String.format("%8s %8s %s", "Line #", "Count", "Route")); + sw.println(String.format("%8s %8s %s", "------", "-----", "-----")); + + int covered = 0; + for (RouteCoverageNode node : model) { + if (node.getCount() > 0) { + covered++; + } + String pad = padString(node.getLevel()); + sw.println(String.format("%8s %8s %s", node.getLineNumber(), node.getCount(), pad + node.getName())); + } + + if (covered != model.size()) { + // okay here is a route that was not fully covered + notCovered.incrementAndGet(); + } + + // calculate percentage of route coverage (must use double to have decimals) + double percentage = ((double) covered / (double) model.size()) * 100; + sw.println(); + sw.println("Coverage: " + covered + " out of " + model.size() + " (" + String.format("%.1f", percentage) + "%)"); + sw.println(); + + return bos.toString(); + } + + private static List<RouteCoverageNode> gatherRouteCoverageSummary(CamelNodeDetails route, List<CoverageData> coverageData) { + List<RouteCoverageNode> answer = new ArrayList<>(); + + Iterator<CoverageData> it = coverageData.iterator(); + AtomicInteger level = new AtomicInteger(); + gatherRouteCoverageSummary(route, it, level, answer); + + return answer; + } + + private static void gatherRouteCoverageSummary(CamelNodeDetails node, Iterator<CoverageData> it, AtomicInteger level, List<RouteCoverageNode> answer) { + RouteCoverageNode data = new RouteCoverageNode(); + data.setName(node.getName()); + data.setLineNumber(Integer.valueOf(node.getLineNumber())); + data.setLevel(level.get()); + data.setClassName(node.getClassName()); + data.setMethodName(node.getMethodName()); + + // add data + answer.add(data); + + // find count + boolean found = false; + while (!found && it.hasNext()) { + CoverageData holder = it.next(); + found = holder.getNode().equals(node.getName()); + if (found) { + data.setCount(holder.getCount()); + } + } + + if (node.getOutputs() != null) { + level.addAndGet(1); + for (CamelNodeDetails child : node.getOutputs()) { + gatherRouteCoverageSummary(child, it, level, answer); + } + level.addAndGet(-1); + } + } + + private static String padString(int level) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < level; i++) { + sb.append(" "); + } + return sb.toString(); + } + + private void findJavaFiles(File dir, Set<File> javaFiles) { + File[] files = dir.isDirectory() ? dir.listFiles() : null; + if (files != null) { + for (File file : files) { + if (file.getName().endsWith(".java")) { + javaFiles.add(file); + } else if (file.isDirectory()) { + findJavaFiles(file, javaFiles); + } + } + } + } + + private void findXmlFiles(File dir, Set<File> xmlFiles) { + File[] files = dir.isDirectory() ? dir.listFiles() : null; + if (files != null) { + for (File file : files) { + if (file.getName().endsWith(".xml")) { + xmlFiles.add(file); + } else if (file.isDirectory()) { + findXmlFiles(file, xmlFiles); + } + } + } + } + + private boolean matchFile(File file) { + if (excludes == null && includes == null) { + return true; + } + + // exclude take precedence + if (excludes != null) { + for (String exclude : excludes.split(",")) { + exclude = exclude.trim(); + // try both with and without directory in the name + String fqn = stripRootPath(asRelativeFile(file.getAbsolutePath())); + boolean match = EndpointHelper.matchPattern(fqn, exclude) || EndpointHelper.matchPattern(file.getName(), exclude); + if (match) { + return false; + } + } + } + + // include + if (includes != null) { + for (String include : includes.split(",")) { + include = include.trim(); + // try both with and without directory in the name + String fqn = stripRootPath(asRelativeFile(file.getAbsolutePath())); + boolean match = EndpointHelper.matchPattern(fqn, include) || EndpointHelper.matchPattern(file.getName(), include); + if (match) { + return true; + } + } + // did not match any includes + return false; + } + + // was not excluded nor failed include so its accepted + return true; + } + + private String asRelativeFile(String name) { + String answer = name; + + String base = project.getBasedir().getAbsolutePath(); + if (name.startsWith(base)) { + answer = name.substring(base.length()); + // skip leading slash for relative path + if (answer.startsWith(File.separator)) { + answer = answer.substring(1); + } + } + return answer; + } + + private String stripRootPath(String name) { + // strip out any leading source / resource directory + + List list = project.getCompileSourceRoots(); + for (Object obj : list) { + String dir = (String) obj; + dir = asRelativeFile(dir); + if (name.startsWith(dir)) { + return name.substring(dir.length() + 1); + } + } + list = project.getTestCompileSourceRoots(); + for (Object obj : list) { + String dir = (String) obj; + dir = asRelativeFile(dir); + if (name.startsWith(dir)) { + return name.substring(dir.length() + 1); + } + } + List resources = project.getResources(); + for (Object obj : resources) { + Resource resource = (Resource) obj; + String dir = asRelativeFile(resource.getDirectory()); + if (name.startsWith(dir)) { + return name.substring(dir.length() + 1); + } + } + resources = project.getTestResources(); + for (Object obj : resources) { + Resource resource = (Resource) obj; + String dir = asRelativeFile(resource.getDirectory()); + if (name.startsWith(dir)) { + return name.substring(dir.length() + 1); + } + } + + return name; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f015f7b0/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java ---------------------------------------------------------------------- diff --git a/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java new file mode 100644 index 0000000..cb6aba6 --- /dev/null +++ b/tooling/maven/camel-maven-plugin/src/main/java/org/apache/camel/maven/model/RouteCoverageNode.java @@ -0,0 +1,77 @@ +/** + * 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.camel.maven.model; + +public final class RouteCoverageNode { + + private String className; + private String methodName; + + private String name; + private int lineNumber; + private int count; + private int level; + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getLineNumber() { + return lineNumber; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public int getLevel() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } + +}
