/**
* 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.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.catalog.EndpointValidationResult;
import org.apache.camel.catalog.SimpleValidationResult;
import org.apache.camel.catalog.lucene.LuceneSuggestionStrategy;
import org.apache.camel.catalog.maven.MavenVersionManager;
import org.apache.camel.maven.helper.EndpointHelper;
import org.apache.camel.parser.RouteBuilderParser;
import org.apache.camel.parser.XmlRouteParser;
import org.apache.camel.parser.model.CamelEndpointDetails;
import org.apache.camel.parser.model.CamelSimpleExpressionDetails;
import org.apache.maven.model.Dependency;
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;
/**
* Parses the source code and validates the Camel routes has valid endpoint uris and simple expressions.
*
* @goal validate
*/
public class ValidateMojo extends AbstractExecMojo {
/**
* The maven project.
*
* @parameter property="project"
* @required
* @readonly
*/
protected MavenProject project;
/**
* Whether to fail if invalid Camel endpoints was found. By default the plugin logs the errors at WARN level
*
* @parameter property="camel.failOnError"
* default-value="false"
*/
private boolean failOnError;
/**
* Whether to log endpoint URIs which was un-parsable and therefore not possible to validate
*
* @parameter property="camel.logUnparseable"
* default-value="false"
*/
private boolean logUnparseable;
/**
* Whether to include Java files to be validated for invalid Camel endpoints
*
* @parameter property="camel.includeJava"
* default-value="true"
*/
private boolean includeJava;
/**
* Whether to include XML files to be validated for invalid Camel endpoints
*
* @parameter property="camel.includeXml"
* default-value="true"
*/
private boolean includeXml;
/**
* 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;
/**
* Whether to ignore unknown components
*
* @parameter property="camel.ignoreUnknownComponent"
* default-value="true"
*/
private boolean ignoreUnknownComponent;
/**
* Whether to ignore incapable of parsing the endpoint uri
*
* @parameter property="camel.ignoreIncapable"
* default-value="true"
*/
private boolean ignoreIncapable;
/**
* Whether to ignore components that uses lenient properties. When this is true, then the uri validation is stricter
* but would fail on properties that are not part of the component but in the uri because of using lenient properties.
* For example using the HTTP components to provide query parameters in the endpoint uri.
*
* @parameter property="camel.ignoreLenientProperties"
* default-value="true"
*/
private boolean ignoreLenientProperties;
/**
* Whether to show all endpoints and simple expressions (both invalid and valid).
*
* @parameter property="camel.showAll"
* default-value="false"
*/
private boolean showAll;
/**
* Whether to allow downloading Camel catalog version from the internet. This is needed if the project
* uses a different Camel version than this plugin is using by default.
*
* @parameter property="camel.downloadVersion"
* default-value="true"
*/
private boolean downloadVersion;
// CHECKSTYLE:OFF
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
CamelCatalog catalog = new DefaultCamelCatalog();
// add activemq as known component
catalog.addComponent("activemq", "org.apache.activemq.camel.component.ActiveMQComponent");
// enable did you mean
catalog.setSuggestionStrategy(new LuceneSuggestionStrategy());
// enable loading other catalog versions dynamically
catalog.setVersionManager(new MavenVersionManager());
// enable caching
catalog.enableCache();
if (downloadVersion) {
String catalogVersion = catalog.getCatalogVersion();
String version = findCamelVersion(project);
if (version != null && !version.equals(catalogVersion)) {
// the project uses a different Camel version so attempt to load it
getLog().info("Downloading Camel version: " + version);
boolean loaded = catalog.loadVersion(version);
if (!loaded) {
getLog().warn("Error downloading Camel version: " + version);
}
}
}
// if using the same version as the camel-maven-plugin we must still load it
if (catalog.getLoadedVersion() == null) {
catalog.loadVersion(catalog.getCatalogVersion());
}
if (catalog.getLoadedVersion() != null) {
getLog().info("Using Camel version: " + catalog.getLoadedVersion());
} else {
// force load version from the camel-maven-plugin
getLog().info("Using Camel version: " + catalog.getCatalogVersion());
}
List<CamelEndpointDetails> endpoints = new ArrayList<>();
List<CamelSimpleExpressionDetails> simpleExpressions = new ArrayList<>();
Set<File> javaFiles = new LinkedHashSet<File>();
Set<File> xmlFiles = new LinkedHashSet<File>();
// find all java route builder classes
if (includeJava) {
List list = project.getCompileSourceRoots();
for (Object obj : list) {
String dir = (String) obj;
findJavaFiles(new File(dir), javaFiles);
}
if (includeTest) {
list = project.getTestCompileSourceRoots();
for (Object obj : list) {
String dir = (String) obj;
findJavaFiles(new File(dir), javaFiles);
}
}
}
// find all xml routes
if (includeXml) {
List list = project.getResources();
for (Object obj : list) {
Resource dir = (Resource) obj;
findXmlFiles(new File(dir.getDirectory()), xmlFiles);
}
if (includeTest) {
list = project.getTestResources();
for (Object obj : list) {
Resource dir = (Resource) obj;
findXmlFiles(new File(dir.getDirectory()), xmlFiles);
}
}
}
for (File file : javaFiles) {
if (matchFile(file)) {
try {
List<CamelEndpointDetails> fileEndpoints = new ArrayList<>();
List<CamelSimpleExpressionDetails> fileSimpleExpressions = new ArrayList<>();
List<String> unparsable = new ArrayList<>();
// 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;
RouteBuilderParser.parseRouteBuilderEndpoints(clazz, baseDir, fqn, fileEndpoints, unparsable, includeTest);
RouteBuilderParser.parseRouteBuilderSimpleExpressions(clazz, baseDir, fqn, fileSimpleExpressions);
// add what we found in this file to the total list
endpoints.addAll(fileEndpoints);
simpleExpressions.addAll(fileSimpleExpressions);
// was there any unparsable?
if (logUnparseable && !unparsable.isEmpty()) {
for (String uri : unparsable) {
getLog().warn("Cannot parse endpoint uri " + uri + " in java file " + file);
}
}
}
} catch (Exception e) {
getLog().warn("Error parsing java file " + file + " code due " + e.getMessage(), e);
}
}
}
for (File file : xmlFiles) {
if (matchFile(file)) {
try {
List<CamelEndpointDetails> fileEndpoints = new ArrayList<>();
List<CamelSimpleExpressionDetails> fileSimpleExpressions = new ArrayList<>();
// parse the xml source code and find Camel routes
String fqn = file.getPath();
String baseDir = ".";
InputStream is = new FileInputStream(file);
XmlRouteParser.parseXmlRouteEndpoints(is, baseDir, fqn, fileEndpoints);
is.close();
// need a new stream
is = new FileInputStream(file);
XmlRouteParser.parseXmlRouteSimpleExpressions(is, baseDir, fqn, fileSimpleExpressions);
is.close();
// add what we found in this file to the total list
endpoints.addAll(fileEndpoints);
simpleExpressions.addAll(fileSimpleExpressions);
} catch (Exception e) {
getLog().warn("Error parsing xml file " + file + " code due " + e.getMessage(), e);
}
}
}
int endpointErrors = 0;
int unknownComponents = 0;
int incapableErrors = 0;
for (CamelEndpointDetails detail : endpoints) {
getLog().debug("Validating endpoint: " + detail.getEndpointUri());
EndpointValidationResult result = catalog.validateEndpointProperties(detail.getEndpointUri(), ignoreLenientProperties);
boolean ok = result.isSuccess();
if (!ok && ignoreUnknownComponent && result.getUnknownComponent() != null) {
// if we failed due unknown component then be okay if we should ignore that
unknownComponents++;
ok = true;
}
if (!ok && ignoreIncapable && result.getIncapable() != null) {
// if we failed due incapable then be okay if we should ignore that
incapableErrors++;
ok = true;
}
if (!ok) {
if (result.getUnknownComponent() != null) {
unknownComponents++;
} else if (result.getIncapable() != null) {
incapableErrors++;
} else {
endpointErrors++;
}
StringBuilder sb = new StringBuilder();
sb.append("Endpoint validation error at: ");
if (detail.getClassName() != null && detail.getLineNumber() != null) {
// this is from java code
sb.append(detail.getClassName());
if (detail.getMethodName() != null) {
sb.append(".").append(detail.getMethodName());
}
sb.append("(").append(asSimpleClassName(detail.getClassName())).append(".java:");
sb.append(detail.getLineNumber()).append(")");
} else if (detail.getLineNumber() != null) {
// this is from xml
String fqn = stripRootPath(asRelativeFile(detail.getFileName()));
if (fqn.endsWith(".xml")) {
fqn = fqn.substring(0, fqn.length() - 4);
fqn = asPackageName(fqn);
}
sb.append(fqn);
sb.append("(").append(asSimpleClassName(fqn)).append(".xml:");
sb.append(detail.getLineNumber()).append(")");
} else {
sb.append(detail.getFileName());
}
sb.append("\n\n");
String out = result.summaryErrorMessage(false);
sb.append(out);
sb.append("\n\n");
getLog().warn(sb.toString());
} else if (showAll) {
StringBuilder sb = new StringBuilder();
sb.append("Endpoint validation passsed at: ");
if (detail.getClassName() != null && detail.getLineNumber() != null) {
// this is from java code
sb.append(detail.getClassName());
if (detail.getMethodName() != null) {
sb.append(".").append(detail.getMethodName());
}
sb.append("(").append(asSimpleClassName(detail.getClassName())).append(".java:");
sb.append(detail.getLineNumber()).append(")");
} else if (detail.getLineNumber() != null) {
// this is from xml
String fqn = stripRootPath(asRelativeFile(detail.getFileName()));
if (fqn.endsWith(".xml")) {
fqn = fqn.substring(0, fqn.length() - 4);
fqn = asPackageName(fqn);
}
sb.append(fqn);
sb.append("(").append(asSimpleClassName(fqn)).append(".xml:");
sb.append(detail.getLineNumber()).append(")");
} else {
sb.append(detail.getFileName());
}
sb.append("\n");
sb.append("\n\t").append(result.getUri());
sb.append("\n\n");
getLog().info(sb.toString());
}
}
String endpointSummary;
if (endpointErrors == 0) {
int ok = endpoints.size() - endpointErrors - incapableErrors - unknownComponents;
endpointSummary = String.format("Endpoint validation success: (%s = passed, %s = invalid, %s = incapable, %s = unknown components)",
ok, endpointErrors, incapableErrors, unknownComponents);
} else {
int ok = endpoints.size() - endpointErrors - incapableErrors - unknownComponents;
endpointSummary = String.format("Endpoint validation error: (%s = passed, %s = invalid, %s = incapable, %s = unknown components)",
ok, endpointErrors, incapableErrors, unknownComponents);
}
if (endpointErrors > 0) {
getLog().warn(endpointSummary);
} else {
getLog().info(endpointSummary);
}
int simpleErrors = 0;
for (CamelSimpleExpressionDetails detail : simpleExpressions) {
SimpleValidationResult result;
boolean predicate = detail.isPredicate();
if (predicate) {
getLog().debug("Validating simple predicate: " + detail.getSimple());
result = catalog.validateSimplePredicate(detail.getSimple());
} else {
getLog().debug("Validating simple expression: " + detail.getSimple());
result = catalog.validateSimpleExpression(detail.getSimple());
}
if (!result.isSuccess()) {
simpleErrors++;
StringBuilder sb = new StringBuilder();
sb.append("Simple validation error at: ");
if (detail.getClassName() != null && detail.getLineNumber() != null) {
// this is from java code
sb.append(detail.getClassName());
if (detail.getMethodName() != null) {
sb.append(".").append(detail.getMethodName());
}
sb.append("(").append(asSimpleClassName(detail.getClassName())).append(".java:");
sb.append(detail.getLineNumber()).append(")");
} else if (detail.getLineNumber() != null) {
// this is from xml
String fqn = stripRootPath(asRelativeFile(detail.getFileName()));
if (fqn.endsWith(".xml")) {
fqn = fqn.substring(0, fqn.length() - 4);
fqn = asPackageName(fqn);
}
sb.append(fqn);
sb.append("(").append(asSimpleClassName(fqn)).append(".xml:");
sb.append(detail.getLineNumber()).append(")");
} else {
sb.append(detail.getFileName());
}
sb.append("\n");
String[] lines = result.getError().split("\n");
for (String line : lines) {
sb.append("\n\t").append(line);
}
sb.append("\n");
getLog().warn(sb.toString());
} else if (showAll) {
StringBuilder sb = new StringBuilder();
sb.append("Simple validation passed at: ");
if (detail.getClassName() != null && detail.getLineNumber() != null) {
// this is from java code
sb.append(detail.getClassName());
if (detail.getMethodName() != null) {
sb.append(".").append(detail.getMethodName());
}
sb.append("(").append(asSimpleClassName(detail.getClassName())).append(".java:");
sb.append(detail.getLineNumber()).append(")");
} else if (detail.getLineNumber() != null) {
// this is from xml
String fqn = stripRootPath(asRelativeFile(detail.getFileName()));
if (fqn.endsWith(".xml")) {
fqn = fqn.substring(0, fqn.length() - 4);
fqn = asPackageName(fqn);
}
sb.append(fqn);
sb.append("(").append(asSimpleClassName(fqn)).append(".xml:");
sb.append(detail.getLineNumber()).append(")");
} else {
sb.append(detail.getFileName());
}
sb.append("\n");
sb.append("\n\t").append(result.getSimple());
sb.append("\n\n");
getLog().info(sb.toString());
}
}
String simpleSummary;
if (simpleErrors == 0) {
int ok = simpleExpressions.size() - simpleErrors;
simpleSummary = String.format("Simple validation success: (%s = passed, %s = invalid)", ok, simpleErrors);
} else {
int ok = simpleExpressions.size() - simpleErrors;
simpleSummary = String.format("Simple validation error: (%s = passed, %s = invalid)", ok, simpleErrors);
}
if (failOnError && (endpointErrors > 0 || simpleErrors > 0)) {
throw new MojoExecutionException(endpointSummary + "\n" + simpleSummary);
}
if (simpleErrors > 0) {
getLog().warn(simpleSummary);
} else {
getLog().info(simpleSummary);
}
}
// CHECKSTYLE:ON
private static String findCamelVersion(MavenProject project) {
Dependency candidate = null;
List list = project.getDependencies();
for (Object obj : list) {
Dependency dep = (Dependency) obj;
if ("org.apache.camel".equals(dep.getGroupId())) {
if ("camel-core".equals(dep.getArtifactId())) {
// favor camel-core
candidate = dep;
break;
} else {
candidate = dep;
}
}
}
if (candidate != null) {
return candidate.getVersion();
}
return null;
}
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;
}
private static String asPackageName(String name) {
return name.replace(File.separator, ".");
}
private static String asSimpleClassName(String className) {
int dot = className.lastIndexOf('.');
if (dot > 0) {
return className.substring(dot + 1);
} else {
return className;
}
}
}