Thanks to the relase of the maven tidy plugin, it is now easy to assure your pom has all elements in a standard order:
mvn tidy:pom |
Commit. Done.
Thanks to the relase of the maven tidy plugin, it is now easy to assure your pom has all elements in a standard order:
mvn tidy:pom |
Commit. Done.
I have no direct clues, but an exception with following message
Caused by: com.thoughtworks.xstream.converters.ConversionException: Cannot create org.joda.time.chrono.ISOChronology$Stub by JDK serialization : null : Cannot create org.joda.time.chrono.ISOChronology$Stub by JDK serialization : null ---- Debugging information ---- message : Cannot create org.joda.time.chrono.ISOChronology$Stub by JDK serialization : null cause-exception : com.thoughtworks.xstream.converters.reflection.ObjectAccessException cause-message : Cannot create org.joda.time.chrono.ISOChronology$Stub by JDK serialization : null class : be.vlaanderen.winterdienst.eigenpersoneel.domain.Winterperiode required-type : org.joda.time.chrono.ISOChronology path : /be.vlaanderen.winterdienst.eigenpersoneel.domain.Winterperiode/aanvangsDatum/iChronology line number : 15 ------------------------------- |
Was caused by using Java7. Reverting to Java6 fixed the problem.
This was using joda-time 1.5. It may be fixed in more recent versions.
In CRUD REST services it often happens that the data which can be requested or saved or updated in not always the same. It can depend on various aspects like the role of the user or the state of the object in question.
Let’s see how this can be solved declaratively using jTransfo. The actual services use transfer objects. While this introduced a step to copy the fields (which would not exist in the case of detached objects) this explicitly allows control over the data to copy, possibly changing the representation. Additionally this shields your services from changes in your domain model which allows you to keep your API stable.
For this example, let’s look at a proposal. This contains estimates of amounts (and unit price) of goods which are needed to do a particular job. When the proposal has been accepted by the customer, the actual amounts can be set on the estimates. This results in the following domain model.
A proposal can have a couple of states:
The checking when fields can be updated are handled declaratively in the domain objects, so the logic for the create, get and update methods (skipping validation) are quite lean.
@Autowired private JTransfo jTransfo; @Autowired private proposalDao proposalDao; @Transactional public void create(ProposalTo proposalTo) { Proposal proposal = jTransfo.convertTo(proposalTo, ProposalTo.class, "CREATE"); proposalDao.save(proposal); } @Transactional public void update(ProposalTo proposalTo) { Proposal proposal = proposalDao.get(proposalTo.getId()); proposal = jTransfo.convert(proposalTo, proposal, proposal.getState()); proposalDao.update(proposal); } @Transactional(readOnly = true) public ProposalTo get(long proposalId) { Proposal proposal = proposalDao.get(proposalId); return jTransfo.convertTo(proposal, ProposalTo.class); } |
The magic is in the tags passed (last parameter) to the jTransfo convert methods in save and update methods combined with annotations in the transfer object definitions. For creating “CREATE” is passed and the object state is used when the object already existed. The methods to change the state of the object are not shown here.
In the transfer objects, MapOnly annotations are used to indicate when (based on the tags) fields can be copied. A field without MapOnly annotation is always copied (unless it is marked with @NotMapped). When a MapOnly annotation exists, it is only copied when the convert contains a tag which is mentioned in the annotations. Defining several MapOnly annotations is possible by grouping inside a MapOnlies annotation. For fallback, “*” matches all tags.
The transfer object definitions use lombok to generate getters and setters.
@Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @DomainClass("pkg.Proposal") public class ProposalTo extends AbstractChangeLoggedIdentifiedTo { @MapOnlies({ @MapOnly(value = { "CREATE" }), @MapOnly(value = "*", readOnly = true) }) private String description; @MappedBy(readOnly = true) private String state; // "CREATED", "APPROVED", "FINISHED" @MapOnlies({ @MapOnly(value = { "CREATE", "CREATED", "APPROVED" }), @MapOnly(value = "*", readOnly = true) }) @MappedBy(typeConverter = "estimateToList") private List<EstimateTo> estimates = new ArrayList<EstimateTo>(); } |
@Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @DomainClass("pkg.Estimate") public class EstimateTo extends AbstractIdentifiedTo { @MapOnlies({ @MapOnly(value = { "CREATE", "CREATED" }), @MapOnly(value = "APPROVED", typeConverter = "newReadOnlyDomain" ), @MapOnly(value = "*", readOnly = true) }) @MappedBy(typeConverter = "readOnlyDomain") private ProductTo product; @MapOnlies({ @MapOnly(value = { "CREATE", "CREATED" }), @MapOnly(value = "*", readOnly = true) }) private Double estimatedCount; @MapOnlies({ @MapOnly(value = { "APPROVED" }), @MapOnly(value = "*", readOnly = true) }) @MappedBy(typeConverter = "executionToList") private List<ExecutionTo> uitvoeringen = new ArrayList<ExecutionTo>(); } |
For completeness, the product and execution transfer objects are also displayed. The product records are not updated as this used the “readOnlyDomain” type converter. The execution has no restrictions.
@Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @DomainClass("pkg.Product") public class ProductTo extends AbstractIdentifiedTo { private String description; private Double unitPrice; } |
@Data @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @DomainClass("pkg.Execution") public class ExecutionTo extends AbstractIdentifiedTo { private double count; private Date date; } |