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

dev.restate.sdk.gen.ElementConverter Maven / Gradle / Ivy

The newest version!
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.gen;

import dev.restate.sdk.Context;
import dev.restate.sdk.ObjectContext;
import dev.restate.sdk.SharedObjectContext;
import dev.restate.sdk.SharedWorkflowContext;
import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.*;
import dev.restate.sdk.common.ServiceType;
import dev.restate.sdk.gen.model.*;
import dev.restate.sdk.gen.model.Handler;
import dev.restate.sdk.gen.model.Service;
import dev.restate.sdk.gen.utils.AnnotationUtils;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.processing.Messager;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import org.jspecify.annotations.Nullable;

public class ElementConverter {

  private static final PayloadType EMPTY_PAYLOAD =
      new PayloadType(true, "", "Void", "dev.restate.sdk.common.Serde.VOID");
  private static final String RAW_SERDE = "dev.restate.sdk.common.Serde.RAW";

  private final Messager messager;
  private final Elements elements;
  private final Types types;

  public ElementConverter(Messager messager, Elements elements, Types types) {
    this.messager = messager;
    this.elements = elements;
    this.types = types;
  }

  public Service fromTypeElement(TypeElement element) {
    validateType(element);

    dev.restate.sdk.annotation.Service serviceAnnotation =
        element.getAnnotation(dev.restate.sdk.annotation.Service.class);
    dev.restate.sdk.annotation.VirtualObject virtualObjectAnnotation =
        element.getAnnotation(dev.restate.sdk.annotation.VirtualObject.class);
    dev.restate.sdk.annotation.Workflow workflowAnnotation =
        element.getAnnotation(dev.restate.sdk.annotation.Workflow.class);
    boolean isAnnotatedWithService = serviceAnnotation != null;
    boolean isAnnotatedWithVirtualObject = virtualObjectAnnotation != null;
    boolean isAnnotatedWithWorkflow = workflowAnnotation != null;

    // Should be guaranteed by the caller
    assert isAnnotatedWithWorkflow || isAnnotatedWithVirtualObject || isAnnotatedWithService;

    // Check there's no more than one annotation
    if (!Boolean.logicalXor(
        isAnnotatedWithService,
        Boolean.logicalXor(isAnnotatedWithWorkflow, isAnnotatedWithVirtualObject))) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "The type can be annotated only with one annotation between @VirtualObject, @Workflow and @Service",
          element);
    }

    ServiceType type =
        isAnnotatedWithWorkflow
            ? ServiceType.WORKFLOW
            : isAnnotatedWithService ? ServiceType.SERVICE : ServiceType.VIRTUAL_OBJECT;

    // Infer names

    CharSequence targetPkg = elements.getPackageOf(element).getQualifiedName();
    CharSequence targetFqcn = element.getQualifiedName();

    String serviceName =
        isAnnotatedWithService
            ? serviceAnnotation.name()
            : isAnnotatedWithVirtualObject
                ? virtualObjectAnnotation.name()
                : workflowAnnotation.name();
    if (serviceName.isEmpty()) {
      // Use simple class name, flattening subclasses names
      serviceName =
          targetFqcn.toString().substring(targetPkg.length()).replaceAll(Pattern.quote("."), "");
    }

    // Compute handlers
    List handlers =
        elements.getAllMembers(element).stream()
            .filter(e -> e instanceof ExecutableElement)
            .filter(
                e ->
                    e.getAnnotation(dev.restate.sdk.annotation.Handler.class) != null
                        || e.getAnnotation(Workflow.class) != null
                        || e.getAnnotation(Exclusive.class) != null
                        || e.getAnnotation(Shared.class) != null)
            .map(e -> fromExecutableElement(type, ((ExecutableElement) e)))
            .collect(Collectors.toList());

    if (handlers.isEmpty()) {
      messager.printMessage(
          Diagnostic.Kind.WARNING, "The service " + serviceName + " has no handlers", element);
    }

    try {
      return new Service.Builder()
          .withTargetPkg(targetPkg)
          .withTargetFqcn(targetFqcn)
          .withServiceName(serviceName)
          .withServiceType(type)
          .withHandlers(handlers)
          .validateAndBuild();
    } catch (Exception e) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "Can't build the service " + serviceName + ": " + e.getMessage(),
          element);
      return null;
    }
  }

  private void validateType(TypeElement element) {
    if (!element.getTypeParameters().isEmpty()) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "The ServiceProcessor doesn't support services with generics",
          element);
    }
    if (element.getKind().equals(ElementKind.ENUM)) {
      messager.printMessage(
          Diagnostic.Kind.ERROR, "The EntityProcessor doesn't support enums", element);
    }

    if (element.getModifiers().contains(Modifier.PRIVATE)) {
      messager.printMessage(Diagnostic.Kind.ERROR, "The annotated class is private", element);
    }
  }

  private Handler fromExecutableElement(ServiceType serviceType, ExecutableElement element) {
    if (!element.getTypeParameters().isEmpty()) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "The EntityProcessor doesn't support methods with generics",
          element);
    }
    if (element.getKind().equals(ElementKind.CONSTRUCTOR)) {
      messager.printMessage(
          Diagnostic.Kind.ERROR, "You cannot annotate a constructor as Restate method");
    }
    if (element.getKind().equals(ElementKind.STATIC_INIT)) {
      messager.printMessage(
          Diagnostic.Kind.ERROR, "You cannot annotate a static init as Restate method");
    }
    if (element.getModifiers().contains(Modifier.PRIVATE)) {
      messager.printMessage(Diagnostic.Kind.ERROR, "The annotated method is private");
    }

    boolean isAnnotatedWithShared = element.getAnnotation(Shared.class) != null;
    boolean isAnnotatedWithExclusive = element.getAnnotation(Exclusive.class) != null;
    boolean isAnnotatedWithWorkflow = element.getAnnotation(Workflow.class) != null;

    // Check there's no more than one annotation
    boolean hasAnyAnnotation =
        isAnnotatedWithExclusive || isAnnotatedWithShared || isAnnotatedWithWorkflow;
    boolean hasExactlyOneAnnotation =
        Boolean.logicalXor(
            isAnnotatedWithShared,
            Boolean.logicalXor(isAnnotatedWithWorkflow, isAnnotatedWithExclusive));
    if (!(!hasAnyAnnotation || hasExactlyOneAnnotation)) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "You can have only one annotation between @Shared, @Exclusive and @Workflow to a method",
          element);
    }

    HandlerType handlerType =
        isAnnotatedWithWorkflow
            ? HandlerType.WORKFLOW
            : isAnnotatedWithShared
                ? HandlerType.SHARED
                : isAnnotatedWithExclusive
                    ? HandlerType.EXCLUSIVE
                    : defaultHandlerType(serviceType);

    validateMethodSignature(serviceType, handlerType, element);

    try {
      return new Handler.Builder()
          .withName(element.getSimpleName())
          .withHandlerType(handlerType)
          .withInputAccept(inputAcceptFromParameterList(element.getParameters()))
          .withInputType(inputPayloadFromParameterList(element.getParameters()))
          .withOutputType(outputPayloadFromExecutableElement(element))
          .validateAndBuild();
    } catch (Exception e) {
      messager.printMessage(Diagnostic.Kind.ERROR, "Error when building handler: " + e, element);
      return null;
    }
  }

  private HandlerType defaultHandlerType(ServiceType serviceType) {
    switch (serviceType) {
      case SERVICE:
        return HandlerType.STATELESS;
      case VIRTUAL_OBJECT:
        return HandlerType.EXCLUSIVE;
      case WORKFLOW:
        return HandlerType.SHARED;
    }
    throw new IllegalStateException("Unexpected");
  }

  private void validateMethodSignature(
      ServiceType serviceType, HandlerType handlerType, ExecutableElement element) {
    switch (handlerType) {
      case SHARED:
        if (serviceType == ServiceType.WORKFLOW) {
          validateFirstParameterType(SharedWorkflowContext.class, element);
        } else if (serviceType == ServiceType.VIRTUAL_OBJECT) {
          validateFirstParameterType(SharedObjectContext.class, element);
        } else {
          messager.printMessage(
              Diagnostic.Kind.ERROR,
              "The annotation @Shared is not supported by the service type " + serviceType,
              element);
        }
        break;
      case EXCLUSIVE:
        if (serviceType == ServiceType.VIRTUAL_OBJECT) {
          validateFirstParameterType(ObjectContext.class, element);
        } else {
          messager.printMessage(
              Diagnostic.Kind.ERROR,
              "The annotation @Exclusive is not supported by the service type " + serviceType,
              element);
        }
        break;
      case STATELESS:
        validateFirstParameterType(Context.class, element);
        break;
      case WORKFLOW:
        if (serviceType == ServiceType.WORKFLOW) {
          validateFirstParameterType(WorkflowContext.class, element);
        } else {
          messager.printMessage(
              Diagnostic.Kind.ERROR,
              "The annotation @Shared is not supported by the service type " + serviceType,
              element);
        }
        break;
    }
  }

  private void validateFirstParameterType(Class clazz, ExecutableElement element) {
    if (element.getParameters().isEmpty()
        || !types.isSameType(
            element.getParameters().get(0).asType(),
            elements.getTypeElement(clazz.getCanonicalName()).asType())) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "The method signature must have " + clazz.getCanonicalName() + " as first parameter",
          element);
    }
  }

  private String inputAcceptFromParameterList(List element) {
    if (element.size() <= 1) {
      return null;
    }

    Accept accept = element.get(1).getAnnotation(Accept.class);
    if (accept == null) {
      return null;
    }
    return accept.value();
  }

  private PayloadType inputPayloadFromParameterList(List element) {
    if (element.size() <= 1) {
      return EMPTY_PAYLOAD;
    }

    Element parameterElement = element.get(1);
    return payloadFromTypeMirrorAndAnnotations(
        parameterElement.asType(),
        parameterElement.getAnnotation(Json.class),
        parameterElement.getAnnotation(Raw.class),
        parameterElement);
  }

  private PayloadType outputPayloadFromExecutableElement(ExecutableElement element) {
    return payloadFromTypeMirrorAndAnnotations(
        element.getReturnType(),
        element.getAnnotation(Json.class),
        element.getAnnotation(Raw.class),
        element);
  }

  private PayloadType payloadFromTypeMirrorAndAnnotations(
      TypeMirror ty, @Nullable Json jsonAnnotation, @Nullable Raw rawAnnotation, Element element) {
    if (ty.getKind().equals(TypeKind.VOID)) {
      if (rawAnnotation != null || jsonAnnotation != null) {
        messager.printMessage(
            Diagnostic.Kind.ERROR, "Unexpected annotation for void type.", element);
      }
      return EMPTY_PAYLOAD;
    }
    // Some validation
    if (rawAnnotation != null && jsonAnnotation != null) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "A parameter cannot be annotated both with @Raw and @Json.",
          element);
    }
    if (rawAnnotation != null
        && !types.isSameType(ty, types.getArrayType(types.getPrimitiveType(TypeKind.BYTE)))) {
      messager.printMessage(
          Diagnostic.Kind.ERROR,
          "A parameter annotated with @Raw MUST be of type byte[], was " + ty,
          element);
    }

    String serdeDecl = rawAnnotation != null ? RAW_SERDE : jsonSerdeDecl(ty);
    if (rawAnnotation != null
        && !rawAnnotation
            .contentType()
            .equals(AnnotationUtils.getAnnotationDefaultValue(Raw.class, "contentType"))) {
      serdeDecl = contentTypeDecoratedSerdeDecl(serdeDecl, rawAnnotation.contentType());
    }
    if (jsonAnnotation != null
        && !jsonAnnotation
            .contentType()
            .equals(AnnotationUtils.getAnnotationDefaultValue(Json.class, "contentType"))) {
      serdeDecl = contentTypeDecoratedSerdeDecl(serdeDecl, jsonAnnotation.contentType());
    }

    return new PayloadType(false, ty.toString(), boxedType(ty), serdeDecl);
  }

  private static String contentTypeDecoratedSerdeDecl(String serdeDecl, String contentType) {
    return "dev.restate.sdk.common.Serde.withContentType(\""
        + contentType
        + "\", "
        + serdeDecl
        + ")";
  }

  private static String jsonSerdeDecl(TypeMirror ty) {
    switch (ty.getKind()) {
      case BOOLEAN:
        return "dev.restate.sdk.JsonSerdes.BOOLEAN";
      case BYTE:
        return "dev.restate.sdk.JsonSerdes.BYTE";
      case SHORT:
        return "dev.restate.sdk.JsonSerdes.SHORT";
      case INT:
        return "dev.restate.sdk.JsonSerdes.INT";
      case LONG:
        return "dev.restate.sdk.JsonSerdes.LONG";
      case CHAR:
        return "dev.restate.sdk.JsonSerdes.CHAR";
      case FLOAT:
        return "dev.restate.sdk.JsonSerdes.FLOAT";
      case DOUBLE:
        return "dev.restate.sdk.JsonSerdes.DOUBLE";
      case VOID:
        return "dev.restate.sdk.common.Serde.VOID";
      default:
        // Default to Jackson type reference serde
        return "dev.restate.sdk.serde.jackson.JacksonSerdes.of(new com.fasterxml.jackson.core.type.TypeReference<"
            + ty
            + ">() {})";
    }
  }

  private static String boxedType(TypeMirror ty) {
    switch (ty.getKind()) {
      case BOOLEAN:
        return "Boolean";
      case BYTE:
        return "Byte";
      case SHORT:
        return "Short";
      case INT:
        return "Integer";
      case LONG:
        return "Long";
      case CHAR:
        return "Char";
      case FLOAT:
        return "Float";
      case DOUBLE:
        return "Double";
      case VOID:
        return "Void";
      default:
        return ty.toString();
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy