Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelogs/feature/extend-sip-maven-plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"author": "Nemikor",
"pullrequestId": "316",
"message": "Add a new goal declarative-structure-check in SIP Maven plugin, which checks whether declarative structure classes are valid."
}
14 changes: 14 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@
<guava.version>32.0.1-jre</guava.version> <!-- Maintain with protobuf-->
<error_prone_annotations.version>2.18.0</error_prone_annotations.version>
<j2objc-annotations.version>2.8</j2objc-annotations.version>
<java-parser.version>3.27.1</java-parser.version>
<checher-qual.version>3.51.1</checher-qual.version> <!-- Maintain with java-parser -->


<cxf.version>4.1.1</cxf.version> <!-- keep in sync with camel-dependencies cxf-version property -->
Expand Down Expand Up @@ -380,6 +382,18 @@
<artifactId>j2objc-annotations</artifactId>
<version>${j2objc-annotations.version}</version>
</dependency>

<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-symbol-solver-core</artifactId>
<version>${java-parser.version}</version>
</dependency>

<dependency>
<groupId>org.checkerframework</groupId>
<artifactId>checker-qual</artifactId>
<version>${checher-qual.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
9 changes: 9 additions & 0 deletions sip-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
<artifactId>maven-plugin-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-symbol-solver-core</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand All @@ -72,6 +76,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>one.x1f.sip.foundation</groupId>
<artifactId>sip-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package one.x1f.sip.foundation.mvnplugin;

import static one.x1f.sip.foundation.mvnplugin.DeclarativeStructureCheckMojo.X1F_SIP_GROUP;

import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithName;
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
import com.github.javaparser.resolution.types.ResolvedReferenceType;
import java.util.*;
import one.x1f.sip.foundation.mvnplugin.model.ClassAnalysisResult;

public class DeclarativeClassAnalyser {

private final Map<String, String> expectedParentInterfaces =
Map.of(
"InboundConnector",
"one.x1f.sip.foundation.core.declarative.connector.InboundConnectorDefinition",
"OutboundConnector",
"one.x1f.sip.foundation.core.declarative.connector.OutboundConnectorDefinition",
"IntegrationScenario",
"one.x1f.sip.foundation.core.declarative.scenario.IntegrationScenarioDefinition",
"CompositeProcess",
"one.x1f.sip.foundation.core.declarative.process.CompositeProcessDefinition",
"ConnectorGroup",
"one.x1f.sip.foundation.core.declarative.connectorgroup.ConnectorGroupDefinition");

private final Map<String, String> expectedAnnotationClasses =
Map.of(
"one.x1f.sip.foundation.core.declarative.connector.InboundConnectorDefinition",
"InboundConnector",
"one.x1f.sip.foundation.core.declarative.connector.OutboundConnectorDefinition",
"OutboundConnector",
"one.x1f.sip.foundation.core.declarative.scenario.IntegrationScenarioDefinition",
"IntegrationScenario",
"one.x1f.sip.foundation.core.declarative.process.CompositeProcessDefinition",
"CompositeProcess",
"one.x1f.sip.foundation.core.declarative.connectorgroup.ConnectorGroupDefinition",
"ConnectorGroup");

public ClassAnalysisResult doAnalysis(ClassOrInterfaceDeclaration clazz) {
var annotations = clazz.getAnnotations();
var resolved = clazz.resolve();
List<ResolvedReferenceType> ancestors = new ArrayList<>();
resolved.getAncestors().forEach(ancestor -> addResolvableAncestor(ancestor, ancestors));

return analyseDeclarativeStructure(resolved, annotations, ancestors);
}

private void addResolvableAncestor(
ResolvedReferenceType current, List<ResolvedReferenceType> ancestors) {
try {
String name = current.getQualifiedName();

if (!name.startsWith(X1F_SIP_GROUP)) return;

ancestors.add(current);
current
.getTypeDeclaration()
.ifPresent(
typeDeclaration ->
typeDeclaration
.getAncestors()
.forEach(ancestor -> addResolvableAncestor(ancestor, ancestors)));
} catch (Exception e) {
// skip because non resolvable
}
}

private ClassAnalysisResult analyseDeclarativeStructure(
ResolvedReferenceTypeDeclaration resolved,
NodeList<AnnotationExpr> annotations,
List<ResolvedReferenceType> ancestors) {
List<String> annotationNames = annotations.stream().map(NodeWithName::getNameAsString).toList();
ClassAnalysisResult result = analyseAnnotations(resolved, ancestors, annotationNames);
if (result != null) return result;
result = analyseAncestors(resolved, ancestors, annotationNames);
if (result != null) return result;

return new ClassAnalysisResult(false, resolved.getQualifiedName());
}

private ClassAnalysisResult analyseAncestors(
ResolvedReferenceTypeDeclaration resolved,
List<ResolvedReferenceType> ancestors,
List<String> annotationNames) {
List<String> invalidAncestorMatches =
ancestors.stream()
.map(ResolvedReferenceType::getQualifiedName)
.filter(expectedAnnotationClasses::containsKey)
.filter(
ancestor -> !doesImplementedClassHaveMatchingAnnotation(annotationNames, ancestor))
.toList();
if (!invalidAncestorMatches.isEmpty()) {
String ancestorName = invalidAncestorMatches.get(0);
String second =
String.format(
"Class %s must be annotated with @%s to match the required base class.",
resolved.getQualifiedName(), expectedAnnotationClasses.get(ancestorName));
return new ClassAnalysisResult(true, second);
}
return null;
}

private ClassAnalysisResult analyseAnnotations(
ResolvedReferenceTypeDeclaration resolved,
List<ResolvedReferenceType> ancestors,
List<String> annotationNames) {
List<String> annotationMatches =
annotationNames.stream().filter(expectedParentInterfaces::containsKey).toList();
if (!annotationMatches.isEmpty()) {
String annotationName = annotationMatches.get(0);
if (!doesAnnotatedClassHaveMatchingParent(ancestors, annotationName)) {
String message =
String.format(
"Class %s annotated with @%s does not extend the required base type.",
resolved.getQualifiedName(), annotationName);
return new ClassAnalysisResult(true, message);
}
}
return null;
}

private boolean doesAnnotatedClassHaveMatchingParent(
List<ResolvedReferenceType> ancestors, String annotationName) {
return ancestors.stream()
.anyMatch(i -> i.getQualifiedName().equals(expectedParentInterfaces.get(annotationName)));
}

private boolean doesImplementedClassHaveMatchingAnnotation(
List<String> annotations, String interfaceName) {
return annotations.stream()
.anyMatch(
annotationName -> annotationName.equals(expectedAnnotationClasses.get(interfaceName)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package one.x1f.sip.foundation.mvnplugin;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.resolution.typesolvers.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
import one.x1f.sip.foundation.mvnplugin.model.ClassAnalysisResult;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

@Mojo(
name = "declarative-structure-check",
defaultPhase = LifecyclePhase.COMPILE,
requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class DeclarativeStructureCheckMojo extends AbstractMojo {

public static final String X1F_SIP_GROUP = "one.x1f.sip";
public static final String JAVA_EXTENSION = ".java";
public static final String JAR_EXTENSION = ".jar";
public static final String APACHE_CAMEL_GROUP = "org.apache.camel";

@Parameter(defaultValue = "${project}", required = true, readonly = true)
MavenProject mavenProject;

@Parameter(defaultValue = "${project.basedir}/src/main/java")
private File sourceDir;

private final DeclarativeClassAnalyser declarativeClassAnalyser = new DeclarativeClassAnalyser();

@Override
public void execute() throws MojoExecutionException {
try {
CombinedTypeSolver typeSolver = new CombinedTypeSolver();
typeSolver.add(new ReflectionTypeSolver());
typeSolver.add(new JavaParserTypeSolver(sourceDir));

for (Artifact dep : mavenProject.getArtifacts()) {
if (!dep.getGroupId().startsWith(X1F_SIP_GROUP)
&& !dep.getGroupId().startsWith(APACHE_CAMEL_GROUP)) continue;
File file = dep.getFile();
if (file != null && file.exists() && file.getName().endsWith(JAR_EXTENSION)) {
typeSolver.add(new JarTypeSolver(file));
} else if (file != null && file.exists() && file.getAbsolutePath().contains("sip-core")) {
typeSolver.add(new JavaParserTypeSolver(file));
}
}

ParserConfiguration config =
new ParserConfiguration().setSymbolResolver(new JavaSymbolSolver(typeSolver));

JavaParser parser = new JavaParser(config);

try (Stream<Path> pathStream = Files.walk(sourceDir.toPath())) {
var results =
pathStream
.filter(p -> p.toString().endsWith(JAVA_EXTENSION))
.map(path -> analyseClass(parser, path))
.toList();
validate(results);
}
} catch (Exception e) {
throw new MojoExecutionException("Error analysing declarative structure.", e);
}
}

private ClassAnalysisResult analyseClass(JavaParser parser, Path path) {
try {
Optional<CompilationUnit> result = parser.parse(path).getResult();
if (result.isEmpty()) {
return new ClassAnalysisResult(false, null);
}
for (ClassOrInterfaceDeclaration clazz :
result.get().findAll(ClassOrInterfaceDeclaration.class)) {
var classAnalysisResult = declarativeClassAnalyser.doAnalysis(clazz);
if (classAnalysisResult.error()) {
return classAnalysisResult;
}
}
} catch (Exception e) {
getLog().warn("Failed to parse " + path + ": " + e.getMessage());
}
return new ClassAnalysisResult(false, null);
}

private void validate(List<ClassAnalysisResult> analyseResult) throws MojoExecutionException {
boolean hasErrors = false;

for (ClassAnalysisResult res : analyseResult) {
if (res.error()) {
getLog().error(res.message());
hasErrors = true;
}
}

if (hasErrors) {
throw new MojoExecutionException("Validation failed.");
} else {
getLog().info("Declarative Structure is valid.");
}
}

protected void setMavenProject(MavenProject mavenProject) {
this.mavenProject = mavenProject;
}

protected void setSourceDir(File sourceDir) {
this.sourceDir = sourceDir;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package one.x1f.sip.foundation.mvnplugin.model;

public record ClassAnalysisResult(boolean error, String message) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package one.x1f.sip.foundation.connectors.con1;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import one.x1f.sip.foundation.core.declarative.connector.OutboundConnectorDefinition;
import one.x1f.sip.foundation.core.declarative.orchestration.Orchestrator;
import one.x1f.sip.foundation.core.declarative.orchestration.connector.ConnectorOrchestrationInfo;
import org.apache.camel.model.RouteDefinition;

public class NoAnnotationConnector implements OutboundConnectorDefinition {

@Override
public void defineOutboundEndpoints(RouteDefinition routeDefinition) {
// test
}

@Override
public String getConnectorGroupId() {
return "";
}

@Override
public String getScenarioId() {
return "";
}

@Override
public Class<?> getRequestModelClass() {
return null;
}

@Override
public Optional<Class<?>> getResponseModelClass() {
return Optional.empty();
}

@Override
public String[] getConfigurationIds() {
return new String[0];
}

@Override
public List<Method> getOnExceptionHandler() {
return List.of();
}

@Override
public String getId() {
return "";
}

@Override
public String getPathToDocumentationResource() {
return "";
}

@Override
public Orchestrator<ConnectorOrchestrationInfo> getOrchestrator() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package one.x1f.sip.foundation.connectors.con1;

import one.x1f.sip.foundation.core.declarative.annotation.InboundConnector;

@InboundConnector(
connectorGroup = "test",
integrationScenario = "test",
requestModel = Object.class)
public class NoParentConnector {}
Loading