Skip to content
Open
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
21 changes: 21 additions & 0 deletions api/src/main/java/org/mapstruct/tools/gem/GemValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ public static GemValue<String> createEnum(AnnotationValue annotationValue,
return new GemValue<>( value, defaultValue, annotationValue );
}

public static <E extends Enum<E>> GemValue<E> createEnum(AnnotationValue annotationValue,
AnnotationValue annotationDefaultValue,
Class<E> enumClass) {
ValueAnnotationValueVisitor<VariableElement, E> visitor = new ValueAnnotationValueVisitor<>(
variableElement -> Enum.valueOf( enumClass, variableElement.getSimpleName().toString() ) );
E value = visit( annotationValue, visitor, VariableElement.class );
E defaultValue = visit( annotationDefaultValue, visitor, VariableElement.class );
return new GemValue<>( value, defaultValue, annotationValue );
}

public static GemValue<List<String>> createEnumArray(AnnotationValue annotationValue,
AnnotationValue annotationDefaultValue) {
ValueAnnotationValueListVisitor<VariableElement, String> visitor = new ValueAnnotationValueListVisitor<>(
Expand All @@ -58,6 +68,17 @@ public static GemValue<List<String>> createEnumArray(AnnotationValue annotationV
return new GemValue<>( value, defaultValue, annotationValue );
}

public static <E extends Enum<E>> GemValue<List<E>> createEnumArray(AnnotationValue annotationValue,
AnnotationValue annotationDefaultValue,
Class<E> enumClass) {
ValueAnnotationValueListVisitor<VariableElement, E> visitor = new ValueAnnotationValueListVisitor<>(
variableElement -> Enum.valueOf( enumClass, variableElement.getSimpleName().toString() ) );
List<E> value = visitList( annotationValue, visitor, VariableElement.class );
List<E> defaultValue = visitList( annotationDefaultValue, visitor, VariableElement.class );

return new GemValue<>( value, defaultValue, annotationValue );
}

public static <V> GemValue<V> create(AnnotationValue annotationValue, AnnotationValue annotationDefaultValue,
Function<AnnotationMirror, V> creator) {
ValueAnnotationValueVisitor<AnnotationMirror, V> visitor = new ValueAnnotationValueVisitor<>( creator );
Expand Down
25 changes: 25 additions & 0 deletions api/src/main/java/org/mapstruct/tools/gem/RegisterGem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.tools.gem;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Registers the annotated {@code enum} as the gem enum for {@code value()}.
* The annotated enum must contain exactly all constants of the {@code enum} specified in {@code value()}.
*/
@Retention(RetentionPolicy.SOURCE)
@Target({ ElementType.TYPE })
public @interface RegisterGem {

/**
* @return the {@code enum} which should be represented by the annotated class.
*/
Class<? extends Enum<?>> value();
}
429 changes: 423 additions & 6 deletions api/src/test/java/org/mapstruct/tools/gem/GemValueTest.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.tools.gem.processor;

import java.util.Arrays;
import java.util.List;
import javax.lang.model.element.Element;

public class GemEnum {
private final String gemPackageName;
private final String gemName;
private final String originalEnumFullName;
private final List<String> enumConstants;
private final Element[] originatingElements;

public GemEnum(String gemPackageName, String gemName, String originalEnumFullName,
List<String> values, Element... originatingElements) {
this.gemPackageName = gemPackageName;
this.gemName = gemName;
this.originalEnumFullName = originalEnumFullName;
this.enumConstants = values;
this.originatingElements = Arrays.copyOf( originatingElements, originatingElements.length );
}

public String getGemPackageName() {
return gemPackageName;
}

public String getGemName() {
return gemName;
}

public String getOriginalEnumFullName() {
return originalEnumFullName;
}

public List<String> getEnumConstants() {
return enumConstants;
}

public Element[] getOriginatingElements() {
return originatingElements;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ public Element[] getOriginatingElements() {
}

private boolean isNotSamePackage(GemValueType valueType ) {
return !valueType.getPacakage().equals( gemPackageName );
return !valueType.getPackageName().equals( gemPackageName );
}

private boolean isNotJavaLang( GemValueType valueType ) {
return !"java.lang".equals( valueType.getPacakage() );
return !"java.lang".equals( valueType.getPackageName() );
}

private boolean isNotTypeMirror( GemValueType valueType ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -21,7 +22,9 @@
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
Expand All @@ -34,16 +37,19 @@
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.Version;

/**
* @author sjaakd
*/
@SupportedAnnotationTypes( {"org.mapstruct.tools.gem.GemDefinitions", "org.mapstruct.tools.gem.GemDefinition"} )
@SupportedAnnotationTypes( {"org.mapstruct.tools.gem.GemDefinitions", "org.mapstruct.tools.gem.GemDefinition",
"org.mapstruct.tools.gem.RegisterGem"} )
public class GemProcessor extends AbstractProcessor {

private Util util;
private List<GemInfo> gemInfos = new ArrayList<>( 10 );
private final List<GemInfo> gemInfos = new ArrayList<>( 10 );
private final List<GemEnum> gemEnums = new ArrayList<>();
private final Map<String, GemEnum> gemEnumMap = new HashMap<>();
private boolean noErrors = true;

@Override
public SourceVersion getSupportedSourceVersion() {
Expand Down Expand Up @@ -74,13 +80,18 @@ public boolean process(Set<? extends TypeElement> annotationTypes, RoundEnvironm
);
gemDefinitionMirrors.forEach( m -> addGemInfo( m, definingElement ) );
}
else if ( "org.mapstruct.tools.gem.RegisterGem".equals( annotationName ) ) {
addEnumMapping( gemDefinitionsMirror, definingElement );
}
else {
addGemInfo( gemDefinitionsMirror, definingElement );
}
}
}
postProcessGemInfo();
write();
if ( noErrors ) {
postProcessGemInfo();
write();
}
}
catch ( RuntimeException ex ) {
StringWriter sw = new StringWriter();
Expand All @@ -95,27 +106,51 @@ private void addGemInfo(AnnotationMirror gemDefinitionMirror, Element definingEl

// create gem info
DeclaredType gemDeclaredType = util.getAnnotationValue( gemDefinitionMirror, "value", DeclaredType.class );
Element element = gemDeclaredType.asElement();
ElementKind kind = element.getKind();
String annotationName = util.getSimpleName( gemDeclaredType );
String gemName = getGemName( gemDefinitionMirror, annotationName );
PackageElement pkg = processingEnv.getElementUtils().getPackageOf( definingElement );
String gemFqn = util.getFullyQualifiedName( gemDeclaredType );
String gemPackage = pkg.getQualifiedName().toString();
if ( kind == ElementKind.ENUM ) {
List<String> enumConstants = getEnumConstants( element );
GemEnum gemEnum = new GemEnum(gemPackage, gemName, gemFqn, enumConstants, definingElement, element);
gemEnums.add( gemEnum );
if ( gemEnumMap.put( gemFqn, gemEnum ) != null ) {
processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR,
"Enum gem " + gemFqn + " can only be registered once" );
noErrors = false;
}
}
else if ( kind == ElementKind.ANNOTATION_TYPE ) {

// collect value info
List<ExecutableElement> methods = ElementFilter.methodsIn( element.getEnclosedElements() );
List<GemValueInfo> gemValueInfos = methods.stream()
.map( e -> new GemValueInfo( e.getSimpleName().toString(), e.getReturnType() ) )
.collect( Collectors.toList() );
GemInfo gemInfo = new GemInfo(
gemPackage,
gemName,
annotationName,
gemFqn,
gemValueInfos,
definingElement,
element
);
gemInfos.add( gemInfo );
}
else {
throw new IllegalArgumentException();
}

// collect value info
List<ExecutableElement> methods = ElementFilter.methodsIn( gemDeclaredType.asElement().getEnclosedElements() );
List<GemValueInfo> gemValueInfos = methods.stream()
.map( e -> new GemValueInfo( e.getSimpleName().toString(), e.getReturnType() ) )
.collect( Collectors.toList() );
GemInfo gemInfo = new GemInfo(
gemPackage,
gemName,
annotationName,
gemFqn,
gemValueInfos,
definingElement,
gemDeclaredType.asElement()
);
gemInfos.add( gemInfo );
}

private List<String> getEnumConstants(Element element) {
return element.getEnclosedElements().stream()
.filter( e -> e.getKind() == ElementKind.ENUM_CONSTANT )
.map( Element::getSimpleName ).map( Name::toString ).collect( Collectors.toList() );
}

private String getGemName(AnnotationMirror gemDefinitionMirror, String annotationName) {
Expand Down Expand Up @@ -148,7 +183,13 @@ private GemValueType getGemValueType(TypeMirror type, boolean isArray) {
DeclaredType declaredType = (DeclaredType) type;
String fqn = util.getFullyQualifiedName( declaredType );
if ( util.isEnumeration( declaredType ) ) {
valueType = new GemValueType( String.class, true, isArray );
GemEnum gemEnum = gemEnumMap.get( fqn );
if (gemEnum != null) {
valueType = new GemValueType( gemEnum, isArray );
}
else {
valueType = new GemValueType( String.class, true, isArray );
}
}
else if ( Class.class.getName().equals( fqn ) ) {
valueType = new GemValueType( TypeMirror.class, false, isArray );
Expand All @@ -157,16 +198,11 @@ else if (String.class.getName().equals( fqn ) ) {
valueType = new GemValueType( String.class, false, isArray );
}
else {
GemInfo usedGem = gemInfos.stream()
valueType = gemInfos.stream()
.filter( g -> fqn.equals( g.getAnnotationFqn() ) )
.findFirst()
.orElse( null );
if ( usedGem != null ) {
valueType = new GemValueType( usedGem, isArray );
}
else {
valueType = new GemValueType( TypeMirror.class, false, isArray );
}
.map( usedGem -> new GemValueType( usedGem, isArray ) )
.orElseGet( () -> new GemValueType( TypeMirror.class, false, isArray ) );
}
break;
case BOOLEAN:
Expand Down Expand Up @@ -199,17 +235,74 @@ else if (String.class.getName().equals( fqn ) ) {
return valueType;
}

private void addEnumMapping(AnnotationMirror gemDefinitionsMirror, Element definingElement) {
if ( definingElement.getKind() != ElementKind.ENUM) {
throw new IllegalArgumentException();
}
String packageName = processingEnv.getElementUtils().getPackageOf( definingElement )
.getQualifiedName().toString();
DeclaredType original = util.getAnnotationValue( gemDefinitionsMirror, "value", DeclaredType.class );
String originalFullName = util.getFullyQualifiedName( original );
List<String> originalElements = getEnumConstants( original.asElement() );
Set<String> originalValues = new HashSet<>( originalElements );
List<String> enumConstants = getEnumConstants( definingElement );
Set<String> definedValues = new HashSet<>( enumConstants );
List<String> missingOriginals = originalValues.stream()
.filter( value -> !definedValues.remove( value ) ).collect( Collectors.toList() );

if ( !missingOriginals.isEmpty() ) {
processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, "Enum constants " + missingOriginals
+ " are missing in " + packageName + "." + definingElement.getSimpleName() + ". A enum gem of "
+ originalFullName + " should exactly contain " + originalElements );
noErrors = false;
}
if ( !definedValues.isEmpty() ) {
processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR, "Enum constants " + definedValues
+ " are only present in " + packageName + "." + definingElement.getSimpleName() + ". A enum gem of "
+ originalFullName + " should exactly contain " + originalElements );
noErrors = false;
}
if ( gemEnumMap.put( originalFullName, new GemEnum(
packageName,
definingElement.getSimpleName().toString(),
originalFullName,
enumConstants
) ) != null ) {
processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR,
"Enum gem " + originalFullName + " can only be registered once" );
noErrors = false;
}
}

private void write( ) {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
cfg.setClassForTemplateLoading( GemProcessor.class, "/" );
cfg.setDefaultEncoding( "UTF-8" );
for (GemEnum gemEnum : gemEnums) {
try (Writer writer = processingEnv.getFiler()
.createSourceFile(
gemEnum.getGemPackageName() + "." + gemEnum.getGemName(),
gemEnum.getOriginatingElements()
)
.openWriter() ) {
Map<String, Object> templateData = new HashMap<>();

templateData.put( "gemEnum", gemEnum );
Template template = cfg.getTemplate( "org/mapstruct/tools/gem/processor/Enum.ftl" );
template.process( templateData, writer );
writer.flush();
}
catch (TemplateException | IOException ex) {
throw new IllegalStateException(ex);
}
}
for ( GemInfo gemInfo : gemInfos ) {
try (Writer writer = processingEnv.getFiler()
.createSourceFile(
gemInfo.getGemPackageName() + "." + gemInfo.getGemName(),
gemInfo.getOriginatingElements()
)
.openWriter()) {
Configuration cfg = new Configuration( new Version( "2.3.21" ) );
cfg.setClassForTemplateLoading( GemProcessor.class, "/" );
cfg.setDefaultEncoding( "UTF-8" );
.openWriter() ) {

Map<String, Object> templateData = new HashMap<>();

Expand All @@ -223,5 +316,7 @@ private void write( ) {
}
// handled all info, clear
gemInfos.clear();
gemEnums.clear();
gemEnumMap.clear();
}
}
Loading