Maximum Zeal ~ Emphatic prose on indulged fascinations

Eclipse AJDT intertypes and Push-In refactoring

In the wake of the recent 1.0.0 release of Spring Roo, while going through Ben Alex’s captivating three part tutorial (1,2,3) on the tool, I experienced one of those moments that are normally few and far between when you know you’ve come across something new – something rare and revolutionary for the space of technology that it’s acting within that would never have occurred to you – something lateral in nature (no pun intended as we’ll see later on). Meet Eclipse ADJT, intertypes and Push-In refactoring.

Let’s take the Spring Roo project in the above tutorial as an example.

Spring Roo Project

Spring Roo Project

Imagine the following pojo.

package name.dhruba.wedding.domain;

import javax.persistence.Entity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.tostring.RooToString;
import org.springframework.roo.addon.entity.RooEntity;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.springframework.format.annotation.DateTimeFormat;

@Entity
@RooJavaBean
@RooToString
@RooEntity(finders = { "findRsvpsByCodeEquals" })
public class Rsvp {

    @NotNull
    @Size(min = 1, max = 30)
    private String code;

    @Size(max = 30)
    private String email;

    private Integer attending;

    @Size(max = 100)
    private String specialRequests;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(style = "S-")
    private Date confirmed;
    
    
}

It has no methods of any kind. Now imagine a separate compilation unit that provides these methods.

package name.dhruba.wedding.domain;

import java.lang.Integer;
import java.lang.Long;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PersistenceContext;
import javax.persistence.Version;
import name.dhruba.wedding.domain.Rsvp;
import org.springframework.transaction.annotation.Transactional;

privileged aspect Rsvp_Roo_Entity {
    
    @PersistenceContext    
    transient EntityManager Rsvp.entityManager;    
    
    @Id    
    @GeneratedValue(strategy = GenerationType.AUTO)    
    @Column(name = "id")    
    private Long Rsvp.id;    
    
    @Version    
    @Column(name = "version")    
    private Integer Rsvp.version;    
    
    public Long Rsvp.getId() {    
        return this.id;        
    }    
    
    public void Rsvp.setId(Long id) {    
        this.id = id;        
    }    
    
    public Integer Rsvp.getVersion() {    
        return this.version;        
    }    
    
    public void Rsvp.setVersion(Integer version) {    
        this.version = version;        
    }    
    
    @Transactional    
    public void Rsvp.persist() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        this.entityManager.persist(this);        
    }    
    
    @Transactional    
    public void Rsvp.remove() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        if (this.entityManager.contains(this)) {        
            this.entityManager.remove(this);            
        } else {        
            Rsvp attached = this.entityManager.find(Rsvp.class, this.id);            
            this.entityManager.remove(attached);            
        }        
    }    
    
    @Transactional    
    public void Rsvp.flush() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        this.entityManager.flush();        
    }    
    
    @Transactional    
    public void Rsvp.merge() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        Rsvp merged = this.entityManager.merge(this);        
        this.entityManager.flush();        
        this.id = merged.getId();        
    }    
    
    public static final EntityManager Rsvp.entityManager() {    
        EntityManager em = new Rsvp().entityManager;        
        if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)");        
        return em;        
    }    
    
    public static long Rsvp.countRsvps() {    
        return (Long) entityManager().createQuery("select count(o) from Rsvp o").getSingleResult();        
    }    
    
    public static List Rsvp.findAllRsvps() {    
        return entityManager().createQuery("select o from Rsvp o").getResultList();        
    }    
    
    public static Rsvp Rsvp.findRsvp(Long id) {    
        if (id == null) throw new IllegalArgumentException("An identifier is required to retrieve an instance of Rsvp");        
        return entityManager().find(Rsvp.class, id);        
    }    
    
    public static List Rsvp.findRsvpEntries(int firstResult, int maxResults) {    
        return entityManager().createQuery("select o from Rsvp o").setFirstResult(firstResult).setMaxResults(maxResults).getResultList();        
    }    
    
}

Imagine that your editor handles the separation seamlessly and merges the two for you to work with the API at compile time.

    @RequestMapping(method = RequestMethod.POST)
    public String post(@ModelAttribute("rsvp") Rsvp rsvp, ModelMap modelMap) {
        rsvp.setConfirmed(new Date());
        if (rsvp.getId() == null) {
            rsvp.persist();
        } else {
            rsvp.merge();
        }
        if (rsvp.getEmail().length() > 0) {
            sendMessage("Ben Alex ", "RSVP to our wedding", rsvp.getEmail(),
                    "Your RSVP has been saved: " + rsvp.toString());
        }
        modelMap.put("rsvp", rsvp);
        return "thanks";
    }

This allows Spring Roo to achieve a true separation of concerns as well a multitude of other compelling benefits.

Now imagine you want to merge such duality into one, project wide, once your initial project harness is ready. You can.

Right click the project » Refactor » Push-In.

Eclipse AJDT Push-In Menu

Eclipse AJDT Push-In Menu

Check pending actions.

Push-In pending actions

Push-In pending actions

Preview the action.

Preview Push-In Refactoring

Preview Push-In Refactoring

Apply and you can get the following outcome.

package name.dhruba.wedding.domain;

import javax.persistence.Entity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.tostring.RooToString;
import org.springframework.roo.addon.entity.RooEntity;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Version;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.format.annotation.DateTimeFormat;

@Configurable
@Entity
@RooJavaBean
@RooToString
@RooEntity(finders = { "findRsvpsByCodeEquals" })
public class Rsvp {

    @NotNull
    @Size(min = 1, max = 30)
    private String code;

    @Size(max = 30)
    private String email;

    private Integer attending;

    @Size(max = 100)
    private String specialRequests;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(style = "S-")
    private Date confirmed;
    
    

	public String toString() {    
        StringBuilder sb = new StringBuilder();        
        sb.append("Id: ").append(getId()).append(", ");        
        sb.append("Version: ").append(getVersion()).append(", ");        
        sb.append("Code: ").append(getCode()).append(", ");        
        sb.append("Email: ").append(getEmail()).append(", ");        
        sb.append("Attending: ").append(getAttending()).append(", ");        
        sb.append("SpecialRequests: ").append(getSpecialRequests()).append(", ");        
        sb.append("Confirmed: ").append(getConfirmed());        
        return sb.toString();        
    }

	@PersistenceContext    
    transient EntityManager entityManager;

	@Id    
    @GeneratedValue(strategy = GenerationType.AUTO)    
    @Column(name = "id")    
    private Long id;

	@Version    
    @Column(name = "version")    
    private Integer version;

	public Long getId() {    
        return this.id;        
    }

	public void setId(Long id) {    
        this.id = id;        
    }

	public Integer getVersion() {    
        return this.version;        
    }

	public void setVersion(Integer version) {    
        this.version = version;        
    }

	@Transactional    
    public void persist() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        this.entityManager.persist(this);        
    }

	@Transactional    
    public void remove() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        if (this.entityManager.contains(this)) {        
            this.entityManager.remove(this);            
        } else {        
            Rsvp attached = this.entityManager.find(Rsvp.class, this.id);            
            this.entityManager.remove(attached);            
        }        
    }

	@Transactional    
    public void flush() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        this.entityManager.flush();        
    }

	@Transactional    
    public void merge() {    
        if (this.entityManager == null) this.entityManager = entityManager();        
        Rsvp merged = this.entityManager.merge(this);        
        this.entityManager.flush();        
        this.id = merged.getId();        
    }

	public static final EntityManager entityManager() {    
        EntityManager em = new Rsvp().entityManager;        
        if (em == null) throw new IllegalStateException("Entity manager has not been injected (is the Spring Aspects JAR configured as an AJC/AJDT aspects library?)");        
        return em;        
    }

	public static long countRsvps() {    
        return (Long) entityManager().createQuery("select count(o) from Rsvp o").getSingleResult();        
    }

	public static List findAllRsvps() {    
        return entityManager().createQuery("select o from Rsvp o").getResultList();        
    }

	public static Rsvp findRsvp(Long id) {    
        if (id == null) throw new IllegalArgumentException("An identifier is required to retrieve an instance of Rsvp");        
        return entityManager().find(Rsvp.class, id);        
    }

	public static List findRsvpEntries(int firstResult, int maxResults) {    
        return entityManager().createQuery("select o from Rsvp o").setFirstResult(firstResult).setMaxResults(maxResults).getResultList();        
    }

	public String getCode() {    
        return this.code;        
    }

	public void setCode(String code) {    
        this.code = code;        
    }

	public String getEmail() {    
        return this.email;        
    }

	public void setEmail(String email) {    
        this.email = email;        
    }

	public Integer getAttending() {    
        return this.attending;        
    }

	public void setAttending(Integer attending) {    
        this.attending = attending;        
    }

	public String getSpecialRequests() {    
        return this.specialRequests;        
    }

	public void setSpecialRequests(String specialRequests) {    
        this.specialRequests = specialRequests;        
    }

	public Date getConfirmed() {    
        return this.confirmed;        
    }

	public void setConfirmed(Date confirmed) {    
        this.confirmed = confirmed;        
    }

	public static Query findRsvpsByCodeEquals(String code) {    
        if (code == null || code.length() == 0) throw new IllegalArgumentException("The code argument is required");        
        EntityManager em = Rsvp.entityManager();        
        Query q = em.createQuery("SELECT Rsvp FROM Rsvp AS rsvp WHERE rsvp.code = :code");        
        q.setParameter("code", code);        
        return q;        
    }
}

If we look at the difference in the number of files before and after you can clearly see the intertypes have been deleted.

$ find wedding.original/ | wc -l
     211
$ find wedding/ | wc -l
     191

Simply genius.

Support is upcoming for the opposite: Pull-Out.

Note that in order for the above to work you must either have STS which comes with the AJDT plugin or Eclipse with the AJDT plugin installed. Also you must either have the AspectJ nature or AJDT weaving enabled or both.

2 Responses to Eclipse AJDT intertypes and Push-In refactoring

  1. Ben Alex says:

    Thanks for posting this blog. I always enjoy seeing people’s reaction when they discover the power of inter-type declarations (ITDs) and the incredible usefulness of it. The surprising fact is ITDs have existing in AspectJ for years and years, resulting in a very mature building block that hasn’t been previously exploited in this manner for productivity or code generation. Those interested in learning more about Roo’s AspectJ usage (with interesting details about how to introspect .class files to see their ITD members etc) might wish to read http://static.springsource.org/spring-roo/reference/html/architecture.html#architecture-critical-technologies-aspectj. We also discuss the pros and cons of push-in refactoring away Roo in the http://static.springsource.org/spring-roo/reference/html/removing.html chapter.

    Cheers
    Ben Alex
    Project Lead, Spring Roo

  2. Hi Ben. Yes it’s certainly a hidden gem that I wasn’t aware of previously and helps explain a little about how Roo does what it does which is something that I wondered about. Very interesting. I will have a look at the links you provided too. Thanks.