All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mapstruct.ap.spi.DefaultBuilderProvider Maven / Gradle / Ivy

There is a newer version: 1.6.3
Show newest version
/*
 * Copyright MapStruct Authors.
 *
 * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
 */
package org.mapstruct.ap.spi;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor6;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;

/**
 * Default implementation of {@link BuilderProvider}.
 *
 * The default builder provider considers all public static parameterless methods of a {@link TypeMirror}
 * as potential builder creation methods. For each potential builder creation method checks in the return type
 * of the method if there exists a method that returns the initial {@link TypeMirror} if such a combination is found
 * the {@link BuilderInfo} is created with those 2 methods.
 * Example:
 * 

 * public class Person {
 *
 *     private final String firstName;
 *     private final String lastName;
 *
 *     private Person(String firstName, String lastName) {
 *         this.firstName = firstName;
 *         this.lastName = lastName;
 *     }
 *
 *     //getters
 *
 *     public static Builder builder() {
 *         return new Builder();
 *     }

 *     public static class Builder {
 *
 *         private String firstName;
 *         private String lastName;
 *
 *         private Builder() {}
 *
 *         //fluent setters
 *
 *         public Person create() {
 *             return new Person( firstName, lastName );
 *         }
 *     }
 * }
 * 
* * In the example above, when searching for a builder for the {@code Person} type. The {@code Person#builder} method * would be a builder creation candidate. Then the return type of {@code Person#builder}, {@code Builder}, is * investigated for a parameterless method that returns {@code Person}. When {@code Builder#create} is found * the {@link BuilderInfo} is created with the {@code Person#builder} as a builder creation method and * {@code Builder#create} as a build method. *

* IMPORTANT: Types from the {@code java} and {@code javax} packages are excluded from inspection * * @author Filip Hrisafov */ public class DefaultBuilderProvider implements BuilderProvider { private static final Pattern JAVA_JAVAX_PACKAGE = Pattern.compile( "^javax?\\..*" ); protected Elements elementUtils; protected Types typeUtils; @Override public void init(MapStructProcessingEnvironment processingEnvironment) { this.elementUtils = processingEnvironment.getElementUtils(); this.typeUtils = processingEnvironment.getTypeUtils(); } @Override public BuilderInfo findBuilderInfo(TypeMirror type) { TypeElement typeElement = getTypeElement( type ); if ( typeElement == null ) { return null; } return findBuilderInfo( typeElement ); } /** * Find the {@link TypeElement} for the given {@link TypeMirror}. * * @param type for which the {@link TypeElement} needs to be found/ * * @return the type element or {@code null} if the {@link TypeMirror} is not a {@link DeclaredType} * and the declared type element is not {@link TypeElement} * * @throws TypeHierarchyErroneousException if the {@link TypeMirror} is of kind {@link TypeKind#ERROR} */ protected TypeElement getTypeElement(TypeMirror type) { DeclaredType declaredType = getDeclaredType( type ); return getTypeElement( declaredType ); } /** * Find the {@link TypeElement} for the given {@link DeclaredType}. * * @param declaredType for which the {@link TypeElement} needs to be found. * @return the type element or {@code null} if the declared type element is not {@link TypeElement} */ private TypeElement getTypeElement(DeclaredType declaredType) { if ( declaredType == null ) { return null; } return declaredType.asElement().accept( new SimpleElementVisitor6() { @Override public TypeElement visitType(TypeElement e, Void p) { return e; } }, null ); } /** * Find the {@link DeclaredType} for the given {@link TypeMirror}. * * @param type for which the {@link DeclaredType} needs to be found. * @return the declared or {@code null} if the {@link TypeMirror} is not a {@link DeclaredType} * @throws TypeHierarchyErroneousException if the {@link TypeMirror} is of kind {@link TypeKind#ERROR} */ private DeclaredType getDeclaredType(TypeMirror type) { if ( type.getKind() == TypeKind.ERROR ) { throw new TypeHierarchyErroneousException( type ); } return type.accept( new SimpleTypeVisitor6() { @Override public DeclaredType visitDeclared(DeclaredType t, Void p) { return t; } }, null ); } /** * Find the {@link BuilderInfo} for the given {@code typeElement}. *

* The default implementation iterates over all the methods in {@code typeElement} and uses * {@link DefaultBuilderProvider#isPossibleBuilderCreationMethod(ExecutableElement, TypeElement)} and * {@link DefaultBuilderProvider#findBuildMethods(TypeElement, TypeElement)} to create the * {@link BuilderInfo}. *

* The default implementation uses {@link DefaultBuilderProvider#shouldIgnore(TypeElement)} to check if the * {@code typeElement} should be ignored. *

* In case there are multiple {@link BuilderInfo} then a {@link MoreThanOneBuilderCreationMethodException} is * thrown. * * @param typeElement the type element for which a builder searched * @return the {@link BuilderInfo} or {@code null} if no builder was found for the type * {@link DefaultBuilderProvider#findBuildMethods(TypeElement, TypeElement)} * @throws MoreThanOneBuilderCreationMethodException if there are multiple builder creation methods */ protected BuilderInfo findBuilderInfo(TypeElement typeElement) { if ( shouldIgnore( typeElement ) ) { return null; } List methods = ElementFilter.methodsIn( typeElement.getEnclosedElements() ); List builderInfo = new ArrayList<>(); for ( ExecutableElement method : methods ) { if ( isPossibleBuilderCreationMethod( method, typeElement ) ) { TypeElement builderElement = getTypeElement( method.getReturnType() ); Collection buildMethods = findBuildMethods( builderElement, typeElement ); if ( !buildMethods.isEmpty() ) { builderInfo.add( new BuilderInfo.Builder() .builderCreationMethod( method ) .buildMethod( buildMethods ) .build() ); } } } if ( builderInfo.size() == 1 ) { return builderInfo.get( 0 ); } else if ( builderInfo.size() > 1 ) { throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), builderInfo ); } return findBuilderInfo( typeElement.getSuperclass() ); } /** * Checks if the {@code method} is a possible builder creation method. *

* The default implementation considers a method as a possible creation method if the following is satisfied: *

    *
  • The method has no parameters
  • *
  • It is a {@code public static} method
  • *
  • The return type of the {@code method} is not the same as the {@code typeElement}
  • *
  • *
* * @param method The method that needs to be checked * @param typeElement the enclosing element of the method, i.e. the type in which the method is located in * @return {@code true} if the {@code method} is a possible builder creation method, {@code false} otherwise */ protected boolean isPossibleBuilderCreationMethod(ExecutableElement method, TypeElement typeElement) { return method.getParameters().isEmpty() && method.getModifiers().contains( Modifier.PUBLIC ) && method.getModifiers().contains( Modifier.STATIC ) && method.getReturnType().getKind() != TypeKind.VOID // Only compare raw elements // Reason: if the method is a generic method ( Holder build()) and the type element is (Holder) // then the return type of the method does not match the type of the type element && !typeUtils.isSameType( typeUtils.erasure( method.getReturnType() ), typeUtils.erasure( typeElement.asType() ) ); } /** * Searches for a build method for {@code typeElement} within the {@code builderElement}. *

* The default implementation iterates over each method in {@code builderElement} and uses * {@link DefaultBuilderProvider#isBuildMethod(ExecutableElement, DeclaredType, TypeElement)} * to check if the method is a build method for {@code typeElement}. *

* The default implementation uses {@link DefaultBuilderProvider#shouldIgnore(TypeElement)} to check if the * {@code builderElement} should be ignored, i.e. not checked for build elements. *

* @param builderElement the element for the builder * @param typeElement the element for the type that is being built * @return the build method for the {@code typeElement} if it exists, or {@code null} if it does not * {@code build} */ protected Collection findBuildMethods(TypeElement builderElement, TypeElement typeElement) { if ( shouldIgnore( builderElement ) || typeElement == null ) { return Collections.emptyList(); } DeclaredType builderType = getDeclaredType( builderElement.asType() ); if ( builderType == null ) { return Collections.emptyList(); } return findBuildMethods( builderElement, builderType, typeElement ); } private Collection findBuildMethods(TypeElement builderElement, DeclaredType builderType, TypeElement typeElement) { if ( shouldIgnore( builderElement ) ) { return Collections.emptyList(); } List builderMethods = ElementFilter.methodsIn( builderElement.getEnclosedElements() ); List buildMethods = new ArrayList<>(); for ( ExecutableElement buildMethod : builderMethods ) { if ( isBuildMethod( buildMethod, builderType, typeElement ) ) { buildMethods.add( buildMethod ); } } if ( !buildMethods.isEmpty() ) { return buildMethods; } Collection parentClassBuildMethods = findBuildMethods( getTypeElement( builderElement.getSuperclass() ), builderType, typeElement ); if ( !parentClassBuildMethods.isEmpty() ) { return parentClassBuildMethods; } List interfaces = builderElement.getInterfaces(); if ( interfaces.isEmpty() ) { return Collections.emptyList(); } Collection interfaceBuildMethods = new ArrayList<>(); for ( TypeMirror builderInterface : interfaces ) { interfaceBuildMethods.addAll( findBuildMethods( getTypeElement( builderInterface ), builderType, typeElement ) ); } return interfaceBuildMethods; } /** * @see #isBuildMethod(ExecutableElement, DeclaredType, TypeElement) * @deprecated use {@link #isBuildMethod(ExecutableElement, DeclaredType, TypeElement)} instead */ @Deprecated protected boolean isBuildMethod(ExecutableElement buildMethod, TypeElement typeElement) { return buildMethod.getParameters().isEmpty() && buildMethod.getModifiers().contains( Modifier.PUBLIC ) && typeUtils.isAssignable( buildMethod.getReturnType(), typeElement.asType() ); } /** * Checks if the {@code buildMethod} is a method that creates the {@code typeElement} * as a member of the {@code builderType}. *

* The default implementation considers a method to be a build method if the following is satisfied: *

    *
  • The method has no parameters
  • *
  • The method is public
  • *
  • The return type of method is assignable to the {@code typeElement}
  • *
* * @param buildMethod the method that should be checked * @param builderType the type of the builder in which the {@code buildMethod} is located in * @param typeElement the type element that needs to be built * @return {@code true} if the {@code buildMethod} is a build method for {@code typeElement}, {@code false} * otherwise */ protected boolean isBuildMethod(ExecutableElement buildMethod, DeclaredType builderType, TypeElement typeElement) { if ( !buildMethod.getParameters().isEmpty() ) { return false; } if ( !buildMethod.getModifiers().contains( Modifier.PUBLIC ) ) { return false; } TypeMirror buildMethodType = typeUtils.asMemberOf( builderType, buildMethod ); if ( buildMethodType instanceof ExecutableType ) { return typeUtils.isAssignable( ( (ExecutableType) buildMethodType ).getReturnType(), typeElement.asType() ); } return false; } /** * Whether the {@code typeElement} should be ignored, i.e. not used in inspection. *

* The default implementations ignores {@code null} elements and elements that belong to the {@code java} and * {@code javax} packages * @param typeElement that needs to be checked * @return {@code true} if the element should be ignored, {@code false} otherwise */ protected boolean shouldIgnore(TypeElement typeElement) { return typeElement == null || JAVA_JAVAX_PACKAGE.matcher( typeElement.getQualifiedName() ).matches(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy