package net.java.cargotracker.domain.model.cargo; import java.io.Serializable; import java.util.Date; import java.util.Iterator; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.Embedded; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.validation.constraints.NotNull; import static net.java.cargotracker.domain.model.cargo.RoutingStatus.*; import static net.java.cargotracker.domain.model.cargo.TransportStatus.*; import net.java.cargotracker.domain.model.handling.HandlingEvent; import net.java.cargotracker.domain.model.handling.HandlingHistory; import net.java.cargotracker.domain.model.location.Location; import net.java.cargotracker.domain.model.voyage.Voyage; import net.java.cargotracker.domain.shared.DomainObjectUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; /** * The actual transportation of the cargo, as opposed to the customer * requirement (RouteSpecification) and the plan (Itinerary). */ @Embeddable public class Delivery implements Serializable { private static final long serialVersionUID = 1L; // Null object pattern. public static final Date ETA_UNKOWN = null; // Null object pattern public static final HandlingActivity NO_ACTIVITY = new HandlingActivity(); @Enumerated(EnumType.STRING) @Column(name = "transport_status") @NotNull private TransportStatus transportStatus; @ManyToOne @JoinColumn(name = "last_known_location_id") private Location lastKnownLocation; @ManyToOne @JoinColumn(name = "current_voyage_id") private Voyage currentVoyage; @NotNull private boolean misdirected; @Temporal(TemporalType.DATE) private Date eta; @Embedded private HandlingActivity nextExpectedActivity; @Column(name = "unloaded_at_dest") @NotNull private boolean isUnloadedAtDestination; @Enumerated(EnumType.STRING) @Column(name = "routing_status") @NotNull private RoutingStatus routingStatus; @Temporal(TemporalType.TIMESTAMP) @Column(name = "calculated_at") @NotNull private Date calculatedAt; @ManyToOne @JoinColumn(name = "last_event_id") private HandlingEvent lastEvent; public Delivery() { // Nothing to initialize } public Delivery(HandlingEvent lastEvent, Itinerary itinerary, RouteSpecification routeSpecification) { this.calculatedAt = new Date(); this.lastEvent = lastEvent; this.misdirected = calculateMisdirectionStatus(itinerary); this.routingStatus = calculateRoutingStatus(itinerary, routeSpecification); this.transportStatus = calculateTransportStatus(); this.lastKnownLocation = calculateLastKnownLocation(); this.currentVoyage = calculateCurrentVoyage(); this.eta = calculateEta(itinerary); this.nextExpectedActivity = calculateNextExpectedActivity( routeSpecification, itinerary); this.isUnloadedAtDestination = calculateUnloadedAtDestination(routeSpecification); } /** * Creates a new delivery snapshot to reflect changes in routing, i.e. when * the route specification or the itinerary has changed but no additional * handling of the cargo has been performed. */ Delivery updateOnRouting(RouteSpecification routeSpecification, Itinerary itinerary) { Validate.notNull(routeSpecification, "Route specification is required"); return new Delivery(this.lastEvent, itinerary, routeSpecification); } /** * Creates a new delivery snapshot based on the complete handling history of * a cargo, as well as its route specification and itinerary. * * @param routeSpecification route specification * @param itinerary itinerary * @param handlingHistory delivery history * @return An up to date delivery. */ static Delivery derivedFrom(RouteSpecification routeSpecification, Itinerary itinerary, HandlingHistory handlingHistory) { Validate.notNull(routeSpecification, "Route specification is required"); Validate.notNull(handlingHistory, "Delivery history is required"); HandlingEvent lastEvent = handlingHistory .getMostRecentlyCompletedEvent(); return new Delivery(lastEvent, itinerary, routeSpecification); } public TransportStatus getTransportStatus() { return transportStatus; } public void setTransportStatus(TransportStatus transportStatus) { this.transportStatus = transportStatus; } public Location getLastKnownLocation() { return DomainObjectUtils.nullSafe(lastKnownLocation, Location.UNKNOWN); } public void setLastKnownLocation(Location lastKnownLocation) { this.lastKnownLocation = lastKnownLocation; } public void setLastEvent(HandlingEvent lastEvent) { this.lastEvent = lastEvent; } public Voyage getCurrentVoyage() { return DomainObjectUtils.nullSafe(currentVoyage, Voyage.NONE); } /** * 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() { return misdirected; } public void setMisdirected(boolean misdirected) { this.misdirected = misdirected; } public Date getEstimatedTimeOfArrival() { if (eta != ETA_UNKOWN) { return new Date(eta.getTime()); } else { return ETA_UNKOWN; } } public HandlingActivity getNextExpectedActivity() { return nextExpectedActivity; } /** * @return True if the cargo has been unloaded at the final destination. */ public boolean isUnloadedAtDestination() { return isUnloadedAtDestination; } public void setUnloadedAtDestination(boolean isUnloadedAtDestination) { this.isUnloadedAtDestination = isUnloadedAtDestination; } public RoutingStatus getRoutingStatus() { return routingStatus; } public void setRoutingStatus(RoutingStatus routingStatus) { this.routingStatus = routingStatus; } /** * @return When this delivery was calculated. */ public Date getCalculatedAt() { return new Date(calculatedAt.getTime()); } public void setCalculatedAt(Date calculatedAt) { this.calculatedAt = calculatedAt; } private TransportStatus calculateTransportStatus() { if (lastEvent == null) { return NOT_RECEIVED; } switch (lastEvent.getType()) { case LOAD: return ONBOARD_CARRIER; case UNLOAD: case RECEIVE: case CUSTOMS: return IN_PORT; case CLAIM: return CLAIMED; default: return UNKNOWN; } } private Location calculateLastKnownLocation() { if (lastEvent != null) { return lastEvent.getLocation(); } else { return null; } } private Voyage calculateCurrentVoyage() { if (getTransportStatus().equals(ONBOARD_CARRIER) && lastEvent != null) { return lastEvent.getVoyage(); } else { return null; } } private boolean calculateMisdirectionStatus(Itinerary itinerary) { if (lastEvent == null) { return false; } else { return !itinerary.isExpected(lastEvent); } } private Date calculateEta(Itinerary itinerary) { if (onTrack()) { return itinerary.getFinalArrivalDate(); } else { return ETA_UNKOWN; } } private HandlingActivity calculateNextExpectedActivity( RouteSpecification routeSpecification, Itinerary itinerary) { if (!onTrack()) { return NO_ACTIVITY; } if (lastEvent == null) { return new HandlingActivity(HandlingEvent.Type.RECEIVE, routeSpecification.getOrigin()); } switch (lastEvent.getType()) { case LOAD: for (Leg leg : itinerary.getLegs()) { if (leg.getLoadLocation().sameIdentityAs( lastEvent.getLocation())) { return new HandlingActivity(HandlingEvent.Type.UNLOAD, leg.getUnloadLocation(), leg.getVoyage()); } } return NO_ACTIVITY; case UNLOAD: for (Iterator<Leg> iterator = itinerary.getLegs().iterator(); iterator .hasNext();) { Leg leg = iterator.next(); if (leg.getUnloadLocation().sameIdentityAs( lastEvent.getLocation())) { if (iterator.hasNext()) { Leg nextLeg = iterator.next(); return new HandlingActivity(HandlingEvent.Type.LOAD, nextLeg.getLoadLocation(), nextLeg.getVoyage()); } else { return new HandlingActivity(HandlingEvent.Type.CLAIM, leg.getUnloadLocation()); } } } return NO_ACTIVITY; case RECEIVE: Leg firstLeg = itinerary.getLegs().iterator().next(); return new HandlingActivity(HandlingEvent.Type.LOAD, firstLeg.getLoadLocation(), firstLeg.getVoyage()); case CLAIM: default: return NO_ACTIVITY; } } private RoutingStatus calculateRoutingStatus(Itinerary itinerary, RouteSpecification routeSpecification) { if (itinerary == null || itinerary == Itinerary.EMPTY_ITINERARY) { return NOT_ROUTED; } else { if (routeSpecification.isSatisfiedBy(itinerary)) { return ROUTED; } else { return MISROUTED; } } } private boolean calculateUnloadedAtDestination( RouteSpecification routeSpecification) { return lastEvent != null && HandlingEvent.Type.UNLOAD.sameValueAs(lastEvent.getType()) && routeSpecification.getDestination().sameIdentityAs( lastEvent.getLocation()); } private boolean onTrack() { return routingStatus.equals(ROUTED) && !misdirected; } private boolean sameValueAs(Delivery other) { return other != null && new EqualsBuilder() .append(this.transportStatus, other.transportStatus) .append(this.lastKnownLocation, other.lastKnownLocation) .append(this.currentVoyage, other.currentVoyage) .append(this.misdirected, other.misdirected) .append(this.eta, other.eta) .append(this.nextExpectedActivity, other.nextExpectedActivity) .append(this.isUnloadedAtDestination, other.isUnloadedAtDestination) .append(this.routingStatus, other.routingStatus) .append(this.calculatedAt, other.calculatedAt) .append(this.lastEvent, other.lastEvent).isEquals(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Delivery other = (Delivery) o; return sameValueAs(other); } @Override public int hashCode() { return new HashCodeBuilder().append(transportStatus) .append(lastKnownLocation).append(currentVoyage) .append(misdirected).append(eta).append(nextExpectedActivity) .append(isUnloadedAtDestination).append(routingStatus) .append(calculatedAt).append(lastEvent).toHashCode(); } }