immutables icon indicating copy to clipboard operation
immutables copied to clipboard

Pass annotation to field instead of getter

Open peterjohansen opened this issue 6 years ago • 7 comments
trafficstars

Consider this class:

@Value.Immutable
@Value.Style(passAnnotations = { Id.class })
@Entity
public abstract class HelloWorld {

    @Id
    @GeneratedValue
    public abstract Optional<Long> id();

}

In this case the @Id annotation is placed on the generated getter. Is it possible to specify that it should be placed on the generated field instead? Like so:

...
public final class ImmutableHelloWorld extends HelloWorld {
  @Id // Place it here
  private final @Nullable Long id;

  /**
   * @return The value of the {@code id} attribute
   */
  @Override
  // No @Id annotation here
  public Optional<Long> id() {
    return Optional.ofNullable(id);
  }

  ...
}

I looked at #456 and #475 but it's still not clear to me if this is possible.

peterjohansen avatar Jan 15 '19 15:01 peterjohansen

thank you for raising this. Pass annotation is a special, narrow mechanism, only to replicate same annotation on the generated class on the same element. To generate arbitrary annotation on fields and other derived element, please, use annoration injection mechanism, maven module org.immutables:annotate , see @InjectAnnotation's JavaDoc, there are also some hints in #475

elucash avatar Jan 15 '19 15:01 elucash

I had org.immutables.value.internal.$processor$.meta.$AnnotationInjections.AnnotationInjection imported because I was missing the org.immutables:annotate artifact and I was skeptical about using it since it seemed to be for internal use. Using the one you mentioned worked.

It's a bit disappointing that this requires a separate interface to do. Seems like a very basic and necessary feature when working with libraries that make heavy use of annotations. Are there any technical reasons why it is done this way? Other than that, I really like this library.

For future readers, creating this annotation:

@InjectAnnotation(type = Id.class, target = InjectAnnotation.Where.FIELD)
public @interface InjectJpaId {
}

And then applying it alongside @Id successfully placed the annotation on the field.

peterjohansen avatar Jan 15 '19 17:01 peterjohansen

I agree with @peterjohansen. Having to create extra meta-annotations for every annotation we want to use on a field can be cumbersome when you consider all the possible libraries that may be commonly used (JPA, Jackson, validation, etc.).

For some use-cases such as JPA entities, it's almost more natural to manually create the mutable / modifiable class and have Immutables auto-generate the "immutable" interface and type. By default, all fields in the mutable class would have getter methods in the interface but maybe this could be customized with @Value.Style or other annotation to exclude certain fields, for example. The mutable class could extend the yet-to-be generated interface similar to how it is done currently for builders. Is it possible to do something like this?

rnbrich34 avatar Aug 12 '19 22:08 rnbrich34

@peterjohansen does it work for abstracts though?

I'm trying this:

@Immutable
    @InjectAnnotation(type=ApiModelProperty.class, target = InjectAnnotation.Where.FIELD)
    @ApiModel(value = "test")
    static abstract class AbstractTestAPIRequest implements TestDomain.TestProvider {
        public abstract List<TestItem> getTestItems();
        @ApiModelProperty(value = "bbbb", example = "en")
        public abstract String getLanguage();
    }

But I get an error: '@InjectAnnotation' not applicable to type

I also tried Style(passAnnotations) instead, but in that case, the immutable is not even generated

thiagomgd avatar Jun 10 '20 21:06 thiagomgd

@InjectAnnotation is a meta-annotation:

@InjectAnnotation(type=ApiModelProperty.class, target = InjectAnnotation.Where.FIELD)
@interface ImmModelProp {}


    @Immutable
    @ImmModelProp // <!-- this can be placed on type or individual property
    static abstract class AbstractTestAPIRequest implements TestDomain.TestProvider {
        public abstract List<TestItem> getTestItems();
        @ApiModelProperty(value = "bbbb", example = "en")
        public abstract String getLanguage();
    }

elucash avatar Jun 10 '20 22:06 elucash

how would you create with both a field and a "property" with on the same class? also where's the documentation for this additional library? seems it's not documented, in which case please document.

a popular use case would be jpa entities.

Also, if I understand you correctly I would have to create an annotation for every annotation I want to use? that sounds really annoying for JPA

xenoterracide avatar Mar 18 '21 18:03 xenoterracide

trying to do what you suggested

package com.mckesson.dex.annotation.jpa.immutables;

import javax.persistence.AccessType;
import org.immutables.annotate.InjectAnnotation;

@InjectAnnotation(type= javax.persistence.Access.class, target = { InjectAnnotation.Where.ACCESSOR, InjectAnnotation.Where.FIELD })
public @interface Access {

    /**
     * (Required)  Specification of field- or property-based access.
     */
    AccessType value();
}

given this entity:

package com.mckesson.dex.model.principal;

import java.io.Serializable;
import javax.persistence.AccessType;
import javax.persistence.EnumType;
import javax.persistence.ForeignKey;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import com.mckesson.dex.annotation.jpa.immutables.Access;
import com.mckesson.dex.annotation.jpa.immutables.Cache;
import com.mckesson.dex.annotation.jpa.immutables.Cacheable;
import com.mckesson.dex.annotation.jpa.immutables.Column;
import com.mckesson.dex.annotation.jpa.immutables.Entity;
import com.mckesson.dex.annotation.jpa.immutables.Enumerated;
import com.mckesson.dex.annotation.jpa.immutables.Id;
import com.mckesson.dex.annotation.jpa.immutables.JoinTable;
import com.mckesson.dex.annotation.jpa.immutables.ManyToOne;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.immutables.serial.Serial;
import org.immutables.value.Value;

@Entity
@Cacheable
@Value.Immutable
@Serial.Version( 1L )
@Access(AccessType.FIELD)
@Table(name = "country_subdivision")
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public interface CountrySubdivision extends Serializable {

    @ManyToOne(optional = false)
    @JoinTable(
        name = "jurisdiction_country_subdivision",
        indexes = @Index(name = "idx_jcs_uq", columnList = "jurisdiction,country_subdivision", unique = true),
        joinColumns = @javax.persistence.JoinColumn(name = "country_subdivision"),
        foreignKey = @ForeignKey(name = "mac_juris_juris_fk"),
        inverseJoinColumns = @javax.persistence.JoinColumn(name = "jurisdiction")
    )
    Jurisdiction getJurisdiction();

    @Id
    @NotNull
    @Access(AccessType.PROPERTY)
    @Enumerated(EnumType.STRING)
    @Column(name = "code", length = 3, nullable = false, insertable = false, updatable = false)
    public CountrySubdivisionCode getCode( );
}

it generated this code

package com.mckesson.dex.model.principal;

import com.google.common.base.MoreObjects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.Var;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.persistence.Access;
import javax.persistence.Cacheable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.Cache;
import org.immutables.value.Generated;

/**
 * Immutable implementation of {@link CountrySubdivision}.
 * <p>
 * Use the builder to create immutable instances:
 * {@code ImmutableCountrySubdivision.builder()}.
 */
@Generated(from = "CountrySubdivision", generator = "Immutables")
@SuppressWarnings({"all"})
@ParametersAreNonnullByDefault
@javax.annotation.Generated("org.immutables.processor.ProxyProcessor")
@Immutable
@CheckReturnValue
public final class ImmutableCountrySubdivision implements CountrySubdivision {
  @Access
  private final Jurisdiction jurisdiction;
  @Column
  @Enumerated
  @Access
  private final CountrySubdivisionCode code;

  private ImmutableCountrySubdivision(
      Jurisdiction jurisdiction,
      CountrySubdivisionCode code) {
    this.jurisdiction = jurisdiction;
    this.code = code;
  }

  /**
   * @return The value of the {@code jurisdiction} attribute
   */
  @Entity@ManyToOne@JoinTable@Cacheable@Cache@Access
  @Override
  public Jurisdiction getJurisdiction() {
    return jurisdiction;
  }

  /**
   * @return The value of the {@code code} attribute
   */
  @Column@Entity@Cacheable@Enumerated@Cache@Id@Access
  @Override
  public CountrySubdivisionCode getCode() {
    return code;
  }

  /**
   * Copy the current immutable object by setting a value for the {@link CountrySubdivision#getJurisdiction() jurisdiction} attribute.
   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for jurisdiction
   * @return A modified copy of the {@code this} object
   */
  public final ImmutableCountrySubdivision withJurisdiction(Jurisdiction value) {
    if (this.jurisdiction == value) return this;
    Jurisdiction newValue = Objects.requireNonNull(value, "jurisdiction");
    return new ImmutableCountrySubdivision(newValue, this.code);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link CountrySubdivision#getCode() code} attribute.
   * A value equality check is used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for code
   * @return A modified copy of the {@code this} object
   */
  public final ImmutableCountrySubdivision withCode(CountrySubdivisionCode value) {
    if (this.code == value) return this;
    CountrySubdivisionCode newValue = Objects.requireNonNull(value, "code");
    if (this.code.equals(newValue)) return this;
    return new ImmutableCountrySubdivision(this.jurisdiction, newValue);
  }

  /**
   * This instance is equal to all instances of {@code ImmutableCountrySubdivision} that have equal attribute values.
   * @return {@code true} if {@code this} is equal to {@code another} instance
   */
  @Override
  public boolean equals(@Nullable Object another) {
    if (this == another) return true;
    return another instanceof ImmutableCountrySubdivision
        && equalTo((ImmutableCountrySubdivision) another);
  }

  private boolean equalTo(ImmutableCountrySubdivision another) {
    return jurisdiction.equals(another.jurisdiction)
        && code.equals(another.code);
  }

  /**
   * Computes a hash code from attributes: {@code jurisdiction}, {@code code}.
   * @return hashCode value
   */
  @Override
  public int hashCode() {
    @Var int h = 5381;
    h += (h << 5) + jurisdiction.hashCode();
    h += (h << 5) + code.hashCode();
    return h;
  }

  /**
   * Prints the immutable value {@code CountrySubdivision} with attribute values.
   * @return A string representation of the value
   */
  @Override
  public String toString() {
    return MoreObjects.toStringHelper("CountrySubdivision")
        .omitNullValues()
        .add("jurisdiction", jurisdiction)
        .add("code", code)
        .toString();
  }

  /**
   * Creates an immutable copy of a {@link CountrySubdivision} value.
   * Uses accessors to get values to initialize the new immutable instance.
   * If an instance is already immutable, it is returned as is.
   * @param instance The instance to copy
   * @return A copied immutable CountrySubdivision instance
   */
  public static ImmutableCountrySubdivision copyOf(CountrySubdivision instance) {
    if (instance instanceof ImmutableCountrySubdivision) {
      return (ImmutableCountrySubdivision) instance;
    }
    return ImmutableCountrySubdivision.builder()
        .from(instance)
        .build();
  }

  private static final long serialVersionUID = 1L;

  /**
   * Creates a builder for {@link ImmutableCountrySubdivision ImmutableCountrySubdivision}.
   * <pre>
   * ImmutableCountrySubdivision.builder()
   *    .jurisdiction(com.mckesson.dex.model.principal.Jurisdiction) // required {@link CountrySubdivision#getJurisdiction() jurisdiction}
   *    .code(com.mckesson.dex.model.principal.CountrySubdivisionCode) // required {@link CountrySubdivision#getCode() code}
   *    .build();
   * </pre>
   * @return A new ImmutableCountrySubdivision builder
   */
  public static ImmutableCountrySubdivision.Builder builder() {
    return new ImmutableCountrySubdivision.Builder();
  }

  /**
   * Builds instances of type {@link ImmutableCountrySubdivision ImmutableCountrySubdivision}.
   * Initialize attributes and then invoke the {@link #build()} method to create an
   * immutable instance.
   * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
   * but instead used immediately to create instances.</em>
   */
  @Generated(from = "CountrySubdivision", generator = "Immutables")
  @NotThreadSafe
  public static final class Builder {
    private static final long INIT_BIT_JURISDICTION = 0x1L;
    private static final long INIT_BIT_CODE = 0x2L;
    private long initBits = 0x3L;

    private @Nullable Jurisdiction jurisdiction;
    private @Nullable CountrySubdivisionCode code;

    private Builder() {
    }

    /**
     * Fill a builder with attribute values from the provided {@code CountrySubdivision} instance.
     * Regular attribute values will be replaced with those from the given instance.
     * Absent optional values will not replace present values.
     * @param instance The instance from which to copy values
     * @return {@code this} builder for use in a chained invocation
     */
    @CanIgnoreReturnValue 
    public final Builder from(CountrySubdivision instance) {
      Objects.requireNonNull(instance, "instance");
      jurisdiction(instance.getJurisdiction());
      code(instance.getCode());
      return this;
    }

    /**
     * Initializes the value for the {@link CountrySubdivision#getJurisdiction() jurisdiction} attribute.
     * @param jurisdiction The value for jurisdiction 
     * @return {@code this} builder for use in a chained invocation
     */
    @CanIgnoreReturnValue 
    public final Builder jurisdiction(Jurisdiction jurisdiction) {
      this.jurisdiction = Objects.requireNonNull(jurisdiction, "jurisdiction");
      initBits &= ~INIT_BIT_JURISDICTION;
      return this;
    }

    /**
     * Initializes the value for the {@link CountrySubdivision#getCode() code} attribute.
     * @param code The value for code 
     * @return {@code this} builder for use in a chained invocation
     */
    @CanIgnoreReturnValue 
    public final Builder code(CountrySubdivisionCode code) {
      this.code = Objects.requireNonNull(code, "code");
      initBits &= ~INIT_BIT_CODE;
      return this;
    }

    /**
     * Builds a new {@link ImmutableCountrySubdivision ImmutableCountrySubdivision}.
     * @return An immutable instance of CountrySubdivision
     * @throws java.lang.IllegalStateException if any required attributes are missing
     */
    public ImmutableCountrySubdivision build() {
      if (initBits != 0) {
        throw new IllegalStateException(formatRequiredAttributesMessage());
      }
      return new ImmutableCountrySubdivision(jurisdiction, code);
    }

    private String formatRequiredAttributesMessage() {
      List<String> attributes = new ArrayList<>();
      if ((initBits & INIT_BIT_JURISDICTION) != 0) attributes.add("jurisdiction");
      if ((initBits & INIT_BIT_CODE) != 0) attributes.add("code");
      return "Cannot build CountrySubdivision, some of required attributes are not set " + attributes;
    }
  }
}

I also tried code="[[*]]" and it generated it as @Accessvalue = AccessType.FIELD` with the right import, but that's not a valid annotation usage. If I do this

@InjectAnnotation(type= javax.persistence.Access.class, code="@Access([[*]])", target = { InjectAnnotation.Where.ACCESSOR, InjectAnnotation.Where.FIELD })

it generates the proper annotation on the field but leaves out the import.

version 2.8.2

What gives?

xenoterracide avatar Mar 18 '21 19:03 xenoterracide