View Javadoc

1   package se.citerus.dddsample.domain.model.cargo;
2   
3   import org.apache.commons.lang.Validate;
4   import org.apache.commons.lang.builder.EqualsBuilder;
5   import org.apache.commons.lang.builder.HashCodeBuilder;
6   import static se.citerus.dddsample.domain.model.cargo.RoutingStatus.*;
7   import static se.citerus.dddsample.domain.model.cargo.TransportStatus.*;
8   import se.citerus.dddsample.domain.model.handling.HandlingEvent;
9   import se.citerus.dddsample.domain.model.handling.HandlingHistory;
10  import se.citerus.dddsample.domain.model.location.Location;
11  import se.citerus.dddsample.domain.model.voyage.Voyage;
12  import se.citerus.dddsample.domain.shared.DomainObjectUtils;
13  import se.citerus.dddsample.domain.shared.ValueObject;
14  
15  import java.util.Date;
16  import java.util.Iterator;
17  
18  /**
19   * The actual transportation of the cargo, as opposed to
20   * the customer requirement (RouteSpecification) and the plan (Itinerary). 
21   *
22   */
23  public class Delivery implements ValueObject<Delivery> {
24  
25    private TransportStatus transportStatus;
26    private Location lastKnownLocation;
27    private Voyage currentVoyage;
28    private boolean misdirected;
29    private Date eta;
30    private HandlingActivity nextExpectedActivity;
31    private boolean isUnloadedAtDestination;
32    private RoutingStatus routingStatus;
33    private Date calculatedAt;
34    private HandlingEvent lastEvent;
35  
36    private static final Date ETA_UNKOWN = null;
37    private static final HandlingActivity NO_ACTIVITY = null;
38  
39    /**
40     * Creates a new delivery snapshot to reflect changes in routing, i.e.
41     * when the route specification or the itinerary has changed
42     * but no additional handling of the cargo has been performed.
43     *
44     * @param routeSpecification route specification
45     * @param itinerary itinerary
46     * @return An up to date delivery
47     */
48    Delivery updateOnRouting(RouteSpecification routeSpecification, Itinerary itinerary) {
49      Validate.notNull(routeSpecification, "Route specification is required");
50  
51      return new Delivery(this.lastEvent, itinerary, routeSpecification);
52    }
53  
54    /**
55     * Creates a new delivery snapshot based on the complete handling history of a cargo,
56     * as well as its route specification and itinerary.
57     *
58     * @param routeSpecification route specification
59     * @param itinerary itinerary
60     * @param handlingHistory delivery history
61     * @return An up to date delivery.
62     */
63    static Delivery derivedFrom(RouteSpecification routeSpecification, Itinerary itinerary, HandlingHistory handlingHistory) {
64      Validate.notNull(routeSpecification, "Route specification is required");
65      Validate.notNull(handlingHistory, "Delivery history is required");
66  
67      final HandlingEvent lastEvent = handlingHistory.mostRecentlyCompletedEvent();
68  
69      return new Delivery(lastEvent, itinerary, routeSpecification);
70    }
71  
72    /**
73     * Internal constructor.
74     *
75     * @param lastEvent last event
76     * @param itinerary itinerary
77     * @param routeSpecification route specification
78     */
79    private Delivery(HandlingEvent lastEvent, Itinerary itinerary, RouteSpecification routeSpecification) {
80      this.calculatedAt = new Date();
81      this.lastEvent = lastEvent;
82  
83      this.misdirected = calculateMisdirectionStatus(itinerary);
84      this.routingStatus = calculateRoutingStatus(itinerary, routeSpecification);
85      this.transportStatus = calculateTransportStatus();
86      this.lastKnownLocation = calculateLastKnownLocation();
87      this.currentVoyage = calculateCurrentVoyage();
88      this.eta = calculateEta(itinerary);
89      this.nextExpectedActivity = calculateNextExpectedActivity(routeSpecification, itinerary);
90      this.isUnloadedAtDestination = calculateUnloadedAtDestination(routeSpecification);
91    }
92  
93    /**
94     * @return Transport status
95     */
96    public TransportStatus transportStatus() {
97      return transportStatus;
98    }
99  
100   /**
101    * @return Last known location of the cargo, or Location.UNKNOWN if the delivery history is empty.
102    */
103   public Location lastKnownLocation() {
104     return DomainObjectUtils.nullSafe(lastKnownLocation, Location.UNKNOWN);
105   }
106 
107   /**
108    * @return Current voyage.
109    */
110   public Voyage currentVoyage() {
111     return DomainObjectUtils.nullSafe(currentVoyage, Voyage.NONE);
112   }
113 
114   /**
115    * Check if cargo is misdirected.
116    * <p/>
117    * <ul>
118    * <li>A cargo is misdirected if it is in a location that's not in the itinerary.
119    * <li>A cargo with no itinerary can not be misdirected.
120    * <li>A cargo that has received no handling events can not be misdirected.
121    * </ul>
122    *
123    * @return <code>true</code> if the cargo has been misdirected,
124    */
125   public boolean isMisdirected() {
126     return misdirected;
127   }
128 
129   /**
130    * @return Estimated time of arrival
131    */
132   public Date estimatedTimeOfArrival() {
133     if (eta != ETA_UNKOWN) {
134       return new Date(eta.getTime());
135     } else {
136       return ETA_UNKOWN;
137     }
138   }
139 
140   /**
141    * @return The next expected handling activity.
142    */
143   public HandlingActivity nextExpectedActivity() {
144     return nextExpectedActivity;
145   }
146 
147   /**
148    * @return True if the cargo has been unloaded at the final destination.
149    */
150   public boolean isUnloadedAtDestination() {
151     return isUnloadedAtDestination;
152   }
153 
154   /**
155    * @return Routing status.
156    */
157   public RoutingStatus routingStatus() {
158     return routingStatus;
159   }
160 
161   /**
162    * @return When this delivery was calculated.
163    */
164   public Date calculatedAt() {
165     return new Date(calculatedAt.getTime());
166   }
167 
168   // TODO add currentCarrierMovement (?)
169 
170 
171   // --- Internal calculations below ---
172 
173 
174   private TransportStatus calculateTransportStatus() {
175     if (lastEvent == null) {
176       return NOT_RECEIVED;
177     }
178 
179     switch (lastEvent.type()) {
180       case LOAD:
181         return ONBOARD_CARRIER;
182       case UNLOAD:
183       case RECEIVE:
184       case CUSTOMS:
185         return IN_PORT;
186       case CLAIM:
187         return CLAIMED;
188       default:
189         return UNKNOWN;
190     }
191   }
192 
193   private Location calculateLastKnownLocation() {
194     if (lastEvent != null) {
195       return lastEvent.location();
196     } else {
197       return null;
198     }
199   }
200 
201   private Voyage calculateCurrentVoyage() {
202     if (transportStatus().equals(ONBOARD_CARRIER) && lastEvent != null) {
203       return lastEvent.voyage();
204     } else {
205       return null;
206     }
207   }
208 
209   private boolean calculateMisdirectionStatus(Itinerary itinerary) {
210     if (lastEvent == null) {
211       return false;
212     } else {
213       return !itinerary.isExpected(lastEvent);
214     }
215   }
216 
217   private Date calculateEta(Itinerary itinerary) {
218     if (onTrack()) {
219       return itinerary.finalArrivalDate();
220     } else {
221       return ETA_UNKOWN;
222     }
223   }
224 
225   private HandlingActivity calculateNextExpectedActivity(RouteSpecification routeSpecification, Itinerary itinerary) {
226     if (!onTrack()) return NO_ACTIVITY;
227 
228     if (lastEvent == null) return new HandlingActivity(HandlingEvent.Type.RECEIVE, routeSpecification.origin());
229 
230     switch (lastEvent.type()) {
231 
232       case LOAD:
233         for (Leg leg : itinerary.legs()) {
234           if (leg.loadLocation().sameIdentityAs(lastEvent.location())) {
235             return new HandlingActivity(HandlingEvent.Type.UNLOAD, leg.unloadLocation(), leg.voyage());
236           }
237         }
238 
239         return NO_ACTIVITY;
240 
241       case UNLOAD:
242         for (Iterator<Leg> it = itinerary.legs().iterator(); it.hasNext();) {
243           final Leg leg = it.next();
244           if (leg.unloadLocation().sameIdentityAs(lastEvent.location())) {
245             if (it.hasNext()) {
246               final Leg nextLeg = it.next();
247               return new HandlingActivity(HandlingEvent.Type.LOAD, nextLeg.loadLocation(), nextLeg.voyage());
248             } else {
249               return new HandlingActivity(HandlingEvent.Type.CLAIM, leg.unloadLocation());
250             }
251           }
252         }
253 
254         return NO_ACTIVITY;
255 
256       case RECEIVE:
257         final Leg firstLeg = itinerary.legs().iterator().next();
258         return new HandlingActivity(HandlingEvent.Type.LOAD, firstLeg.loadLocation(), firstLeg.voyage());
259 
260       case CLAIM:
261       default:
262         return NO_ACTIVITY;
263     }
264   }
265 
266   private RoutingStatus calculateRoutingStatus(Itinerary itinerary, RouteSpecification routeSpecification) {
267     if (itinerary == null) {
268       return NOT_ROUTED;
269     } else {
270       if (routeSpecification.isSatisfiedBy(itinerary)) {
271         return ROUTED;
272       } else {
273         return MISROUTED;
274       }
275     }
276   }
277 
278   private boolean calculateUnloadedAtDestination(RouteSpecification routeSpecification) {
279     return lastEvent != null &&
280       HandlingEvent.Type.UNLOAD.sameValueAs(lastEvent.type()) &&
281       routeSpecification.destination().sameIdentityAs(lastEvent.location());
282   }
283 
284   private boolean onTrack() {
285     return routingStatus.equals(ROUTED) && !misdirected;
286   }
287 
288   @Override
289   public boolean sameValueAs(final Delivery other) {
290     return other != null && new EqualsBuilder().
291       append(this.transportStatus, other.transportStatus).
292       append(this.lastKnownLocation, other.lastKnownLocation).
293       append(this.currentVoyage, other.currentVoyage).
294       append(this.misdirected, other.misdirected).
295       append(this.eta, other.eta).
296       append(this.nextExpectedActivity, other.nextExpectedActivity).
297       append(this.isUnloadedAtDestination, other.isUnloadedAtDestination).
298       append(this.routingStatus, other.routingStatus).
299       append(this.calculatedAt, other.calculatedAt).
300       append(this.lastEvent, other.lastEvent).
301       isEquals();
302   }
303 
304   @Override
305   public boolean equals(final Object o) {
306     if (this == o) return true;
307     if (o == null || getClass() != o.getClass()) return false;
308 
309     final Delivery other = (Delivery) o;
310 
311     return sameValueAs(other);
312   }
313 
314   @Override
315   public int hashCode() {
316     return new HashCodeBuilder().
317       append(transportStatus).
318       append(lastKnownLocation).
319       append(currentVoyage).
320       append(misdirected).
321       append(eta).
322       append(nextExpectedActivity).
323       append(isUnloadedAtDestination).
324       append(routingStatus).
325       append(calculatedAt).
326       append(lastEvent).
327       toHashCode();
328   }
329 
330   Delivery() {
331     // Needed by Hibernate
332   }
333 }