How to use Structural Search & Replace to make "fluent" objects?

Answered

I'd like to take legacy Entity objects in a hierarchy and convert them to have fluent setters in order to make them easier to deal with.

For example, we have an interface and base class for Entity

public interface Entity {
Long getId();
void setId();
}
public class AbstractEntity implements Entity {
protected Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

and then the classes we actually use

public class Person extends Entity {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

public class Pet extends Entity {
  private String name;
private Breed breed; // elided enum
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Breed getBreed() {
return breed;
}
public void setBreed(Breed breed) {
this.breed = breed;
}
}

I want to change all the setters for all types that extend Entity to return 'this'. E.g. Pet::setBreed would become

public Pet setBreed(Breed breed) {
this.breed = breed;
return this;
}

And all the setters for all the classes that extend Entity (and, of course, Entity itself) would be changed accordingly.

When I use a class-based SSR with 0,infinity searches on method names, it the method name replacement replaces the method names with all the method names found in the class. I also can't figure out how to get the proper class name replacement.

When I use a method-based SSR, can get all the methods based on defining a search scope based on Class Hierarchy, but I can't figure out how to get the class type for the proper return type.

How can I do this with SSR? (Assuming SSR is the right place to do this. If not, is there a better way?

[Edit] After some initial testing, I'll need to figure out the generics incantations to use as well.

2 comments
Comment actions Permalink
Official comment

A pattern like the following should work:

<replaceConfiguration name="type" text="public void $setter$($T$ $v$) {&#10; $st$;&#10;}" recursive="false" caseInsensitive="true" type="JAVA" pattern_context="member" reformatAccordingToStyle="true" shortenFQN="true" replacement="public $Class$ $setter$($T$ $v$) {&#10; $st$;&#10; return this;&#10;}">
<constraint name="__context__" within="" contains="" />
<constraint name="setter" regexp="set.*" within="" contains="" />
<constraint name="T" within="" contains="" />
<constraint name="v" within="" contains="" />
<constraint name="st" within="" contains="" />
<variableDefinition name="Class" script="&quot;setter.getContainingClass().getQualifiedName()&#10;&quot;" />
</replaceConfiguration>

(Use "Import Template from Clipboard" under the tools/cog button menu in the Structural Replace dialog to import this pattern)

 

Bas

Comment actions Permalink

Thanks! That gets me close, but opened a new rabbit hole or two. E.g. AbstractEntity can't return this. I genericized things and created a cast() method in the base of the hierarchy

public interface Entity<T> {
Long getId();
T setId(Long id);

@SuppressWarnings("unchecked")
default T cast() {
return (T)this;
}
}

Then my setters return cast();

public class AbstractEntity<T extends AbstractEntity<T>> implements Entity<T> {
protected Long id;
public Long getId() {
return id;
}
public T setId(Long id) {
this.id = id;
return cast();
}
}

This works fine for a flat domain model, but the one I'm dealing with has a very deep hierarchy. To extend the example, say we have classes Cat and Dog that extend Pet. Pet, however, is used all over the application. Cat (and Dog) work fine after my modifications, but now I can't instantiate a Pet without getting an raw use warning.

public class Pet<T extends Pet<T>> extends AbstractEntity<T> {

private static void main(String... args) {
// final Pet pet = new Pet().setId(1L); <-- DOESN'T COMPILE (This is all over the code base)
final Pet pet = new Pet<Pet>().setId(1L); // Raw use of parameterized class 'Pet'
}

private String name;
private String breed;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
}
public class Cat extends Pet<Cat> {
public static void main(String... args) {
final Cat cat = new Cat().setId(1L).setNumberOfClaws(11); // works fine
}

int claws = 10;
public Cat setNumberOfClaws(int claws) {
this.claws = claws;
return cast();
}
}

If I don't parameterize the type of Pet, then I can't really use fluent setters on Cat or Dog, but if I do parameterize it throws warnings all over the place that I doubt I'd get the green light to suppress.

I think that due to the deep hierarchies in the data model what I want to do may be impossible to cleanly express in the Java language. On top of that I'm not sure how it would mess with Hibernate. Thanks for your help, though. I got a lot out of your example!

0

Please sign in to leave a comment.