View Javadoc

1   package se.citerus.dddsample.domain.model.cargo;
2   
3   import org.apache.commons.lang.Validate;
4   import se.citerus.dddsample.domain.model.handling.HandlingEvent;
5   import se.citerus.dddsample.domain.model.handling.HandlingHistory;
6   import se.citerus.dddsample.domain.model.location.Location;
7   import se.citerus.dddsample.domain.shared.DomainObjectUtils;
8   import se.citerus.dddsample.domain.shared.Entity;
9   
10  /**
11   * A Cargo. This is the central class in the domain model,
12   * and it is the root of the Cargo-Itinerary-Leg-Delivery-RouteSpecification aggregate.
13   *
14   * A cargo is identified by a unique tracking id, and it always has an origin
15   * and a route specification. The life cycle of a cargo begins with the booking procedure,
16   * when the tracking id is assigned. During a (short) period of time, between booking
17   * and initial routing, the cargo has no itinerary.
18   *
19   * The booking clerk requests a list of possible routes, matching the route specification,
20   * and assigns the cargo to one route. The route to which a cargo is assigned is described
21   * by an itinerary.
22   *
23   * A cargo can be re-routed during transport, on demand of the customer, in which case
24   * a new route is specified for the cargo and a new route is requested. The old itinerary,
25   * being a value object, is discarded and a new one is attached.
26   *
27   * It may also happen that a cargo is accidentally misrouted, which should notify the proper
28   * personnel and also trigger a re-routing procedure.
29   *
30   * When a cargo is handled, the status of the delivery changes. Everything about the delivery
31   * of the cargo is contained in the Delivery value object, which is replaced whenever a cargo
32   * is handled by an asynchronous event triggered by the registration of the handling event.
33   *
34   * The delivery can also be affected by routing changes, i.e. when a the route specification
35   * changes, or the cargo is assigned to a new route. In that case, the delivery update is performed
36   * synchronously within the cargo aggregate.
37   *
38   * The life cycle of a cargo ends when the cargo is claimed by the customer.
39   *
40   * The cargo aggregate, and the entre domain model, is built to solve the problem
41   * of booking and tracking cargo. All important business rules for determining whether
42   * or not a cargo is misdirected, what the current status of the cargo is (on board carrier,
43   * in port etc), are captured in this aggregate.
44   *
45   */
46  public class Cargo implements Entity<Cargo> {
47  
48    private TrackingId trackingId;
49    private Location origin;
50    private RouteSpecification routeSpecification;
51    private Itinerary itinerary;
52    private Delivery delivery;
53  
54    public Cargo(final TrackingId trackingId, final RouteSpecification routeSpecification) {
55      Validate.notNull(trackingId, "Tracking ID is required");
56      Validate.notNull(routeSpecification, "Route specification is required");
57  
58      this.trackingId = trackingId;
59      // Cargo origin never changes, even if the route specification changes.
60      // However, at creation, cargo orgin can be derived from the initial route specification.
61      this.origin = routeSpecification.origin();
62      this.routeSpecification = routeSpecification;
63  
64      this.delivery = Delivery.derivedFrom(
65        this.routeSpecification, this.itinerary, HandlingHistory.EMPTY
66      );
67    }
68  
69    /**
70     * The tracking id is the identity of this entity, and is unique.
71     * 
72     * @return Tracking id.
73     */
74    public TrackingId trackingId() {
75      return trackingId;
76    }
77  
78    /**
79     * @return Origin location.
80     */
81    public Location origin() {
82      return origin;
83    }
84  
85    /**
86     * @return The delivery. Never null.
87     */
88    public Delivery delivery() {
89      return delivery;
90    }
91  
92    /**
93     * @return The itinerary. Never null.
94     */
95    public Itinerary itinerary() {
96      return DomainObjectUtils.nullSafe(this.itinerary, Itinerary.EMPTY_ITINERARY);
97    }
98  
99    /**
100    * @return The route specification.
101    */
102   public RouteSpecification routeSpecification() {
103     return routeSpecification;
104   }
105   
106   /**
107    * Specifies a new route for this cargo.
108    *
109    * @param routeSpecification route specification.
110    */
111   public void specifyNewRoute(final RouteSpecification routeSpecification) {
112     Validate.notNull(routeSpecification, "Route specification is required");
113 
114     this.routeSpecification = routeSpecification;
115     // Handling consistency within the Cargo aggregate synchronously
116     this.delivery = delivery.updateOnRouting(this.routeSpecification, this.itinerary);
117   }
118 
119   /**
120    * Attach a new itinerary to this cargo.
121    *
122    * @param itinerary an itinerary. May not be null.
123    */
124   public void assignToRoute(final Itinerary itinerary) {
125     Validate.notNull(itinerary, "Itinerary is required for assignment");
126 
127     this.itinerary = itinerary;
128     // Handling consistency within the Cargo aggregate synchronously
129     this.delivery = delivery.updateOnRouting(this.routeSpecification, this.itinerary);
130   }
131 
132   /**
133    * Updates all aspects of the cargo aggregate status
134    * based on the current route specification, itinerary and handling of the cargo.
135    * <p/>
136    * When either of those three changes, i.e. when a new route is specified for the cargo,
137    * the cargo is assigned to a route or when the cargo is handled, the status must be
138    * re-calculated.
139    * <p/>
140    * {@link RouteSpecification} and {@link Itinerary} are both inside the Cargo
141    * aggregate, so changes to them cause the status to be updated <b>synchronously</b>,
142    * but changes to the delivery history (when a cargo is handled) cause the status update
143    * to happen <b>asynchronously</b> since {@link HandlingEvent} is in a different aggregate.
144    *
145    * @param handlingHistory handling history
146    */
147   public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
148     // TODO filter events on cargo (must be same as this cargo)
149 
150     // Delivery is a value object, so we can simply discard the old one
151     // and replace it with a new
152     this.delivery = Delivery.derivedFrom(routeSpecification(), itinerary(), handlingHistory);
153   }
154 
155   @Override
156   public boolean sameIdentityAs(final Cargo other) {
157     return other != null && trackingId.sameValueAs(other.trackingId);
158   }
159 
160   /**
161    * @param object to compare
162    * @return True if they have the same identity
163    * @see #sameIdentityAs(Cargo)
164    */
165   @Override
166   public boolean equals(final Object object) {
167     if (this == object) return true;
168     if (object == null || getClass() != object.getClass()) return false;
169 
170     final Cargo other = (Cargo) object;
171     return sameIdentityAs(other);
172   }
173 
174   /**
175    * @return Hash code of tracking id.
176    */
177   @Override
178   public int hashCode() {
179     return trackingId.hashCode();
180   }
181 
182   @Override
183   public String toString() {
184     return trackingId.toString();
185   }
186 
187   Cargo() {
188     // Needed by Hibernate
189   }
190 
191   // Auto-generated surrogate key
192   private Long id;
193 
194 }