When you define a class as being a @Command
Airline will automatically discover command
metadata by examining the class hierarchy of the annotated class. By this we mean that it will walk up the hierarchy to
base classes to discover additional Airline annotations that are inherited by the command class. This means that you
can use standard inheritance to define base classes that contain @Option
definitions
that can be inherited by multiple command implementations.
For example we might want to have all our commands have a verbose option available e.g.
public abstract class BaseCommand implements ExampleRunnable {
@Option(name = { "-v", "--verbose" }, description = "Enables verbose mode")
protected boolean verbose = false;
}
@Command(name = "maybe-verbose")
public abstract class MaybeVerboseCommand extends BaseCommand {
@Override
public int run() {
if (this.verbose) {
System.out.println("Verbose");
} else {
System.out.println("Normal");
}
return 0;
}
public static void main(String[] args) {
ExampleExecutor.executeSingleCommand(MaybeVerboseCommand.class, args);
}
}
Note that we still need to follow normal inheritance best practises about field visibility, in the above example the
field in the parent class is marked as protected
so that we can access in the child class.
When we have larger class hierarchies it may be desirable to override the implementation of a option to be more specific
to a given command. Details on overriding options are given in the documentation of the @Option
Overriding annotation.
When you override an option Airline will still populate all the relevant fields individually. So for example if you override an option in a child class both the parent and child field for that option will get populated if the user specifies the option. This ensures that any logic in the parent and child can use the field values as they would usually.
If you are using Restrictions then any restrictions defined on an option are controlled by the deepest definition of the option with restrictions. This means that you can change the restrictions on an option by defining new restrictions on an override. Equally if you don’t define any restrictions with an override you automatically inherit any restrictions defined by the parent definition.
If you want to remove the restrictions on an overridden option you can use the special
@Unrestricted
annotation to denote this.
If you are using Help Annotations e.g. @Discussion
then any help sections defined
are automatically inherited by child classes. For example you might wish to define a
@Copyright
annotation on your base class to automatically add a Copyright section to
all your commands.
If the same help annotation is defined multiple times in a class hierarchy the deepest definition is used. So if your
parent defines an @Copyright
annotation and your child class also defines an @Copyright
annotation then your child
definition will be used.
If you wish to hide an inherited help section the special @HideSection
annotation
can be used to do this.
Additionally we may want to break out sets of related options into reusable modules and compose these together into our
classes. When Airline is scanning the command class for annotated fields it will also scan any field marked with the
@AirlineModule
annotation. The class for that field will be scanned and any further
annotations included into the command metadata.
For example if we wanted to make our verbose option reusable across commands without any common ancestor we could do the following:
public class VerbosityModule {
@Option(name = { "-v", "--verbosity" }, arity = 1, title = "Level", description = "Sets the desired verbosity")
// The AllowedRawValues annotation allows an option to be restricted to a given set of values
@AllowedRawValues(allowedValues = { "1", "2", "3" })
public int verbosity = 1;
}
@Command(name = "module-reuse", description = "A command that demonstrates re-use of modules and composition with locally defined options")
public class ModuleReuse implements ExampleRunnable {
@AirlineModule
private HelpOption<ExampleRunnable> help;
/**
* A field marked with {@link AirlineModule} will also be scanned for options
*/
@AirlineModule
private VerbosityModule verbosity = new VerbosityModule();
@Arguments
private List<String> args = new ArrayList<String>();
public static void main(String[] args) {
ExampleExecutor.executeSingleCommand(ModuleReuse.class, args);
}
@Override
public int run() {
if (!help.showHelpIfRequested()) {
System.out.println("Verbosity is " + verbosity.verbosity);
System.out.println("Arguments were " + StringUtils.join(args, ", "));
}
return 0;
}
}
Note that we are able to compose as many other classes as we want by defining multiple fields annotated with
@AirlineModule
. Note that when accessing these options we have to access them via their originating fields so again we
need to be aware of field visibility when composing modules together.
In versions of Airline prior to 2.9.0 we used the javax.inject.Inject
annotation to achieve composition, so in the
above example you would replace @AirlineModule
with @Inject
and you may still see this used in old example code or
projects using older Airline releases.
However multiple users have reported over the years that this conflicts with its usage by dependency injection
frameworks. Additionally with the JavaEE javax.*
packages moving under the control of the Eclipse Foundation and
being repacked as jakarta.*
package instead there are now conflicts between different versions of the same annotation.
Therefore the decision was taken as of 2.9.0 to change the annotation used for this to
@AirlineModule
.
For backwards compatibility the 2.9.x releases continue to support the old @Inject
annotation so there is no breaking
change for existing users upgrading to newer Airline releases. Currently the following annotations may be used for
composition:
com.github.rvesse.airline.annotations.AirlineModule
javax.inject.Inject
jakarta.inject.Inject
com.google.inject.Inject
In order to support this backwards compatibility we continue to have dependencies on both the jakarta.inject
and
javax.inject
modules. The latter is indirectly depended upon via a temporary airline-backcompact-javaxinject
to
make it clear that this is for backwards compatibility only and can be excluded for users who have moved to using
the new @AirlineModule
annotation. As noted in the above warning these dependencies
will become optional
in future releases and eventually be removed as mandatory dependencies.
As of 2.9.0 the annotations considered to mark a field as being used for composition, i.e. a field whose value type Airline should scan for further Airline annotations, became fully configurable.
You can configured this via the compositionAnnotationClasses
field of your
@Parser
annotation. This takes an array of strings indicating
annotation classes that you want Airline to consider as composition annotations e.g.
@Parser(compositionAnnotationClasses = {
"com.github.rvesse.airline.annotations.AirlineModule",
"javax.inject.Inject"
})
Would configure the parser to treat both @AirlineModule
and @Inject
as composition
annotations. This field is specified using string class names rather than Class
objects to enable the backwards
compatibility described above and to allow us to drop the extra dependencies in the future
without breaking existing consumers of the library.
If you choose to use additional annotations you may need to explicitly declare additional Maven dependencies to ensure those annotation classes are available at runtime.
This documentation is itself open source and lives in GitHub under the docs/ directory.
I am not a professional technical writer and as the developer of this software I can often make assumptions of
knowledge that you as a user reading this may not have. Improvements to the documentation are always welcome, if you
have suggestions for the documentation please submit pull requests to the main
branch.