package org.sculptor.dddsample.cargo.domain;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.apache.commons.lang.Validate;
import org.sculptor.dddsample.location.domain.Location;
/**
* A Cargo. This is the central class in the domain model, and it is the root of
* the Cargo-Itinerary-Leg-DeliveryHistory aggregate.
*
* A cargo is identified by a unique tracking id, and it always has an origin
* and a destination. The life cycle of a cargo begins with the booking
* procedure, when the tracking id is assigned. During a (short) period of time,
* between booking and initial routing, the cargo has no itinerary.
*
* The booking clerk requests a list of possible routes, matching a route
* specification, and assigns the cargo to one route. An itinerary listing the
* legs of the route is attached to the cargo.
*
* A cargo can be re-routed during transport, on demand of the customer, in
* which case the destination is changed and a new route is requested. The old
* itinerary, being a value object, is discarded and a new one is attached.
*
* It may also happen that a cargo is accidentally misrouted, which should
* notify the proper personnel and also trigger a re-routing procedure.
*
* The life cycle of a cargo ends when the cargo is claimed by the customer.
*
* The cargo aggregate, and the entre domain model, is built to solve the
* problem of booking and tracking cargo. All important business rules for
* determining whether or not a cargo is misrouted, what the current status of
* the cargo is (on board carrier, in port etc), are captured in this aggregate.
*/
@Entity(name = "Cargo")
@Table(name = "CARGO" , uniqueConstraints = @UniqueConstraint(columnNames={"TRACKINGID"}))
public class Cargo extends CargoBase {
private static final long serialVersionUID = -4916991786969251821L;
protected Cargo() {
}
public Cargo(TrackingId trackingId, Location origin, Location destination) {
super(trackingId, origin, destination);
}
public DeliveryHistory deliveryHistory() {
return new DeliveryHistory(getEvents());
}
/**
* @return itinerary, {@link Itinerary.EMPTY_ITINERARY} when null
*/
public Itinerary itinerary() {
return nullSafe(getItinerary(), Itinerary.EMPTY_ITINERARY);
}
/**
* @return Last known location of the cargo, or Location.UNKNOWN if the
* delivery history is empty.
*/
public Location lastKnownLocation() {
final HandlingEvent lastEvent = deliveryHistory().lastEvent();
if (lastEvent != null) {
return lastEvent.getLocation();
} else {
return Location.UNKNOWN;
}
}
/**
* @return True if the cargo has arrived at its final destination.
*/
public boolean hasArrived() {
return getDestination().equals(lastKnownLocation());
}
/**
* Attach a new itinerary to this cargo.
*
* @param itinerary
* an itinerary. May not be null.
*/
public void attachItinerary(final Itinerary itinerary) {
Validate.notNull(itinerary);
// Decouple the old itinerary from this cargo
itinerary().setCargo(null);
// Couple this cargo and the new itinerary
setItinerary(itinerary);
itinerary().setCargo(this);
}
/**
* Detaches the current itinerary from the cargo.
*/
public void detachItinerary() {
itinerary().setCargo(null);
setItinerary(null);
}
/**
* Check if cargo is misdirected.
* <p/>
* <ul>
* <li>A cargo is misdirected if it is in a location that's not in the itinerary.
* <li>A cargo with no itinerary can not be misdirected.
* <li>A cargo that has received no handling events can not be misdirected.
* </ul>
*
* @return <code>true</code> if the cargo has been misdirected,
*/
public boolean isMisdirected() {
final HandlingEvent lastEvent = deliveryHistory().lastEvent();
if (lastEvent == null) {
return false;
} else {
return !itinerary().isExpected(lastEvent);
}
}
/**
* Does not take into account the possibility of the cargo having been
* (errouneously) loaded onto another carrier after it has been unloaded at
* the final destination.
*
* @return True if the cargo has been unloaded at the final destination.
*/
public boolean isUnloadedAtDestination() {
for (HandlingEvent event : deliveryHistory().eventsOrderedByCompletionTime()) {
if (Type.UNLOAD.equals(event.getType()) && getDestination().equals(event.getLocation())) {
return true;
}
}
return false;
}
// Utility for Null Object Pattern - should be moved out of this class
private <T> T nullSafe(T actual, T safe) {
return actual == null ? safe : actual;
}
}