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 }