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

org.babyfish.jimmer.apt.client.ClientProcessor Maven / Gradle / Ivy

There is a newer version: 0.8.177
Show newest version
package org.babyfish.jimmer.apt.client;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonValue;
import org.babyfish.jimmer.Immutable;
import org.babyfish.jimmer.apt.Context;
import org.babyfish.jimmer.apt.GeneratorException;
import org.babyfish.jimmer.apt.MetaException;
import org.babyfish.jimmer.apt.immutable.generator.Annotations;
import org.babyfish.jimmer.apt.util.ConverterMetadata;
import org.babyfish.jimmer.apt.util.GenericParser;
import org.babyfish.jimmer.client.*;
import org.babyfish.jimmer.client.meta.*;
import org.babyfish.jimmer.client.meta.impl.*;
import org.babyfish.jimmer.error.*;
import org.babyfish.jimmer.impl.util.StringUtil;
import org.babyfish.jimmer.ClientException;
import org.babyfish.jimmer.sql.Embeddable;
import org.babyfish.jimmer.sql.Entity;
import org.babyfish.jimmer.sql.MappedSuperclass;
import org.jetbrains.annotations.Nullable;

import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.Elements;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;

public class ClientProcessor {

    private static final String JIMMER_CLIENT = "META-INF/jimmer/client";

    private static final TypeName FETCH_BY_NAME = TypeName.of(FetchBy.class);

    private static final TypeName CODE_BASED_EXCEPTION_NAME = TypeName.of(CodeBasedException.class);

    private static final TypeName CODE_BASED_RUNTIME_EXCEPTION_NAME = TypeName.of(CodeBasedRuntimeException.class);

    private final Context context;

    private final ClientExceptionContext clientExceptionContext;

    private final Elements elements;

    private final Collection delayedClientTypeNames;

    private final File jimmerClientFile;

    private final boolean explicitApi;

    private final SchemaBuilder builder;

    private final Set jsonValueTypeNameStack = new HashSet<>();

    public ClientProcessor(
            Context context,
            Elements elements,
            Filer filer,
            boolean explicitApi,
            Collection delayedClientTypeNames
    ) {
        this.context = context;
        this.clientExceptionContext = new ClientExceptionContext(context);
        this.elements = elements;
        this.explicitApi = explicitApi;
        this.delayedClientTypeNames = delayedClientTypeNames;

        FileObject fileObject;
        try {
            fileObject = filer.getResource(StandardLocation.CLASS_OUTPUT, "", JIMMER_CLIENT);
        } catch (IOException ex) {
            throw new GeneratorException("Cannot get file object \"" + JIMMER_CLIENT + "\"", ex);
        }
        jimmerClientFile = new File(fileObject.getName());

        this.builder = new SchemaBuilder(existingSchema()) {

            @Nullable
            @Override
            protected Element loadSource(String typeName) {
                return elements.getTypeElement(typeName);
            }

            @Override
            protected void throwException(Element source, String message) {
                throw new MetaException(source, message);
            }

            @Override
            protected void fillDefinition(Element source) {
                TypeElement typeElement = (TypeElement) source;
                ClientProcessor.this.fillDefinition(
                        typeElement,
                        typeElement.getAnnotation(Immutable.class) != null ||
                                typeElement.getAnnotation(Entity.class) != null ||
                                typeElement.getAnnotation(MappedSuperclass.class) != null ||
                                typeElement.getAnnotation(Embeddable.class) != null
                );
            }
        };
    }

    private Schema existingSchema() {
        if (jimmerClientFile.exists()) {
            try (Reader reader = new InputStreamReader(Files.newInputStream(jimmerClientFile.toPath()), StandardCharsets.UTF_8)) {
                return Schemas.readServicesFrom(reader);
            } catch (IOException ex) {
                throw new GeneratorException("Cannot read content of  \"" + jimmerClientFile + "\"", ex);
            }
        }
        return null;
    }

    public void process(RoundEnvironment roundEnv) {

        checkJdkVersion(roundEnv);

        for (Element element : roundEnv.getRootElements()) {
            handleService(element);
        }

        if (delayedClientTypeNames != null) {
            for (String delayedClientTypeName : delayedClientTypeNames) {
                handleService(context.getElements().getTypeElement(delayedClientTypeName));
            }
        }

        Schema schema = builder.build();
        jimmerClientFile.getParentFile().mkdirs();
        try (Writer writer = new OutputStreamWriter(Files.newOutputStream(jimmerClientFile.toPath()), StandardCharsets.UTF_8)) {
            Schemas.writeTo(schema, writer);
        } catch (IOException ex) {
            throw new GeneratorException("Cannot write \"" + jimmerClientFile + "\"", ex);
        }
    }

    /**
     * Find this problem on `zulu-1.8 jdk`,
     * `TypeMirror.getAnnotationMirrors` always returns empty list if
     * the current `TypeMirror` is not top type but generic argument.
     */
    private void checkJdkVersion(RoundEnvironment roundEnv) {
        try {
            String.class.getMethod("isBlank");
            return;
        } catch (NoSuchMethodException e) {
            // Do nothing
        }
        boolean hasApiService = false;
        for (Element element : roundEnv.getRootElements()) {
            if (isApiService(element)) {
                hasApiService = true;
                break;
            }
        }
        if (!hasApiService && delayedClientTypeNames != null) {
            for (String typeName : delayedClientTypeNames) {
                if (isApiService(context.getElements().getTypeElement(typeName))) {
                    hasApiService = true;
                    break;
                }
            }
        }
        if (hasApiService) {
            throw new FetchByUnsupportedException();
        }
    }

    private void handleService(Element element) {
        if (!(element instanceof TypeElement)) {
            return;
        }
        TypeElement typeElement = (TypeElement) element;
        if (!isApiService(element)) {
            return;
        }
        if (typeElement.getNestingKind().isNested()) {
            throw new MetaException(
                    typeElement,
                    "the API service type must be top-level"
            );
        }
        if (!typeElement.getTypeParameters().isEmpty()) {
            throw new MetaException(
                    typeElement.getTypeParameters().get(0),
                    "API service cannot declare type parameters"
            );
        }
        SchemaImpl schema = builder.current();
        builder.api(typeElement, typeName(typeElement), apiService -> {
            Api api = typeElement.getAnnotation(Api.class);
            if (api != null) {
                apiService.setGroups(Arrays.asList(api.value()));
            }
            apiService.setDoc(Doc.parse(elements.getDocComment(typeElement)));
            for (Element subElement : typeElement.getEnclosedElements()) {
                if (subElement instanceof ExecutableElement && subElement.getAnnotation(ApiIgnore.class) == null) {
                    ExecutableElement executableElement = (ExecutableElement) subElement;
                    if (isApiOperation(executableElement)) {
                        handleMethod(executableElement);
                    }
                }
            }
            schema.addApiService(apiService);
        });
    }

    private void handleMethod(ExecutableElement method) {
        ApiServiceImpl service = builder.current();
        if (!method.getTypeParameters().isEmpty()) {
            throw new MetaException(
                    method.getTypeParameters().get(0),
                    "API method cannot declare type parameters"
            );
        }
        Api api = method.getAnnotation(Api.class);
        if (api == null) {
            boolean matched = false;
            if (explicitApi) {
                for (String autoOperationAnnotation : ApiOperation.AUTO_OPERATION_ANNOTATIONS) {
                    if (Annotations.annotationMirror(method, autoOperationAnnotation) != null) {
                        matched = true;
                        break;
                    }
                }
            }
            if (!matched) {
                return;
            }
        }
        builder.operation(method, method.getSimpleName().toString(), operation -> {
            if (api != null) {
                List groups = Arrays.asList(api.value());
                if (groups.isEmpty()) {
                    groups = null;
                }
                List parentGroups = service.getGroups();
                if (parentGroups != null && groups != null) {
                    Set set = new LinkedHashSet<>(groups);
                    set.retainAll(parentGroups);
                    if (!set.isEmpty()) {
                        throw new MetaException(
                                operation.getSource(),
                                "It cannot be decorated by \"@" +
                                        Api.class +
                                        "\" with `groups` \"" +
                                        set +
                                        "\" because they are not declared in declaring type \"" +
                                        service.getTypeName() +
                                        "\""
                        );
                    }
                }
                operation.setGroups(groups);
            }
            operation.setDoc(Doc.parse(elements.getDocComment(method)));
            int[] indexRef = new int[1];
            for (VariableElement parameterElement : method.getParameters()) {
                builder.parameter(parameterElement, parameterElement.getSimpleName().toString(), parameter -> {
                    parameter.setOriginalIndex(indexRef[0]++);
                    if (Annotations.annotationMirror(parameterElement, ApiIgnore.class) != null) {
                        operation.addIgnoredParameter(parameter);
                    } else {
                        builder.typeRef(type -> {
                            fillType(parameterElement.asType());
                            setNullityByJetBrainsAnnotation(type, parameterElement, parameterElement.asType());
                            parameter.setType(type);
                        });
                        operation.addParameter(parameter);
                    }
                });
            }
            if (method.getReturnType().getKind() != TypeKind.VOID) {
                builder.typeRef(type -> {
                    fillType(method.getReturnType());
                    setNullityByJetBrainsAnnotation(type, method, method.getReturnType());
                    operation.setReturnType(type);
                });
            }
            operation.setExceptionTypeNames(getExceptionTypeNames(method));
            service.addOperation(operation);
        });
    }

    private Set getExceptionTypeNames(ExecutableElement method) {
        List exceptionTypes = method.getThrownTypes();
        if (exceptionTypes.isEmpty()) {
            return Collections.emptySet();
        }
        Set exceptionTypeNames = new LinkedHashSet<>();
        for (TypeMirror type : exceptionTypes) {
            TypeElement typeElement = (TypeElement) context.getTypes().asElement(type);
            if (typeElement.getAnnotation(ClientException.class) != null) {
                collectExceptionTypeNames(clientExceptionContext.get(typeElement), exceptionTypeNames);
            }
        }
        return exceptionTypeNames;
    }

    private void collectExceptionTypeNames(ClientExceptionMetadata metadata, Set exceptionTypeNames) {
        if (metadata.getCode() != null) {
            exceptionTypeNames.add(typeName(metadata.getElement()));
        }
        for (ClientExceptionMetadata subMetadata : metadata.getSubMetdatas()) {
            collectExceptionTypeNames(subMetadata, exceptionTypeNames);
        }
     }

    private void fillType(TypeMirror type) {
        if (type.getKind() != TypeKind.VOID) {
            TypeRefImpl typeRef = builder.current();
            try {
                determineTypeAndArguments(type);
                determineNullity(type);
                determineFetchBy(type);
                removeOptional(typeRef);
            } catch (JsonValueTypeChangeException ex) {
                typeRef.replaceBy(ex.typeRef, typeRef.isNullable() || ex.typeRef.isNullable());
            }
        }
    }

    private void determineFetchBy(TypeMirror entityType) {

        TypeRefImpl typeRef = builder.current();

        AnnotationMirror fetchBy = entityType.getAnnotationMirrors().stream().filter( it ->
                FETCH_BY_NAME.equals(typeName(it.getAnnotationType().asElement()))
        ).findFirst().orElse(null);
        if (fetchBy == null) {
            return;
        }
        if (!context.isEntity(entityType)) {
            throw new MetaException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "Illegal type because \"" +
                            entityType +
                            "\" which is decorated by `@FetchBy` is not entity type"
            );
        }
        String constant = Annotations.annotationValue(fetchBy, "value", null);
        if (constant.isEmpty()) {
            throw new MetaException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "The `value` of `@FetchBy` is required"
            );
        }
        Object owner = Annotations.annotationValue(fetchBy, "ownerType", null);
        if (owner == null || owner.toString().equals("void")) {
            TypeElement element = (TypeElement) builder.ancestorSource(ApiServiceImpl.class, TypeDefinitionImpl.class);
            assert element != null;
            AnnotationMirror defaultFetcherOwner = Annotations.annotationMirror(element, DefaultFetcherOwner.class);
            if (defaultFetcherOwner != null) {
                owner = Annotations.annotationValue(defaultFetcherOwner, "value", null);
            }
            if (owner == null || owner.toString().equals("void")) {
                owner = element.getQualifiedName().toString();
            }
        }

        Element ownerElement = elements.getTypeElement(owner.toString());
        VariableElement fetcherElement = null;
        for (Element element : ownerElement.getEnclosedElements()) {
            if (element.getKind() == ElementKind.FIELD &&
                    element.getModifiers().contains(Modifier.STATIC) &&
                    element.getSimpleName().toString().equals(constant)) {
                fetcherElement = (VariableElement) element;
                break;
            }
        }
        if (fetcherElement == null) {
            throw new MetaException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "Illegal `@FetcherBy`, there is no static field \"" +
                            constant +
                            "\" in entityType \"\"" +
                            owner
            );
        }
        TypeMirror typeMirror = fetcherElement.asType();
        String genericTypeName = null;
        if (typeMirror instanceof DeclaredType) {
            DeclaredType declaredType = (DeclaredType) typeMirror;
            TypeElement element = (TypeElement) declaredType.asElement();
            if (declaredType.getTypeArguments().isEmpty()) {
                genericTypeName = new GenericParser(
                        "fetcher",
                        element,
                        "org.babyfish.jimmer.sql.fetcher.Fetcher"
                ).parse().argumentTypeNames.get(0).toString();
            } else {
                if (!element.getQualifiedName().toString().equals("org.babyfish.jimmer.sql.fetcher.Fetcher")) {
                    throw new MetaException(
                            builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                            builder.ancestorSource(),
                            "Illegal `@FetcherBy`, there is static field \"" +
                                    constant +
                                    "\" in entityType \"\"" +
                                    owner + " but it is not \"org.babyfish.jimmer.sql.fetcher.Fetcher\""
                    );
                }
                genericTypeName = declaredType.getTypeArguments().get(0).toString();
            }
        }
        if (!((TypeElement)((DeclaredType)entityType).asElement()).getQualifiedName().toString().equals(genericTypeName)) {
            throw new MetaException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "Illegal `@FetcherBy`, there is static field \"" +
                            constant +
                            "\" in owner type \"\"" +
                            owner + " but it is not fetcher for \"" +
                            ((TypeElement)((DeclaredType)entityType).asElement()).getQualifiedName() +
                            "\""
            );
        }

        typeRef.setFetchBy(constant);
        typeRef.setFetcherOwner(typeName(ownerElement));
        typeRef.setFetcherDoc(Doc.parse(context.getElements().getDocComment(fetcherElement)));
    }

    private void determineNullity(TypeMirror type) {
        TypeRefImpl typeRef = builder.current();
        boolean isRawTypePrimitive = type.getKind().isPrimitive();
        boolean isPrimitive = typeRef.getTypeName().isPrimitive();
        if (type.getAnnotation(TNullable.class) != null) {
            if (isRawTypePrimitive) {
                throw new MetaException(
                        builder.ancestorSource(),
                        "Illegal annotation `@" +
                                TNullable.class.getName() +
                                "` which cannot be used to decorate primitive type"
                );
            }
            typeRef.setNullable(true);
        }
        if (isPrimitive && !isRawTypePrimitive) {
            TypeRef parentRef = builder.parent(TypeRefImpl.class);
            if (parentRef == null) {
                typeRef.setNullable(true);
            }
        }
    }

    private void determineTypeAndArguments(TypeMirror type) {

        TypeRefImpl typeRef = builder.current();

        switch (type.getKind()) {
            case BOOLEAN:
                typeRef.setTypeName(TypeName.BOOLEAN);
                break;
            case CHAR:
                typeRef.setTypeName(TypeName.CHAR);
                break;
            case BYTE:
                typeRef.setTypeName(TypeName.BYTE);
                break;
            case SHORT:
                typeRef.setTypeName(TypeName.SHORT);
                break;
            case INT:
                typeRef.setTypeName(TypeName.INT);
                break;
            case LONG:
                typeRef.setTypeName(TypeName.LONG);
                break;
            case FLOAT:
                typeRef.setTypeName(TypeName.FLOAT);
                break;
            case DOUBLE:
                typeRef.setTypeName(TypeName.DOUBLE);
                break;
            case TYPEVAR:
                handleTypeVariable((TypeVariable) type);
                break;
            case WILDCARD:
                handleWildcardType((WildcardType) type);
                break;
            case INTERSECTION:
                handleIntersectionType((IntersectionType) type);
                break;
            case ARRAY:
                handleArrayType((ArrayType) type);
                break;
            case DECLARED:
                handleDeclaredType((DeclaredType) type);
                break;
        }
    }

    private void handleTypeVariable(TypeVariable typeVariable) {

        TypeRefImpl typeRef = builder.current();

        Element element = typeVariable.asElement();
        TypeElement parentElement = (TypeElement) element.getEnclosingElement();
        String name = element.getSimpleName().toString();

        typeRef.setTypeName(typeName(parentElement).typeVariable(name));
    }

    private void handleWildcardType(WildcardType wildcardType) {
        TypeMirror typeMirror = wildcardType.getExtendsBound();
        if (typeMirror == null) {
            throw new UnambiguousTypeException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "Client API system does not accept wildcard type without extends bound"
            );
        }
        fillType(typeMirror);
    }

    private void handleIntersectionType(IntersectionType intersectionType) {
        fillType(intersectionType.getBounds().get(0));
    }

    private void handleArrayType(ArrayType arrayType) {
        TypeRefImpl typeRef = builder.current();
        typeRef.setTypeName(TypeName.LIST);
        builder.typeRef(argument -> {
            fillType(arrayType.getComponentType());
            typeRef.addArgument(argument);
        });
    }

    private void handleDeclaredType(DeclaredType declaredType) {

        TypeRefImpl typeRef = builder.current();

        TypeName unboxedTypeName = unboxedTypeName(declaredType);
        if (unboxedTypeName != null) {
            typeRef.setTypeName(unboxedTypeName);
            return;
        }

        TypeElement typeElement = (TypeElement) declaredType.asElement();
        if (typeElement.getNestingKind().isNested() && !typeElement.getModifiers().contains(Modifier.STATIC)) {
            throw new UnambiguousTypeException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "Client API only accept top-level of static nested type"
            );
        }
        TypeName typeName = typeName(typeElement);
        switch (typeName.toString()) {
            case "java.lang.Boolean":
                typeName = TypeName.BOOLEAN;
                break;
            case "java.lang.Character":
                typeName = TypeName.CHAR;
                break;
            case "java.lang.Byte":
                typeName = TypeName.BYTE;
                break;
            case "java.lang.Short":
                typeName = TypeName.SHORT;
                break;
            case "java.lang.Integer":
                typeName = TypeName.INT;
                break;
            case "java.lang.Long":
                typeName = TypeName.LONG;
                break;
            case "java.lang.Float":
                typeName = TypeName.FLOAT;
                break;
            case "java.lang.Double":
                typeName = TypeName.DOUBLE;
                break;
            case "java.lang.Object":
                typeName = TypeName.OBJECT;
                break;
        }

        TypeRefImpl jsonValueTypeRef = jsonValueTypeRef(typeName);
        if (jsonValueTypeRef != null) {
            throw new JsonValueTypeChangeException(jsonValueTypeRef);
        }
        if (!typeElement.getTypeParameters().isEmpty() && declaredType.getTypeArguments().isEmpty()) {
            throw new NoGenericArgumentsException(
                    builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                    builder.ancestorSource(),
                    "Client API system does not accept raw type of generic type"
            );
        }

        typeRef.setTypeName(typeName);
        for (TypeMirror typeMirror : declaredType.getTypeArguments()) {
            builder.typeRef(argument -> {
                fillType(typeMirror);
                typeRef.addArgument(argument);
            });
        }
    }

    @SuppressWarnings("unchecked")
    private TypeRefImpl jsonValueTypeRef(TypeName typeName) {
        TypeElement typeElement = context.getElements().getTypeElement(typeName.toString());
        for (Element element : typeElement.getEnclosedElements()) {
            if (element.getAnnotation(JsonValue.class) == null) {
                continue;
            }
            if (element.getKind() != ElementKind.METHOD || element.getModifiers().contains(Modifier.STATIC)) {
                continue;
            }
            ExecutableElement methodElement = (ExecutableElement) element;
            if (!methodElement.getParameters().isEmpty() || methodElement.getReturnType().getKind() == TypeKind.VOID) {
                continue;
            }
            if (!jsonValueTypeNameStack.add(typeName)) {
                throw new MetaException(
                        builder.ancestorSource(ApiOperationImpl.class, ApiParameterImpl.class),
                        builder.ancestorSource(),
                        "Cannot resolve \"@" +
                                JsonValue.class.getName() +
                                "\" because of dead recursion: " +
                                jsonValueTypeNameStack
                );
            }
            try {
                TypeRefImpl[] jsonValueTypRef = (TypeRefImpl[]) new TypeRefImpl[1];
                builder.typeRef(type -> {
                    fillType(methodElement.getReturnType());
                    jsonValueTypRef[0] = type;
                });
                return jsonValueTypRef[0];
            } finally {
                jsonValueTypeNameStack.remove(typeName);
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private void fillDefinition(TypeElement typeElement, boolean immutable) {

        TypeDefinitionImpl typeDefinition = builder.current();
        typeDefinition.setDoc(Doc.parse(context.getElements().getDocComment(typeElement)));

        if (typeElement.getKind() == ElementKind.ENUM) {
            fillEnumDefinition(typeElement);
            return;
        }

        typeDefinition.setApiIgnore(typeElement.getAnnotation(ApiIgnore.class) != null);
        if (immutable) {
            typeDefinition.setKind(TypeDefinition.Kind.IMMUTABLE);
        } else {
            typeDefinition.setKind(TypeDefinition.Kind.OBJECT);
        }

        if (!immutable || typeElement.getKind() == ElementKind.INTERFACE) {
            boolean isClientException = typeElement.getAnnotation(ClientException.class) != null;
            for (Element element : typeElement.getEnclosedElements()) {
                if (!(element instanceof ExecutableElement)) {
                    continue;
                }
                ExecutableElement executableElement = (ExecutableElement) element;
                if (!executableElement.getParameters().isEmpty() ||
                        executableElement.getModifiers().contains(Modifier.STATIC) ||
                        !executableElement.getModifiers().contains(Modifier.PUBLIC) ||
                        executableElement.getReturnType().getKind() == TypeKind.VOID ||
                        executableElement.getAnnotation(ApiIgnore.class) != null ||
                        executableElement.getAnnotation(JsonIgnore.class) != null
                ) {
                    continue;
                }
                String name = executableElement.getSimpleName().toString();
                if (executableElement.getReturnType().getKind() == TypeKind.BOOLEAN &&
                        name.length() > 2 &&
                        name.startsWith("is") &&
                        !Character.isLowerCase(name.charAt(2))) {
                    name = StringUtil.identifier(name.substring(2));
                } else if (name.length() > 3 &&
                        name.startsWith("get") &&
                        !Character.isLowerCase(name.charAt(3))) {
                    name = StringUtil.identifier(name.substring(3));
                } else {
                    if (!immutable) {
                        continue;
                    }
                }
                if (isClientException && (name.equals("code") || name.equals("fields"))) {
                    continue;
                }
                ConverterMetadata metadata;
                if (immutable) {
                    metadata = context
                            .getImmutableType(typeElement)
                            .getProps()
                            .get(name)
                            .getConverterMetadata();
                } else {
                    metadata = null;
                }
                builder.prop(executableElement, name, prop -> {
                    try {
                        builder.typeRef(type -> {
                            fillType(metadata != null ? metadata.getTargetType() : executableElement.getReturnType());
                            setNullityByJetBrainsAnnotation(type, executableElement, executableElement.getReturnType());
                            prop.setType(type);
                        });
                        prop.setDoc(Doc.parse(elements.getDocComment(executableElement)));
                        typeDefinition.addProp(prop);
                    } catch (UnambiguousTypeException ex) {
                        // Do nothing
                    }
                });
            }
            for (Element fieldElement : typeElement.getEnclosedElements()) {
                if (fieldElement.getKind() != ElementKind.FIELD || fieldElement.getModifiers().contains(Modifier.STATIC)) {
                    continue;
                }
                PropImpl prop = (PropImpl) typeDefinition.getPropMap().get(fieldElement.getSimpleName().toString());
                if (prop == null) {
                    continue;
                }
                if (fieldElement.getAnnotation(JsonIgnore.class) != null) {
                    typeDefinition.getPropMap().remove(fieldElement.getSimpleName().toString());
                }
                if (prop.getDoc() == null) {
                    prop.setDoc(Doc.parse(context.getElements().getDocComment(fieldElement)));
                }
            }
        }

        ClientException clientException = typeElement.getAnnotation(ClientException.class);
        if (clientException != null) {
            ClientExceptionMetadata metadata = clientExceptionContext.get(typeElement);
            if (metadata.getCode() != null && !metadata.getCode().isEmpty()) {
                typeDefinition.setError(
                        new TypeDefinition.Error(
                                metadata.getFamily(),
                                metadata.getCode()
                        )
                );
            }
        }

        if (typeElement.getKind() == ElementKind.CLASS || typeElement.getKind() == ElementKind.INTERFACE) {
            if (typeElement.getSuperclass().getKind() != TypeKind.NONE) {
                Element superElement = ((DeclaredType) typeElement.getSuperclass()).asElement();
                if (Annotations.annotationMirror(superElement, ApiIgnore.class) == null) {
                    TypeName superName = typeName(superElement);
                    if (superName.isGenerationRequired() &&
                            !superName.equals(CODE_BASED_EXCEPTION_NAME) &&
                            !superName.equals(CODE_BASED_RUNTIME_EXCEPTION_NAME)) {
                        builder.typeRef(type -> {
                            fillType(typeElement.getSuperclass());
                            typeDefinition.addSuperType(type);
                        });
                    }
                }
            }
            for (TypeMirror itf : typeElement.getInterfaces()) {
                Element superElement = ((DeclaredType) itf).asElement();
                if (Annotations.annotationMirror(superElement, ApiIgnore.class) == null) {
                    TypeName superName = typeName(superElement);
                    if (superName.isGenerationRequired() && !superName.equals(CODE_BASED_EXCEPTION_NAME)) {
                        builder.typeRef(type -> {
                            fillType(itf);
                            typeDefinition.addSuperType(type);
                        });
                    }
                }
            }
        }
    }

    private void fillEnumDefinition(TypeElement typeElement) {
        TypeDefinitionImpl definition = builder.current();
        definition.setApiIgnore(typeElement.getAnnotation(ApiIgnore.class) != null);
        definition.setKind(TypeDefinition.Kind.ENUM);

        for (Element constantElement : typeElement.getEnclosedElements()) {
            if (constantElement.getKind() != ElementKind.ENUM_CONSTANT) {
                continue;
            }
            builder.constant(constantElement, constantElement.getSimpleName().toString(), constant -> {
                constant.setDoc(Doc.parse(context.getElements().getDocComment(constantElement)));
                definition.addEnumConstant(constant);
            });
        }
    }

    public boolean isApiService(Element element) {
        if (!(element instanceof TypeElement) || !context.include((TypeElement) element)) {
            return false;
        }
        if (element.getAnnotation(ApiIgnore.class) != null) {
            return false;
        }
        if (element.getAnnotation(Api.class) != null) {
            return true;
        }
        if (!explicitApi) {
            return false;
        }
        return Annotations.annotationMirror(
                element,
                "org.springframework.web.bind.annotation.RestController"
        ) != null;
    }

    public boolean isApiOperation(ExecutableElement element) {
        if (!element.getModifiers().contains(Modifier.PUBLIC) || element.getModifiers().contains(Modifier.STATIC)) {
            return false;
        }
        if (element.getAnnotation(ApiIgnore.class) != null) {
            return false;
        }
        if (element.getAnnotation(Api.class) != null) {
            return true;
        }
        if (!explicitApi) {
            return false;
        }
        return ApiOperation.AUTO_OPERATION_ANNOTATIONS.stream().anyMatch(it ->
            Annotations.annotationMirror(element, it) != null
        );
    }

    private void setNullityByJetBrainsAnnotation(TypeRefImpl typeRef, Element element, TypeMirror rawType) {
        if (typeRef.isNullable()) {
            return;
        }
        boolean isPrimitive = typeRef.getTypeName().isPrimitive();
        boolean isRawTypePrimitive = rawType.getKind().isPrimitive();
        String nullableTypeName = null;
        String nonNullTypeName = null;
        for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
            TypeElement annoElement = (TypeElement) annotationMirror.getAnnotationType().asElement();
            String annoClassName = annoElement.getSimpleName().toString();
            String annoClassFullName = annoElement.getQualifiedName().toString();
            if (annoClassName.equals("Null") || annoClassName.equals("Nullable")) {
                if (isRawTypePrimitive) {
                    throw new MetaException(
                            builder.ancestorSource(),
                            "Illegal annotation `@" +
                                    annoClassName +
                                    "` which cannot be used to decorate primitive type"
                    );
                }
                nullableTypeName = annoClassFullName;
            } else if ((annoClassName.equals("NotNull") || annoClassName.equals("NonNull")) &&
                    !annoClassFullName.equals("javax.validation.constraints.NotNull") &&
                    !annoClassFullName.equals("jakarta.validation.constraints.NotNull")) {
                if (isPrimitive && !isRawTypePrimitive) {
                    throw new MetaException(
                            builder.ancestorSource(),
                            "Illegal annotation `@" +
                                    annoClassName +
                                    "` which cannot be used to decorate boxed type of primitive type, " +
                                    "please replace it to unboxed primitive type"
                    );
                }
                nonNullTypeName = annoClassFullName;
            }
        }
        if (nullableTypeName != null && nonNullTypeName != null) {
            throw new MetaException(
                    element,
                    "Conflict nullity annotation \"@" +
                            nullableTypeName +
                            "\" and \"@" +
                            nonNullTypeName +
                            "\""
            );
        }
        if (nullableTypeName != null) {
            typeRef.setNullable(true);
        }
    }

    private static TypeName unboxedTypeName(TypeMirror type) {
        if (!(type instanceof DeclaredType)) {
            return null;
        }
        TypeElement element = (TypeElement) ((DeclaredType) type).asElement();
        switch (element.getQualifiedName().toString()) {
            case "java.lang.Boolean":
                return TypeName.BOOLEAN;
            case "java.lang.Character":
                return TypeName.CHAR;
            case "java.lang.Byte":
                return TypeName.BYTE;
            case "java.lang.Short":
                return TypeName.SHORT;
            case "java.lang.Integer":
                return TypeName.INT;
            case "java.lang.Long":
                return TypeName.LONG;
            case "java.lang.Float":
                return TypeName.FLOAT;
            case "java.lang.Double":
                return TypeName.DOUBLE;
        }
        return null;
    }

    private static TypeName typeName(Element element) {
        List simpleNames = new ArrayList<>();
        String packageName = null;
        for (Element e = element; e != null; e = e.getEnclosingElement()) {
            if (e instanceof TypeElement) {
                simpleNames.add(e.getSimpleName().toString());
            } else if (e instanceof PackageElement) {
                packageName = ((PackageElement) e).getQualifiedName().toString();
            } else {
                break;
            }
        }
        Collections.reverse(simpleNames);
        return TypeName.of(packageName, simpleNames);
    }

    @SuppressWarnings("unchecked")
    private static void removeOptional(TypeRefImpl typeRef) {
        if (typeRef.getTypeName().equals(TypeName.OPTIONAL)) {
            TypeRefImpl target = (TypeRefImpl) typeRef.getArguments().get(0);
            typeRef.replaceBy(target, true);
        }
    }

    private static class UnambiguousTypeException extends MetaException {

        public UnambiguousTypeException(Element element, Element childElement, String reason) {
            super(element, childElement, reason);
        }
    }

    private static class NoGenericArgumentsException extends MetaException {

        public NoGenericArgumentsException(Element element, Element childElement, String reason) {
            super(element, childElement, reason);
        }
    }

    private static class JsonValueTypeChangeException extends RuntimeException {

        final TypeRefImpl typeRef;

        private JsonValueTypeChangeException(TypeRefImpl typeRef) {
            this.typeRef = typeRef;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy