ascopes opened a new issue, #11378:
URL: https://github.com/apache/maven/issues/11378
### New feature, improvement proposal
Hello.
Proposing that given Maven 4 will baseline to Java 17, sealed classes get a
simplified interface for implementation discovery during the parameter
convertion process.
## Aims
- Reduce boilerplate for users.
- Allow polymorphic conversion of sealed types using intrinsics
- Allow further documentation generation automatically from sealed types.
## Sealed type handling
Effectively, suppose I have the following Mojo:
```java
@Mojo(...)
public class MyMojo extends ... {
@Parameter
ThirdPartyArtifact artifact;
...
}
```
Now, let's assume `ThirdPartyArtifact` is sealed:
```java
public sealed interface ThirdPartyArtifact {
public final class MavenArtifact implements ThirdPartyArtifact { ... }
public final class SystemPathArtifact implements ThirdPartyArtifact { ... }
public final class LocalPathArtifact implements ThirdPartyArtifact { ... }
public final class RemoteUrlArtifact implements ThirdPartyArtifact { ... }
}
```
Currently, my understanding is that the user would have to provide the
canonical class name and package to the `implementation` attribute in their XML
configuration, to avoid ambiguity.
Given the types are sealed, we should be able to query
`Class#getPermittedSubclasses()` to get all implementations. This would in
theory allow us to perform this class lookup based on just the class simple
name, allowing a user to simplify configuration from...
```xml
<configuration>
<artifact
implementation="io.github.ascopes.myplugin.models.MavenArtifact.SystemPathArtifact">
<name>gcc</name>
</artifact>
</configuration>
```
...to just...
```xml
<configuration>
<artifact implementation="SystemPathArtifact">
<name>gcc</name>
</artifact>
</configuration>
```
...which is far less verbose and far more user-friendly.
## Possible implementation
A new converter could be added to the Plexus context by default with the
following implementation:
```java
public class SealedTypeConverter extends AbstractBasicConverter {
@Override
public boolean canConvert(Class<?> cls) {
return cls.isSealed();
}
@Override
public Object fromConfiguration(
ConverterLookup lookup,
PlexusConfiguration configuration,
Class<?> type,
Class<?> enclosingType,
ClassLoader classLoader,
ExpressionEvaluator evaluator
) throws ComponentConfigurationException {
if (configuration.getChildCount() == 0) {
return null;
}
var hint = configuration.getAttribute("implementation");
var implementationClass = Stream
.of(type.getPermittedSubclasses())
.sorted(Class::getName)
.filter(cls -> getPermittedClassNames(cls)
.anyMatch(name -> name.equals(hint))
.findFirst();
if (implementationClass.isNotPresent()) {
throw new ComponentConfigurationException(...);
}
return converterLookup.lookupConverterFromType(implementationClass.get())
.fromConfiguration(...);
}
private Stream<String> getPermittedClassNames(Class<?> type) {
var canonicalNames = Stream.of(
type.getName(),
type.getSimpleName()
);
var customHints = ofNullable(type.getAnnotation(SealedTypeHint.class)
.map(SealedTypeHint::value)
.stream()
.flatMap(Stream::of);
return Stream.concat(canonicalNames, customHints);
}
}
## Addition of metadata to `@Parameter`
It would be sensible to add an attribute to `@Parameter` to allow the
specification of the default implementation type to consider if information is
not provided. I'd envison this being allowed for both sealed and non-sealed
types, for example:
```java
@interface Parameter {
...
Class<?> defaultImplementation() default void.class;
}
```
```java
class MyMojo extends ... {
@Parameter(defaultImplementation = MavenArtifact.class)
ThirdPartyArtifact artifact;
// Also useful for cases where code is generated via an annotation
processor.
// Consider the immutables library:
@Parameter(defaultImplementation = MutableSourceListing.class)
SourceListing source;
}
```
## Caveats and possible improvements
A caveat to this would be if there is more than one implementor with the
same name. Aside from being a very poor design decision, this could be worked
around by either raising a warning/exception, or providing the developer with a
way of "tagging" implementation names to the sealed class implementations via
an annotation.
The annotation for doing this could
be the following:
```java
@Retention(RUNTIME)
@Target({TYPE, RECORD})
public @interface SealedTypeHint {
String[] value();
}
```
...this would allow defining my example from earlier like so:
```java
public sealed interface ThirdPartyArtifact {
@SealedTypeHint("maven")
public final class MavenArtifact implements ThirdPartyArtifact { ... }
@SealedTypeHint("system-path")
public final class SystemPathArtifact implements ThirdPartyArtifact { ... }
@SealedTypeHint("local-path")
public final class LocalPathArtifact implements ThirdPartyArtifact { ... }
@SealedTypeHint("remote")
public final class RemoteUrlArtifact implements ThirdPartyArtifact { ... }
}
```
...enabling the XML example to be just...
```xml
<configuration>
<artifact implementation="system-path">
<name>gcc</name>
</artifact>
</configuration>
```
One other caveat may be the use of `non-sealed` implementation types.
However, in this case, the default behaviour of `implementation` should still
be allowed for backwards compatibility.
### Further possible improvements
- Records could be supported for deserialization via Plexus to allow
developers to create concise parameter definitions.
- Documentation could be generated from the knowledge of the sealed types in
`maven-plugin-plugin` to avoid developers having to manually document supported
types.
- The `maven-plugin-plugin` could configure the generated XSD schema to have
knowledge of the known permitted types for the parameter, allowing IDEs to
provide clear linting and autocomplete for polymorphic types without
having to perform expensive classpath analysis of the plugin and dependency
tree.
- This implementation would be compatible with other schema formats like
JsonSchema, should Maven decide to allow non-XML configuration in the future.
- The `@SealedTypeHint` _in theory_ could be changed to be just a regular
`@TypeHint` or leverage similar Java EE/Jakarta EE metadata annotations for the
same purpose. This would potentially require costly classpath scanning though
which may hinder startup execution times for plugins. In theory though this
information may be possible to collect when `maven-plugin-plugin` is run.
## Workarounds
Right now in Maven 3, parameters have to have a clear type or rely on the
user giving a fully qualified canonical class name. This contributes to the
outdated archetype that Maven is overly verbose for new users, which can hinder
adoption.
Polymorphic types have to be documented explicitly by the developer rather
than being generated from JavaDocs when a type is sealed, which leads to
documentation mistakes and inconsistencies. Many plugins just do not document
this sort of thing properly at all, requiring users of plugins to have to read
the source code to have any idea how to configure this sort of thing properly.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]