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 }