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
17 changes: 17 additions & 0 deletions README.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,30 @@ Aesh is a Java library for building interactive CLI applications with commands,
* Group commands and sub-command mode (e.g. `git rebase`, `git pull`)
* Custom validators, activators, completers, converters, and renderers
* Auto-generated help text from command metadata
* Compile-time annotation processor for reflection-free command metadata (faster startup, GraalVM-friendly)
* Add and remove commands at runtime
* Terminal graphics utilities: progress bar, table, tree display
* Line editing, history, undo/redo, paste buffer
* Emacs and Vi editing modes
* Masking, redirect, alias, pipeline support
* Works on POSIX systems and Windows

== Annotation Processor (Optional)

Add the `aesh-processor` dependency to generate command metadata at compile time, eliminating runtime reflection for annotation scanning and improving startup time. This is especially useful for GraalVM native images.

[source,xml]
----
<dependency>
<groupId>org.aesh</groupId>
<artifactId>aesh-processor</artifactId>
<version>3.0</version>
<scope>provided</scope>
</dependency>
----

No code changes required -- if the generated metadata is on the classpath, Aesh uses it automatically. Otherwise it falls back to the existing reflection-based approach.

== Quick Start

[source,java]
Expand Down
91 changes: 91 additions & 0 deletions aesh-processor/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ JBoss, Home of Professional Open Source
~ Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
~ as indicated by the @authors tag. All rights reserved.
~ See the copyright.txt in the distribution for a
~ full listing of individual contributors.
~
~ Licensed 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.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<parent>
<groupId>org.aesh</groupId>
<artifactId>aesh-all</artifactId>
<version>3.4-dev</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>org.aesh</groupId>
<artifactId>aesh-processor</artifactId>
<packaging>jar</packaging>
<version>3.4-dev</version>
<name>Æsh Annotation Processor</name>
<description>Compile-time annotation processor for Æsh commands</description>

<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>

<dependencies>
<dependency>
<groupId>org.aesh</groupId>
<artifactId>aesh</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.aesh</groupId>
<artifactId>aesh</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- Disable annotation processing during main compilation
to avoid the processor trying to process itself -->
<proc>none</proc>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<trimStackTrace>false</trimStackTrace>
<includes>
<include>**/*TestCase.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/*
* JBoss, Home of Professional Open Source
* Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed 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.aesh.processor;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

import org.aesh.command.Command;
import org.aesh.command.CommandDefinition;
import org.aesh.command.GroupCommandDefinition;

/**
* JSR 269 annotation processor that generates {@code CommandMetadataProvider}
* implementations for classes annotated with {@link CommandDefinition} or
* {@link GroupCommandDefinition}.
*
* @author Aesh team
*/
@SupportedAnnotationTypes({
"org.aesh.command.CommandDefinition",
"org.aesh.command.GroupCommandDefinition"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AeshAnnotationProcessor extends AbstractProcessor {

private Filer filer;
private Messager messager;
private Elements elementUtils;
private Types typeUtils;
private final List<String> generatedProviders = new ArrayList<>();

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.filer = processingEnv.getFiler();
this.messager = processingEnv.getMessager();
this.elementUtils = processingEnv.getElementUtils();
this.typeUtils = processingEnv.getTypeUtils();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
if (!generatedProviders.isEmpty()) {
writeServiceFile();
}
return false;
}

Set<TypeElement> commandElements = new LinkedHashSet<>();

for (Element element : roundEnv.getElementsAnnotatedWith(CommandDefinition.class)) {
if (element.getKind() == ElementKind.CLASS) {
commandElements.add((TypeElement) element);
}
}

for (Element element : roundEnv.getElementsAnnotatedWith(GroupCommandDefinition.class)) {
if (element.getKind() == ElementKind.CLASS) {
commandElements.add((TypeElement) element);
}
}

for (TypeElement commandElement : commandElements) {
if (!validate(commandElement)) {
continue;
}
try {
generateProvider(commandElement);
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Failed to generate metadata provider: " + e.getMessage(), commandElement);
}
}

return false;
}

private boolean validate(TypeElement element) {
boolean valid = true;

if (element.getModifiers().contains(Modifier.ABSTRACT)) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Command class must not be abstract", element);
valid = false;
}

TypeMirror commandType = elementUtils.getTypeElement(Command.class.getCanonicalName()).asType();
if (!typeUtils.isAssignable(typeUtils.erasure(element.asType()), typeUtils.erasure(commandType))) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Command class must implement org.aesh.command.Command", element);
valid = false;
}

boolean hasNoArgConstructor = false;
for (Element enclosed : element.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
javax.lang.model.element.ExecutableElement constructor =
(javax.lang.model.element.ExecutableElement) enclosed;
if (constructor.getParameters().isEmpty()
&& !constructor.getModifiers().contains(Modifier.PRIVATE)) {
hasNoArgConstructor = true;
break;
}
}
}
if (!hasNoArgConstructor) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Command class must have an accessible no-arg constructor", element);
valid = false;
}

// Validate field types
for (VariableElement field : collectFields(element)) {
validateField(field);
}

return valid;
}

private void validateField(VariableElement field) {
if (field.getAnnotation(org.aesh.command.option.OptionList.class) != null) {
TypeMirror collectionType = elementUtils
.getTypeElement(Collection.class.getCanonicalName()).asType();
if (!typeUtils.isAssignable(typeUtils.erasure(field.asType()),
typeUtils.erasure(collectionType))) {
messager.printMessage(Diagnostic.Kind.ERROR,
"OptionList field must be instance of Collection", field);
}
}
if (field.getAnnotation(org.aesh.command.option.Arguments.class) != null) {
TypeMirror collectionType = elementUtils
.getTypeElement(Collection.class.getCanonicalName()).asType();
if (!typeUtils.isAssignable(typeUtils.erasure(field.asType()),
typeUtils.erasure(collectionType))) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Arguments field must be instance of Collection", field);
}
}
if (field.getAnnotation(org.aesh.command.option.OptionGroup.class) != null) {
TypeMirror mapType = elementUtils
.getTypeElement(Map.class.getCanonicalName()).asType();
if (!typeUtils.isAssignable(typeUtils.erasure(field.asType()),
typeUtils.erasure(mapType))) {
messager.printMessage(Diagnostic.Kind.ERROR,
"OptionGroup field must be instance of Map", field);
}
}
}

private List<VariableElement> collectFields(TypeElement element) {
List<VariableElement> fields = new ArrayList<>();
collectFieldsRecursive(element, fields);
return fields;
}

private void collectFieldsRecursive(TypeElement element, List<VariableElement> fields) {
for (Element enclosed : element.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.FIELD) {
fields.add((VariableElement) enclosed);
}
}
TypeMirror superclass = element.getSuperclass();
if (superclass.getKind() != TypeKind.NONE) {
Element superElement = typeUtils.asElement(superclass);
if (superElement instanceof TypeElement) {
collectFieldsRecursive((TypeElement) superElement, fields);
}
}
}

private void generateProvider(TypeElement commandElement) throws IOException {
String qualifiedName = commandElement.getQualifiedName().toString();
String packageName = "";
int lastDot = qualifiedName.lastIndexOf('.');
if (lastDot > 0) {
packageName = qualifiedName.substring(0, lastDot);
}
String simpleName = commandElement.getSimpleName().toString();
String metadataClassName = simpleName + "_AeshMetadata";
String fullMetadataName = packageName.isEmpty() ? metadataClassName : packageName + "." + metadataClassName;

boolean isGroup = commandElement.getAnnotation(GroupCommandDefinition.class) != null;

List<VariableElement> fields = collectFields(commandElement);

String code = CodeGenerator.generate(
packageName, simpleName, metadataClassName, qualifiedName,
commandElement, fields, isGroup, elementUtils, typeUtils);

JavaFileObject sourceFile = filer.createSourceFile(fullMetadataName, commandElement);
try (Writer writer = sourceFile.openWriter()) {
writer.write(code);
}

generatedProviders.add(fullMetadataName);
}

private void writeServiceFile() {
try {
javax.tools.FileObject serviceFile = filer.createResource(
javax.tools.StandardLocation.CLASS_OUTPUT, "",
"META-INF/services/org.aesh.command.metadata.CommandMetadataProvider");
try (Writer writer = serviceFile.openWriter()) {
for (String provider : generatedProviders) {
writer.write(provider);
writer.write("\n");
}
}
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Failed to write ServiceLoader file: " + e.getMessage());
}
}
}
Loading