Skip to content

Latest commit

 

History

History
308 lines (211 loc) · 19.1 KB

DOPPELGANGER.adoc

File metadata and controls

308 lines (211 loc) · 19.1 KB

Doppelganger

Install

The annotation processing system will pick up on Doppelganger annotated classes during compilation given the following included dependencies. Besides Doppelganger itself, you’ll need the annotation subsystem (currently in javax.* but moving to the javax.* namespace), and our JSON scaffolding helper classes.

Doppelganger
<dependency>
  <groupId>org.smallmind</groupId>
  <artifactId>web-json-doppelganger</artifactId>
  <version>LATEST</version>
</dependency>

Raison D’etre

Well designed REST APIs are often backed by a clear services layer, which, in turn, will depend upon database mapped entities (via orthogonal persistence frameworks, like JPA or Throng). Although it may be tempting to shortcut a lot of hassle by simply annotating database entities as the medium of exchange for the REST APIs, as either input parameters our output return values, doing so is usually a mistake. It binds the REST APIs too tightly to both the services business logic, and the particulars of database tables. As requirements change, and the backend evolves, the whole becomes a mess of leaking abstractions. The higher road involve creating a set of 'data transfer objects' to be used within the client facing APIs, which in turn provide evolutionary freedom to what are connected but loosely coupled layers. However, doing the right thing can mean a lot of boiler-plate code and documentation overhead. The Doppelganger annotations embodied in this project relieve much of the burden, auto-generating the views of your entities, while acting as documentation in code (the best kind), while providing the loose coupling necessary to accommodate the independent evolution of the application layers.

Exposition

The Doppelganger annotation framework creates JAXB annotated classes intended for JSON encoding/decoding via the Jackson JSON library. The specificity of this dependency comes from the use of Jackson’s com.fasterxml.jackson.databind.JsonNode in the polymorphic adapters. The annotations provided by this project are capable of creating multiple, finely differentiated views, from a single annotated class. Any field in the annotated class (or 'virtual' fields not present in the class at all) may appear, with different validation constraints, in any of several views. The views are differentiated by visibility (see Visibility below) and, possibly, a freely definable purpose string. The visibility, purpose, and any validation constraints, are held by an Idiom. Many fields may be present in the same idiom, and the set of all defined idioms will determine the set of views generated during annotation processing. The name of each view follows the pattern [Annotated Class Name]<Purpose>[Visibility]View, with each view being generated in the same package as its annotated class.

Tip
For example, given the annotated com.mycompany.MyClass with some fields with Visibility.IN and purposes 'create' and 'update', and some fields with Visibility.OUT and the single purpose 'read', the annotation processor will generate the views com.mycompany.MyClassCreateInView, com.mycompany.MyClassUpdateInView and com.mycompany.MyClassReadOutView.

There’s no strict requirement that a view follow the meaning embodied in its name. In other words, a 'CreateIn' view need not be used for create operations, nor even as an input parameter. However, naming is important, and we think it best to try to name both consistently and with meaning.

Visibility

org.smallmind.web.json.doppelganger.Visibility

The Visibility enum has 3 values, IN, OUT and BOTH (equivalent to having both IN and OUT visibility). The intent, given this project’s focus on JSON encoded data transfer objects (views), is that IN views will be usd as REST parameters, while OUT views will be used as REST return values.

Purpose

The use of purposes is optional, but helps to distinguish input parameters or return values by their particular operations or contexts. Some annotated fields may be required for a create operation, for example, but might be either optional or disallowed in an update operation. This can be accomplished by varying validation constraints between the two, or leaving a field completely out of the latter.

Annotations

@Doppelganger

Holds the base rules for generating the views of the annotated class. The following attributes control the specifics…​

  • String name () default "" - The root name of the element, if empty it will default to the simple name of the annotated class.

  • Polymorphic polymorphic () default @Polymorphic() - The rules for handling polymorphic encoding / decoding (see below). The default presumes a non-polymorphic type.

  • Import[] imports () default {} - A list of additional imports to add to the generated classes, for convenience.

  • Implementation[] implementations () default {} - A list of additional interfaces to be adopted into the implements clause of the generated classes

  • Idiom[] constrainingIdioms () default {} - The list of class level validation constraints to apply to the generated view given the matching idioms (see below).

  • Virtual[] virtual () default {} - The list of virtual properties to be added to the view (see below).

  • Real[] real () default {} - The list of real properties to be added to the view (see below). These should be added via direct @View annotations, but sometimes that won’t be possible and listing them via this property will make sense.

  • Pledge[] pledges () default {} - The list of conditions under which to guarantee a view is generated (only necessary when the view would otherwise not be generated, see below).

  • String comment () default "" - A notation passed into generated classes by annotating the generated types with a @Comment (see below) containing this text. If this value is left blank, then no @Comment will be generated.

@Polymorphic

The rules for generating the views of the polymorphic subclasses of the annotated class.

  • Class[] subClasses () default {} -The list of sub-classes which will be generated with polymorphic annotations.

  • boolean useAttribute () default false - If false, polymorphic subclasses will be generated with a wrapping object, otherwise the subclasses will be genetared with an extra object type attribute.

    Note

    Given a polymorphic subtype named MySubClass with a doppelganger name of "mySubClass", if useAttribute() is false the following JSON would be generated…​

    {"mySubClass":  {... other attributes ...}}

    …​otherwise, if useAttribute() is true the following JSON would be generated…​

    {... other attributes ..., "java/object": "mySubClass"}

@Import

A description of a set of imports which should be added to the generated classes.

  • Visibility visibility () default Visibility.BOTH - The visibility for which these imports should be added, defaults to both in and out views.

  • String[] purposes () default {} - Any purposes for which these imports should be added. If empty then only the default (un-named) purpose will be enforced.

  • String[] value() - An array of imports as valid class names or wildcard import specifications.

@Implementation

A description of a set of interfaces which should be adopted into the implements clause of the generated classes.

  • Visibility visibility () default Visibility.BOTH - The visibility for which these implementations should be adopted, defaults to both in and out views.

  • String[] purposes () default {} - Any purposes for which these implementations should be adopted. If empty then only the default (un-named) purpose will be enforced.

  • Class[] value() - An array of interfaces which should be adopted.

@Constraint

A representation of a javax.validation.Constraint annotation instance which should be added to the generated class or property.

  • Class<?> value () - The class of the javax.validation.Constraint annotation to be applied.

  • String arguments () default "" - The arguments to the validation constraint, given as the text which would otherwise be placed within the () of the annotation, were it used in a more natural context.

    Tip

    For example, applying a numerical minimum validation of '3' could be accomplished with the following…​

    @Constraint(value = Min.class, arguments = "3")

@Virtual

Creates a 'virtual' property, which exists only in the generated views (and not the annotated class).

  • String name () default "" - The name of the JSON attribute generated for the annotated property. If left empty the attribute name will be the same as the field name (see below).

  • Type type () - The type information for the generated property (see below).

  • String field () - The field name of the generated property.

  • Idiom[] idioms () default {} - The list of alternate idioms in which this property should be included (see below). If empty, this property will be included in the default idiom.

  • Class<? extends XmlAdapter> adapter () default NullXmlAdapter.class - The XmlAdapter class, if any, to be used for encoding and decoding this property.

  • Class<?> as () default Void.class - A type hint for tools which may process Doppelganger generated views.

  • boolean required () default false - If the generated JSON element is required. If false, this may be overridden by the idioms (see above). Although an element may be marked as required, the enforcement of this is erratic at best. The use of a NotNull constraint is a far more persuasive argument.

  • String comment () default "" - A notation passed into generated classes by annotating the generated field with a @Comment (see below) containing this text. If this value is left blank, then no @Comment will be generated.

@Real

Creates a reference to a 'real' property of the annotated class. The better way to handle real properties are through @View annotations (see below) directly on the appropriate fields or getters/setters. However, when surfacing fields from non-annotated super classes, or when annotating such a super class would be difficult due the inability to pre-define all of its polymorphic sub-classes, it can be better to treat these fields on a case-by-case basis via this annotation.

  • String name () default "" - The name of the JSON attribute generated for the annotated property. If left empty the attribute name will be the same as the field name (see below).

  • Type type () - The type information for the referenced property (see below).

  • String field () - The field name of the referenced property.

  • Idiom[] idioms () default {} - The list of alternate idioms in which this property should be included (see below). If empty, this property will be included in the default idiom.

  • Class<? extends XmlAdapter> adapter () default NullXmlAdapter.class - The XmlAdapter class, if any, to be used for encoding and decoding this property.

  • Class<?> as () default Void.class - A type hint for tools which may process Doppelganger generated views.

  • boolean required () default false - If the generated JSON element is required. If false, this may be overridden by the idioms (see above). Although an element may be marked as required, the enforcement of this is erratic at best. The use of a NotNull constraint is a far more persuasive argument.

  • String comment () default "" - A notation passed into generated classes by annotating the generated field with a @Comment (see below) containing this text. If this value is left blank, then no @Comment will be generated.

@Type

Represents the type information of a virtual property.

  • Class<?> value () - The class of the generated property.

  • Class[] parameters () default {} - The classes of any parameterizations (generics) of the generated property (useful for collections).

@Idiom

Idioms are the way to differentiate views. There’s the basic differentiation of in or out views, and these can be further decomposed into arbitrary purposes. Each idiom may be marked as required, or not, and may have set of validation constraints applied.

  • Visibility visibility () default Visibility.BOTH - The visibility of the property within this idiom (IN, OUT or default to BOTH).

  • String[] purposes () default {} - The name of this idiom (a short descriptive string such as 'create' or 'internal'). Useful for finely differentiating between create, update and delete operations, for example.

  • Constraint[] constraints () default {} - The constraint annotations to be applied to the property within this idiom (see Constraint above).

  • boolean required () default false - Marks the generated JSON element as required in this idiom (with all of the issues previously noted).

@Pledge

It may be that, given the idioms annotated for the set of properties of the originating class, some of the resultant purposes (see Idiom above) may end up with no properties at all, and those views would, therefore, never be generated. In those cases, you can use a pledge force generation of specific view classes.

  • Visibility visibility () default Visibility.BOTH - The visibility for which views should be generated, defaults to both in and out views.

  • String[] purposes () default {} - Any purposes for which the views should be generated. If empty then only the default (un-named) purpose will be enforced.

@View

Creates a 'view' property, and determines how the annotated field is represented in those generated views.

  • String name () default "" - The name of the JSON attribute generated for the annotated property. If left empty the attribute name will be the same as the field name.

  • Idiom[] idioms () default {} - The list of alternate idioms in which this property should be included (see Idiom above). If empty, this property will be included in the default idiom.

  • Class<? extends XmlAdapter> adapter () default NullXmlAdapter.class - The XmlAdapter class, if any, to be used for encoding and decoding this property.

  • Class<?> as () default Void.class - A type hint for tools which may process Doppelganger generated views.

  • boolean required () default false - Marks the generated JSON element as required (with all the issues previously noted).

  • String comment () default "" - A notation passed into generated classes by annotating the generated field with a @Comment (see below) containing this text. If this value is left blank, then no @Comment will be generated.

@Comment

Simply holds a string which may be used by other tools creation automated descriptions of the annotated entities.

  • String value () default "" - The text of the comment.

In The Wild

The following is a simplified, but still plausible, example of how the Doppelganger annotations might be used. We’ll refrain from reproducing the generated sources here, but you can take the following code and generate them for yourself. It may be instructive to try the resulting constructors and factory methods.

public enum Biome {

  ARCTIC, FOREST, JUNGLE, TUNDRA
}

@Doppelganger(polymorphic = @Polymorphic(subClasses = {Lion.class, Tiger.class, Bear.class}), properties = @Virtual(field = "tame", type = @Type(value = Boolean.class), idioms = @Idiom(purposes = "create", visibility = IN, constraints = @Constraint(NotBlank.class))))
public abstract class Predator {

  @View(idioms = {@Idiom(purposes = "create", visibility = IN, constraints = @Constraint(NotBlank.class)), @Idiom(purposes = "read", visibility = OUT)})
  private String name;
  @View(idioms = {@Idiom(purposes = "create", visibility = IN, constraints = @Constraint(NotBlank.class)), @Idiom(purposes = "update", visibility = IN), @Idiom(purposes = "read", visibility = OUT)})
  private Biome biome;

  public String getName () {

    return name;
  }

  public void setName (String name) {

    this.name = name;
  }

  public Biome getBiome () {

    return biome;
  }

  public void setBiome (Biome biome) {

    this.biome = biome;
  }
}

@Doppelganger(name = "lion")
public class Lion extends Predator {

  @View(idioms = {@Idiom(purposes = {"create", "update"}, visibility = IN, constraints = @Constraint(value = Min.class, arguments = "0")), @Idiom(purposes = "read", visibility = OUT)})
  private int pride;

  public int getPride () {

    return pride;
  }

  public void setPride (int pride) {

    this.pride = pride;
  }
}

@Doppelganger(name = "tiger")
public class Tiger extends Predator {

  @View(idioms = {@Idiom(purposes = "create", visibility = IN, constraints = @Constraint(NotNull.class)), @Idiom(purposes = "update", visibility = IN), @Idiom(purposes = "read", visibility = OUT)})
  private Boolean albino;

  public Boolean getAlbino () {

    return albino;
  }

  public void setAlbino (Boolean albino) {

    this.albino = albino;
  }
}

@Doppelganger
public class Circus {

  @View(idioms = {@Idiom(purposes = "create", visibility = IN, constraints = @Constraint(NotBlank.class)), @Idiom(purposes = "read", visibility = OUT)})
  private String location;

  public String getLocation () {

    return location;
  }

  public void setLocation (String location) {

    this.location = location;
  }
}

@Doppelganger(name = "bear")
public class Bear extends Predator {

  @View(idioms = {@Idiom(purposes = "create", visibility = IN, constraints = @Constraint(NotBlank.class)), @Idiom(purposes = "update", visibility = IN), @Idiom(purposes = "read", visibility = OUT)})
  private String color;
  @View(idioms = {@Idiom(purposes = {"create", "update"}, visibility = IN), @Idiom(purposes = "read", visibility = OUT)})
  private Circus circus;

  public String getColor () {

    return color;
  }

  public void setColor (String color) {

    this.color = color;
  }

  public Circus getCircus () {

    return circus;
  }

  public void setCircus (Circus circus) {

    this.circus = circus;
  }
}