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

dart.henson.processor.IntentBuilderGenerator Maven / Gradle / Ivy

/*
 * Copyright 2013 Jake Wharton
 * Copyright 2014 Prateek Srivastava (@f2prateek)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dart.henson.processor;

import static com.squareup.javapoet.ClassName.get;
import static dart.common.util.ExtraBindingTargetUtil.BUNDLE_BUILDER_SUFFIX;
import static dart.common.util.ExtraBindingTargetUtil.NEXT_STATE_METHOD;

import android.content.Intent;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import dart.common.BaseGenerator;
import dart.common.ExtraBindingTarget;
import dart.common.ExtraInjection;
import dart.common.FieldBinding;
import dart.henson.ActivityClassFinder;
import dart.henson.AllRequiredSetState;
import dart.henson.Bundler;
import dart.henson.RequiredStateSequence;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;

public class IntentBuilderGenerator extends BaseGenerator {

  static final String INITIAL_STATE_CLASS = "InitialState";
  static final String REQUIRED_SEQUENCE_CLASS = "RequiredSequence";
  static final String RESOLVED_OPTIONAL_SEQUENCE_CLASS = "ResolvedAllSet";
  private static final String INITIAL_STATE_METHOD = "getInitialState";
  private static final String OPTIONAL_SEQUENCE_CLASS = "AllSet";
  private static final String OPTIONAL_SEQUENCE_GENERIC = "ALL_SET";
  private static final String OPTIONAL_SEQUENCE_SUBCLASS_GENERIC = "SELF";
  private static final String REQUIRED_SEQUENCE_INTERMEDIARY_CLASS_PREFIX = "AfterSetting";

  private final ExtraBindingTarget target;

  public IntentBuilderGenerator(ExtraBindingTarget target) {
    this.target = target;
  }

  @Override
  public String brewJava() {
    TypeSpec.Builder intentBuilderTypeBuilder =
        TypeSpec.classBuilder(builderClassName()).addModifiers(Modifier.PUBLIC);

    emitInitialStateGetter(intentBuilderTypeBuilder);
    emitNextStateGetter(intentBuilderTypeBuilder);
    emitExtraDSLStateMachine(intentBuilderTypeBuilder);
    emitResolvedOptionalSequence(intentBuilderTypeBuilder);
    emitInitialState(intentBuilderTypeBuilder);

    //build
    JavaFile javaFile =
        JavaFile.builder(target.classPackage, intentBuilderTypeBuilder.build())
            .addFileComment("Generated code from Henson. Do not modify!")
            .addStaticImport(ActivityClassFinder.class, "getClassDynamically")
            .build();
    return javaFile.toString();
  }

  @Override
  public String getFqcn() {
    return target.classPackage + "." + builderClassName();
  }

  private String builderClassName() {
    return target.className + BUNDLE_BUILDER_SUFFIX;
  }

  private void emitInitialStateGetter(TypeSpec.Builder intentBuilderTypeBuilder) {
    MethodSpec.Builder initialStateGetterForHensonBuilder =
        MethodSpec.methodBuilder(INITIAL_STATE_METHOD)
            .addModifiers(Modifier.PUBLIC)
            .addModifiers(Modifier.STATIC)
            .addParameter(get("android.content", "Context"), "context")
            .returns(get(target.classPackage, builderClassName(), INITIAL_STATE_CLASS));

    initialStateGetterForHensonBuilder.addStatement(
        "final $T intent = new $T(context, getClassDynamically($S))",
        Intent.class,
        Intent.class,
        target.getFQN());
    initialStateGetterForHensonBuilder.addStatement(
        "final $T bundler = $T.create()", Bundler.class, Bundler.class);
    initialStateGetterForHensonBuilder.addStatement(
        "return new $L(bundler, intent)", INITIAL_STATE_CLASS);

    intentBuilderTypeBuilder.addMethod(initialStateGetterForHensonBuilder.build());
  }

  private void emitNextStateGetter(TypeSpec.Builder intentBuilderTypeBuilder) {
    final TypeName initialStateGeneric = getInitialStateGeneric(false);
    MethodSpec.Builder initialStateGetterForSubBuilder =
        MethodSpec.methodBuilder(NEXT_STATE_METHOD)
            .addModifiers(Modifier.PUBLIC)
            .addModifiers(Modifier.STATIC)
            .addTypeVariable((TypeVariableName) initialStateGeneric)
            .addParameter(Bundler.class, "bundler")
            .addParameter(initialStateGeneric, "allSetState")
            .returns(getInitialStateType(initialStateGeneric));

    if (target.hasRequiredFields) {
      initialStateGetterForSubBuilder.addStatement(
          "return new $L<>(bundler, allSetState)", REQUIRED_SEQUENCE_CLASS);
      intentBuilderTypeBuilder.addMethod(initialStateGetterForSubBuilder.build());
      return;
    }

    if (target.parentPackage != null) {
      final String parentIntentBuilderClass = target.parentClass + BUNDLE_BUILDER_SUFFIX;
      initialStateGetterForSubBuilder.addStatement(
          "return $T.getNextState(bundler, allSetState)",
          get(target.parentPackage, parentIntentBuilderClass));
      intentBuilderTypeBuilder.addMethod(initialStateGetterForSubBuilder.build());
      return;
    }

    initialStateGetterForSubBuilder.addStatement("return allSetState");
    intentBuilderTypeBuilder.addMethod(initialStateGetterForSubBuilder.build());
  }

  private void emitExtraDSLStateMachine(TypeSpec.Builder intentBuilderTypeBuilder) {
    //separate required extras from optional extras and sort both sublists.
    List requiredInjections = new ArrayList<>();
    List optionalInjections = new ArrayList<>();
    for (ExtraInjection extraInjection : target.bindingMap.values()) {
      if (!extraInjection.getRequiredBindings().isEmpty()) {
        requiredInjections.add(extraInjection);
      } else {
        optionalInjections.add(extraInjection);
      }
    }

    final ExtraInjectionComparator extraInjectionComparator = new ExtraInjectionComparator();
    Collections.sort(requiredInjections, extraInjectionComparator);
    Collections.sort(optionalInjections, extraInjectionComparator);

    emitRequiredSequence(intentBuilderTypeBuilder, requiredInjections);
    emitOptionalSequence(intentBuilderTypeBuilder, optionalInjections);
  }

  private void emitRequiredSequence(
      TypeSpec.Builder intentBuilderTypeBuilder, List requiredInjections) {
    if (!target.hasRequiredFields) {
      return;
    }
    final TypeName generic = getInitialStateGeneric(false);

    TypeSpec.Builder requiredSequenceBuilder =
        TypeSpec.classBuilder(REQUIRED_SEQUENCE_CLASS)
            .superclass(ParameterizedTypeName.get(get(RequiredStateSequence.class), generic))
            .addTypeVariable((TypeVariableName) generic)
            .addModifiers(Modifier.PUBLIC)
            .addModifiers(Modifier.STATIC);

    MethodSpec.Builder constructorBuilder =
        MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(Bundler.class, "bundler")
            .addParameter(generic, "allRequiredSetState")
            .addStatement("super(bundler, allRequiredSetState)");

    requiredSequenceBuilder.addMethod(constructorBuilder.build());

    TypeSpec.Builder builderStateClass = requiredSequenceBuilder;
    for (int i = 0; i < requiredInjections.size(); i++) {
      final boolean isLast = i == requiredInjections.size() - 1;
      final ExtraInjection binding = requiredInjections.get(i);
      final String nextClass = emitRequiredSetter(builderStateClass, binding, generic, isLast);
      builderStateClass = rotateBuilderState(requiredSequenceBuilder, builderStateClass, nextClass);
    }

    intentBuilderTypeBuilder.addType(requiredSequenceBuilder.build());
  }

  private void emitOptionalSequence(
      TypeSpec.Builder intentBuilderTypeBuilder, List optionalInjections) {
    // find type
    final ClassName optionalSequence =
        get(target.classPackage, builderClassName(), OPTIONAL_SEQUENCE_CLASS);
    final ParameterizedTypeName parameterizedOptionalSequence =
        ParameterizedTypeName.get(
            optionalSequence, TypeVariableName.get(OPTIONAL_SEQUENCE_SUBCLASS_GENERIC));
    final TypeVariableName typeVariable =
        TypeVariableName.get(OPTIONAL_SEQUENCE_SUBCLASS_GENERIC, parameterizedOptionalSequence);

    // find superclass
    final TypeName superClass;
    if (target.parentPackage != null) {
      final ClassName parentOptionalSequence =
          get(
              target.parentPackage,
              target.parentClass + BUNDLE_BUILDER_SUFFIX,
              OPTIONAL_SEQUENCE_CLASS);
      superClass = ParameterizedTypeName.get(parentOptionalSequence, typeVariable);
    } else {
      superClass = get(AllRequiredSetState.class);
    }

    TypeSpec.Builder optionalSequenceBuilder =
        TypeSpec.classBuilder(OPTIONAL_SEQUENCE_CLASS)
            .superclass(superClass)
            .addTypeVariable(typeVariable)
            .addModifiers(Modifier.PUBLIC)
            .addModifiers(Modifier.STATIC);

    MethodSpec.Builder constructorBuilder =
        MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(Bundler.class, "bundler")
            .addParameter(get("android.content", "Intent"), "intent")
            .addStatement("super(bundler, intent)");

    optionalSequenceBuilder.addMethod(constructorBuilder.build());

    for (int i = 0; i < optionalInjections.size(); i++) {
      emitOptionalSetter(optionalSequenceBuilder, optionalInjections.get(i), typeVariable);
    }

    intentBuilderTypeBuilder.addType(optionalSequenceBuilder.build());
  }

  private void emitResolvedOptionalSequence(TypeSpec.Builder intentBuilderTypeBuilder) {
    // find superclass
    final ClassName optionalSequence =
        get(target.classPackage, builderClassName(), OPTIONAL_SEQUENCE_CLASS);
    final ClassName resolvedOptional =
        get(target.classPackage, builderClassName(), RESOLVED_OPTIONAL_SEQUENCE_CLASS);

    TypeSpec.Builder resolvedOptionalSequenceBuilder =
        TypeSpec.classBuilder(RESOLVED_OPTIONAL_SEQUENCE_CLASS)
            .superclass(ParameterizedTypeName.get(optionalSequence, resolvedOptional))
            .addModifiers(Modifier.PUBLIC)
            .addModifiers(Modifier.STATIC);

    MethodSpec.Builder constructorBuilder =
        MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(Bundler.class, "bundler")
            .addParameter(get("android.content", "Intent"), "intent")
            .addStatement("super(bundler, intent)");

    resolvedOptionalSequenceBuilder.addMethod(constructorBuilder.build());
    intentBuilderTypeBuilder.addType(resolvedOptionalSequenceBuilder.build());
  }

  public void emitInitialState(TypeSpec.Builder intentBuilderTypeBuilder) {
    TypeSpec.Builder initialStateBuilder =
        TypeSpec.classBuilder(INITIAL_STATE_CLASS)
            .superclass(getInitialStateType(getInitialStateGeneric(true)))
            .addModifiers(Modifier.PUBLIC)
            .addModifiers(Modifier.STATIC);

    MethodSpec.Builder constructorBuilder =
        MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(Bundler.class, "bundler")
            .addParameter(get("android.content", "Intent"), "intent");

    if (!target.hasRequiredFields && target.closestRequiredAncestorPackage == null) {
      constructorBuilder.addStatement("super(bundler, intent)");
    } else {
      constructorBuilder.addStatement(
          "super(bundler, new $L(bundler, intent))", RESOLVED_OPTIONAL_SEQUENCE_CLASS);
    }

    initialStateBuilder.addMethod(constructorBuilder.build());
    intentBuilderTypeBuilder.addType(initialStateBuilder.build());
  }

  /**
   * @param builder the intent builder in which to emit.
   * @param binding the binding to emit.
   * @param generic generic value.
   * @param isLast whether or not the binding is the last mandatory one.
   * @return the name of the next state class to create
   */
  private String emitRequiredSetter(
      TypeSpec.Builder builder, ExtraInjection binding, TypeName generic, boolean isLast) {
    final Collection fieldBindings = binding.getFieldBindings();
    if (fieldBindings.isEmpty()) {
      return null;
    }

    // only used for non-last iterations, but needed for the algorithm
    String nextIntermediary =
        REQUIRED_SEQUENCE_INTERMEDIARY_CLASS_PREFIX + capitalize(binding.getKey());
    ;
    // find next intermediary state
    final TypeName nextState;
    if (isLast) {
      if (target.closestRequiredAncestorPackage == null) {
        nextState = generic;
      } else {
        final String closestRequiredAncestorIntentBuilderClass =
            target.closestRequiredAncestorClass + BUNDLE_BUILDER_SUFFIX;
        final ClassName requiredSequence =
            get(
                target.closestRequiredAncestorPackage,
                closestRequiredAncestorIntentBuilderClass,
                REQUIRED_SEQUENCE_CLASS);
        nextState = ParameterizedTypeName.get(requiredSequence, generic);
      }
    } else {
      nextState =
          get(target.classPackage, builderClassName(), REQUIRED_SEQUENCE_CLASS, nextIntermediary);
    }

    final FieldBinding firstFieldBinding = fieldBindings.iterator().next();
    final TypeMirror extraType = firstFieldBinding.getType();
    final String castToParcelableIfNecessary = doCreateParcelableCastIfExtraIsParcelable(extraType);
    final String value = extractValue(firstFieldBinding);

    MethodSpec.Builder setterBuilder =
        MethodSpec.methodBuilder(binding.getKey())
            .addModifiers(Modifier.PUBLIC)
            .addParameter(TypeName.get(extraType), firstFieldBinding.getName())
            .returns(nextState)
            .addStatement(
                "bundler.put($S," + castToParcelableIfNecessary + " $L)", binding.getKey(), value);

    // find return statement
    if (isLast) {
      if (target.parentPackage != null) {
        final String parentIntentBuilderClass = target.parentClass + BUNDLE_BUILDER_SUFFIX;
        setterBuilder.addStatement(
            "return $T.getNextState(bundler, allRequiredSetState)",
            get(target.parentPackage, parentIntentBuilderClass));
      } else {
        setterBuilder.addStatement("return allRequiredSetState");
      }
    } else {
      setterBuilder.addStatement("return new $T()", nextState);
    }

    builder.addMethod(setterBuilder.build());
    return nextIntermediary;
  }

  /**
   * @param builder the intent builder in which to emit.
   * @param binding the binding to emit.
   * @param generic generic value.
   */
  private void emitOptionalSetter(
      TypeSpec.Builder builder, ExtraInjection binding, TypeName generic) {
    Collection fieldBindings = binding.getFieldBindings();
    if (fieldBindings.isEmpty()) {
      return;
    }

    FieldBinding firstFieldBinding = fieldBindings.iterator().next();
    TypeMirror extraType = firstFieldBinding.getType();
    String castToParcelableIfNecessary = doCreateParcelableCastIfExtraIsParcelable(extraType);
    final String value = extractValue(firstFieldBinding);

    MethodSpec.Builder setterBuilder =
        MethodSpec.methodBuilder(binding.getKey())
            .addModifiers(Modifier.PUBLIC)
            .addParameter(TypeName.get(extraType), firstFieldBinding.getName())
            .returns(generic)
            .addStatement(
                "bundler.put($S," + castToParcelableIfNecessary + " $L)", binding.getKey(), value)
            .addStatement("return ($T) this", generic);

    builder.addMethod(setterBuilder.build());
  }

  private TypeSpec.Builder rotateBuilderState(
      TypeSpec.Builder builder, TypeSpec.Builder builderStateClass, String nextStateClassName) {
    if (builderStateClass != builder) {
      builder.addType(builderStateClass.build());
    }
    //prepare next state class
    builderStateClass = TypeSpec.classBuilder(nextStateClassName).addModifiers(Modifier.PUBLIC);
    return builderStateClass;
  }

  private TypeName getInitialStateType(TypeName generic) {
    if (target.hasRequiredFields) {
      final ClassName requiredSequence =
          get(target.classPackage, builderClassName(), REQUIRED_SEQUENCE_CLASS);
      return ParameterizedTypeName.get(requiredSequence, generic);
    }
    if (target.closestRequiredAncestorPackage != null) {
      final String closestRequiredAncestorIntentBuilderClass =
          target.closestRequiredAncestorClass + BUNDLE_BUILDER_SUFFIX;
      final ClassName requiredSequence =
          get(
              target.closestRequiredAncestorPackage,
              closestRequiredAncestorIntentBuilderClass,
              REQUIRED_SEQUENCE_CLASS);
      return ParameterizedTypeName.get(requiredSequence, generic);
    }
    return generic;
  }

  private TypeName getInitialStateGeneric(boolean resolved) {
    if (resolved) {
      return get(target.classPackage, builderClassName(), RESOLVED_OPTIONAL_SEQUENCE_CLASS);
    }
    final ClassName optionalSequence =
        get(target.classPackage, builderClassName(), OPTIONAL_SEQUENCE_CLASS);
    return TypeVariableName.get(OPTIONAL_SEQUENCE_GENERIC, optionalSequence);
  }

  /**
   * This method returns either an empty String or {@code "(Parcelable)"} if the extra type is
   * Parcelable. We need this explicit conversion in cases where the extra type is both Parcelable
   * and Serializable. In that case we will prefer Parcelable. Not that the extra type has to
   * directly implement Parcelable, not via a super class.
   *
   * @param extraType the type that might be parcelable.
   * @return either an empty String or {@code "(Parcelable)"} if the extra type is Parcelable
   */
  private String doCreateParcelableCastIfExtraIsParcelable(TypeMirror extraType) {
    String castToParcelableIfNecessary = "";
    if (extraType instanceof DeclaredType) {
      boolean isParcelable = false;
      final TypeElement typeElement = (TypeElement) ((DeclaredType) extraType).asElement();
      for (TypeMirror interfaceType : typeElement.getInterfaces()) {
        if ("android.os.Parcelable".equals(interfaceType.toString())) {
          isParcelable = true;
        }
      }

      if (isParcelable) {
        castToParcelableIfNecessary = "(android.os.Parcelable)";
      }
    }
    return castToParcelableIfNecessary;
  }

  private String extractValue(FieldBinding firstFieldBinding) {
    final String value;
    if (firstFieldBinding.isParcel()) {
      value = "org.parceler.Parcels.wrap(" + firstFieldBinding.getName() + ')';
    } else {
      value = firstFieldBinding.getName();
    }
    return value;
  }

  private String capitalize(String key) {
    if (key == null) {
      throw new IllegalArgumentException("Key can't be null.");
    }

    if (key.length() == 0) {
      return "";
    }

    char firstUpper = Character.toUpperCase(key.charAt(0));
    if (key.length() == 1) {
      return Character.toString(firstUpper);
    }

    String rest = key.substring(1);
    return firstUpper + rest;
  }

  private static class ExtraInjectionComparator implements Comparator {
    @Override
    public int compare(ExtraInjection o1, ExtraInjection o2) {
      return o1.getKey().compareTo(o2.getKey());
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy