瀏覽代碼

Asynchronous update of the Cargo aggregate. Several properties of the Cargo aggregate are now calculated and persisted, on handling (asynchronously) and on changes to routing (synchronously, since that's inside the cargo aggregate).

Relation between Cargo and its complete delivery history (handling events) is now replaced by explicit repository lookup at the time of cargo status update.

The copy() method on value objects is removed.

All enums in the cargo are now value objects.
peter_backlund 17 年之前
父節點
當前提交
924b0784c8
共有 19 個檔案被更改,包括 246 行新增200 行删除
  1. 1
    0
      dddsample/src/main/java/se/citerus/dddsample/application/TrackingService.java
  2. 12
    3
      dddsample/src/main/java/se/citerus/dddsample/application/impl/TrackingServiceImpl.java
  3. 8
    8
      dddsample/src/main/java/se/citerus/dddsample/application/util/SampleDataGenerator.java
  4. 0
    7
      dddsample/src/main/java/se/citerus/dddsample/domain/model/ValueObject.java
  5. 63
    41
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Cargo.java
  6. 1
    1
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/CargoRepository.java
  7. 92
    54
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Delivery.java
  8. 3
    13
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Itinerary.java
  9. 0
    5
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Leg.java
  10. 2
    7
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/RouteSpecification.java
  11. 11
    3
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/RoutingStatus.java
  12. 0
    5
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/TrackingId.java
  13. 9
    2
      dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/TransportStatus.java
  14. 0
    10
      dddsample/src/main/java/se/citerus/dddsample/domain/model/carrier/CarrierMovement.java
  15. 3
    14
      dddsample/src/main/java/se/citerus/dddsample/domain/model/carrier/Schedule.java
  16. 0
    5
      dddsample/src/main/java/se/citerus/dddsample/domain/model/carrier/VoyageNumber.java
  17. 23
    13
      dddsample/src/main/java/se/citerus/dddsample/domain/model/handling/HandlingEvent.java
  18. 0
    5
      dddsample/src/main/java/se/citerus/dddsample/domain/model/location/UnLocode.java
  19. 18
    4
      dddsample/src/main/resources/se/citerus/dddsample/infrastructure/persistence/hibernate/Cargo.hbm.xml

+ 1
- 0
dddsample/src/main/java/se/citerus/dddsample/application/TrackingService.java 查看文件

@@ -15,6 +15,7 @@ public interface TrackingService {
15 15
    * @param trackingId cargo tracking id
16 16
    */
17 17
   // TODO rename! The method updates the delivery status on handling
18
+  // TODO [Cargo]InspectionService is fine 
18 19
   void inspectCargo(TrackingId trackingId);
19 20
 
20 21
 }

+ 12
- 3
dddsample/src/main/java/se/citerus/dddsample/application/impl/TrackingServiceImpl.java 查看文件

@@ -9,20 +9,28 @@ import se.citerus.dddsample.application.TrackingService;
9 9
 import se.citerus.dddsample.domain.model.cargo.Cargo;
10 10
 import se.citerus.dddsample.domain.model.cargo.CargoRepository;
11 11
 import se.citerus.dddsample.domain.model.cargo.TrackingId;
12
+import se.citerus.dddsample.domain.model.handling.HandlingEvent;
13
+import se.citerus.dddsample.domain.model.handling.HandlingEventRepository;
14
+
15
+import java.util.List;
12 16
 
13 17
 public class TrackingServiceImpl implements TrackingService {
14 18
 
15 19
   private final ApplicationEvents applicationEvents;
16 20
   private final CargoRepository cargoRepository;
21
+  private final HandlingEventRepository handlingEventRepository;
17 22
   private final Log logger = LogFactory.getLog(getClass());
18 23
 
19
-  public TrackingServiceImpl(final ApplicationEvents applicationEvents, final CargoRepository cargoRepository) {
24
+  public TrackingServiceImpl(final ApplicationEvents applicationEvents,
25
+                             final CargoRepository cargoRepository,
26
+                             final HandlingEventRepository handlingEventRepository) {
20 27
     this.applicationEvents = applicationEvents;
21 28
     this.cargoRepository = cargoRepository;
29
+    this.handlingEventRepository = handlingEventRepository;
22 30
   }
23 31
 
24 32
   @Override
25
-  @Transactional(readOnly = true)
33
+  @Transactional
26 34
   public void inspectCargo(final TrackingId trackingId) {
27 35
     Validate.notNull(trackingId, "Tracking ID is required");
28 36
 
@@ -32,7 +40,8 @@ public class TrackingServiceImpl implements TrackingService {
32 40
       return;
33 41
     }
34 42
 
35
-    // TODO cargo delivery status update would happen here
43
+    final List<HandlingEvent> deliveryHistory = handlingEventRepository.findEventsForCargo(trackingId);
44
+    cargo.updateStatus(deliveryHistory);
36 45
 
37 46
     if (cargo.isMisdirected()) {
38 47
       applicationEvents.cargoWasMisdirected(cargo);

+ 8
- 8
dddsample/src/main/java/se/citerus/dddsample/application/util/SampleDataGenerator.java 查看文件

@@ -120,16 +120,16 @@ public class SampleDataGenerator implements ServletContextListener {
120 120
 
121 121
   private static void loadCargoData(JdbcTemplate jdbcTemplate) {
122 122
     String cargoSql =
123
-      "insert into Cargo (id, tracking_id, origin_id, spec_origin_id, spec_destination_id, spec_arrival_deadline) " +
124
-      "values (?, ?, ?, ?, ?, ?)";
123
+      "insert into Cargo (id, tracking_id, origin_id, spec_origin_id, spec_destination_id, spec_arrival_deadline, transport_status, current_voyage_id, last_known_location_id, is_misdirected, routing_status) " +
124
+      "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
125 125
 
126 126
     Object[][] cargoArgs = {
127
-      {1, "XYZ", 1, 1, 2, ts(10)},
128
-      {2, "ABC", 1, 1, 5, ts(20)},
129
-      {3, "ZYX", 2, 2, 1, ts(30)},
130
-      {4, "CBA", 5, 5, 1, ts(40)},
131
-      {5, "FGH", 1, 3, 5, ts(50)},  // Cargo origin differs from spec origin
132
-      {6, "JKL", 6, 6, 4, ts(60)}
127
+      {1, "XYZ", 1, 1, 2, ts(10), "IN_PORT", null, 1, false, "ROUTED"},
128
+      {2, "ABC", 1, 1, 5, ts(20), "IN_PORT", null, 1, false, "ROUTED"},
129
+      {3, "ZYX", 2, 2, 1, ts(30), "IN_PORT", null, 1, false, "NOT_ROUTED"},
130
+      {4, "CBA", 5, 5, 1, ts(40), "IN_PORT", null, 1, false, "MISROUTED"},
131
+      {5, "FGH", 1, 3, 5, ts(50), "IN_PORT", null, 1, false, "ROUTED"},  // Cargo origin differs from spec origin
132
+      {6, "JKL", 6, 6, 4, ts(60), "IN_PORT", null, 1, true, "ROUTED"}
133 133
     };
134 134
     executeUpdate(jdbcTemplate, cargoSql, cargoArgs);
135 135
   }

+ 0
- 7
dddsample/src/main/java/se/citerus/dddsample/domain/model/ValueObject.java 查看文件

@@ -16,11 +16,4 @@ public interface ValueObject<T> extends Serializable {
16 16
    */
17 17
   boolean sameValueAs(T other);
18 18
 
19
-  /**
20
-   * Value objects may be freely copied.
21
-   *
22
-   * @return A deep copy of this object.
23
-   */
24
-  T copy();
25
-
26 19
 }

+ 63
- 41
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Cargo.java 查看文件

@@ -7,21 +7,24 @@ import se.citerus.dddsample.domain.model.handling.HandlingEvent;
7 7
 import se.citerus.dddsample.domain.model.location.Location;
8 8
 import se.citerus.dddsample.domain.shared.DomainObjectUtils;
9 9
 
10
+import java.util.Collections;
11
+import java.util.List;
12
+
10 13
 /**
11 14
  * A Cargo. This is the central class in the domain model,
12
- * and it is the root of the Cargo-Itinerary-Leg-DeliveryHistory aggregate.
15
+ * and it is the root of the Cargo-Itinerary-Leg-Delivery-RouteSpecification aggregate.
13 16
  *
14 17
  * A cargo is identified by a unique tracking id, and it always has an origin
15
- * and a destination. The life cycle of a cargo begins with the booking procedure,
18
+ * and a route specification. The life cycle of a cargo begins with the booking procedure,
16 19
  * when the tracking id is assigned. During a (short) period of time, between booking
17 20
  * and initial routing, the cargo has no itinerary.
18 21
  *
19
- * The booking clerk requests a list of possible routes, matching a route specification,
20
- * and assigns the cargo to one route. An itinerary listing the legs of the route
21
- * is attached to the cargo.
22
+ * The booking clerk requests a list of possible routes, matching the route specification,
23
+ * and assigns the cargo to one route. The route to which a cargo is assigned is described
24
+ * by an itinerary.
22 25
  *
23 26
  * A cargo can be re-routed during transport, on demand of the customer, in which case
24
- * the destination is changed and a new route is requested. The old itinerary,
27
+ * a new route is specified for the cargo and a new route is requested. The old itinerary,
25 28
  * being a value object, is discarded and a new one is attached.
26 29
  *
27 30
  * It may also happen that a cargo is accidentally misrouted, which should notify the proper
@@ -42,6 +45,8 @@ public class Cargo implements Entity<Cargo> {
42 45
   private Itinerary itinerary;
43 46
   private Delivery delivery;
44 47
   private RouteSpecification routeSpecification;
48
+  private RoutingStatus routingStatus;
49
+  private boolean misdirected;
45 50
 
46 51
   // TODO origin can be taken from route spec on creation, even if the origin never changes
47 52
   public Cargo(final TrackingId trackingId, final Location origin, final RouteSpecification routeSpecification) {
@@ -52,9 +57,9 @@ public class Cargo implements Entity<Cargo> {
52 57
     this.trackingId = trackingId;
53 58
     this.origin = origin;
54 59
     this.routeSpecification = routeSpecification;
60
+    updateStatus(Collections.<HandlingEvent>emptyList());
55 61
   }
56 62
 
57
-
58 63
   /**
59 64
    * The tracking id is the identity of this entity, and is unique.
60 65
    * 
@@ -93,21 +98,16 @@ public class Cargo implements Entity<Cargo> {
93 98
   }
94 99
   
95 100
   /**
96
-   * @return True if the cargo has arrived at its destination.
97
-   */
98
-  public boolean hasArrived() {
99
-    return routeSpecification.destination().equals(delivery.lastKnownLocation());
100
-  }
101
-
102
-  /**
103
-   * Specifies a route for this cargo.
101
+   * Specifies a new route for this cargo.
104 102
    *
105 103
    * @param routeSpecification route specification.
106 104
    */
107
-  public void specifyRoute(RouteSpecification routeSpecification) {
105
+  public void specifyNewRoute(final RouteSpecification routeSpecification) {
108 106
     Validate.notNull(routeSpecification);
109 107
 
110 108
     this.routeSpecification = routeSpecification;
109
+    // Handling consistency within the Cargo aggregate synchronously
110
+    updateRoutingStatus();
111 111
   }
112 112
 
113 113
   /**
@@ -116,16 +116,12 @@ public class Cargo implements Entity<Cargo> {
116 116
    * @param itinerary an itinerary. May not be null.
117 117
    */
118 118
   public void assignToRoute(final Itinerary itinerary) {
119
-    Validate.notNull(itinerary);
120
-    this.itinerary = itinerary;
121
-  }
119
+    Validate.notNull(itinerary, "Itinerary is required for assignment");
122 120
 
123
-  /**
124
-   * @param delivery Cargo delivery history
125
-   */
126
-  void setDeliveryHistory(final Delivery delivery) {
127
-    Validate.notNull(delivery);
128
-    this.delivery = delivery;
121
+    this.itinerary = itinerary;
122
+    // Handling consistency within the Cargo aggregate synchronously
123
+    updateRoutingStatus();
124
+    updateIsMisdirected();
129 125
   }
130 126
 
131 127
   /**
@@ -140,44 +136,70 @@ public class Cargo implements Entity<Cargo> {
140 136
    * @return <code>true</code> if the cargo has been misdirected,
141 137
    */
142 138
   public boolean isMisdirected() {
139
+    return misdirected;
140
+  }
141
+
142
+  private void updateIsMisdirected() {
143 143
     final HandlingEvent lastEvent = delivery().lastEvent();
144 144
     if (lastEvent == null) {
145
-      return false;
145
+      misdirected = false;
146 146
     } else {
147
-      return !itinerary().isExpected(lastEvent);
147
+      misdirected = !itinerary().isExpected(lastEvent);
148 148
     }
149 149
   }
150 150
 
151
+
151 152
   /**
152 153
    * @return Routing status.
153 154
    */
154 155
   public RoutingStatus routingStatus() {
156
+    return routingStatus;
157
+  }
158
+
159
+  /**
160
+   * Updates the routing status.
161
+   */
162
+  private void updateRoutingStatus() {
155 163
     if (itinerary == null) {
156
-      return NOT_ROUTED;
164
+      routingStatus = NOT_ROUTED;
157 165
     } else {
158 166
       if (routeSpecification.isSatisfiedBy(itinerary)) {
159
-        return ROUTED;
167
+        routingStatus = ROUTED;
160 168
       } else {
161
-        return MISROUTED;
169
+        routingStatus = MISROUTED;
162 170
       }
163 171
     }
164 172
   }
165 173
 
166 174
   /**
167
-   * Does not take into account the possibility of the cargo having been
168
-   * (errouneously) loaded onto another carrier after it has been unloaded
169
-   * at the final destination.
170
-   *
171 175
    * @return True if the cargo has been unloaded at the final destination.
172 176
    */
173 177
   public boolean isUnloadedAtDestination() {
174
-    for (HandlingEvent event : delivery().history()) {
175
-      if (HandlingEvent.Type.UNLOAD.equals(event.type())
176
-        && routeSpecification.destination().equals(event.location())) {
177
-        return true;
178
-      }
179
-    }
180
-    return false;
178
+    final HandlingEvent lastEvent = delivery.lastEvent();
179
+    return lastEvent != null &&
180
+           HandlingEvent.Type.UNLOAD.sameValueAs(lastEvent.type()) &&
181
+           routeSpecification.destination().sameIdentityAs(lastEvent.location());
182
+  }
183
+
184
+  /**
185
+   * Updates all aspects of the cargo aggregate status
186
+   * based on the current route specification, itinerary and delivery history.
187
+   * <p/>
188
+   * When either of those three changes, i.e. when a new route is specified for the cargo,
189
+   * the cargo is assigned to a route or when the cargo is handled, the status must be
190
+   * re-calculated.
191
+   * <p/>
192
+   * {@link RouteSpecification} and {@link Itinerary} are both inside the Cargo
193
+   * aggregate, so changes to them cause the status to be updated <b>synchronously</b>,
194
+   * but changes to the delivery history (when a cargo is handled) cause the status update
195
+   * to happen <b>asynchronously</b> since {@link HandlingEvent} is in a different aggregate.
196
+   * @param handlingEvents
197
+   */
198
+  public void updateStatus(final List<HandlingEvent> handlingEvents) {
199
+    // Delivery is a value object, so we can simply discard the old one and replace with a new
200
+    delivery = Delivery.derivedFrom(handlingEvents);
201
+    updateRoutingStatus();
202
+    updateIsMisdirected();
181 203
   }
182 204
 
183 205
   @Override

+ 1
- 1
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/CargoRepository.java 查看文件

@@ -28,7 +28,7 @@ public interface CargoRepository {
28 28
 
29 29
   /**
30 30
    * TODO
31
-   * this is too complex a procedure to belong in the repository -
31
+   * this might be too complex a procedure to belong in the repository -
32 32
    * introduce a TrackingIdFactory (or perhaps a CargoFactory).
33 33
    *
34 34
    * @return A new generated tracking Id.

+ 92
- 54
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Delivery.java 查看文件

@@ -1,12 +1,21 @@
1 1
 package se.citerus.dddsample.domain.model.cargo;
2 2
 
3
+import org.apache.commons.lang.Validate;
4
+import org.apache.commons.lang.builder.EqualsBuilder;
5
+import org.apache.commons.lang.builder.HashCodeBuilder;
3 6
 import se.citerus.dddsample.domain.model.ValueObject;
4 7
 import static se.citerus.dddsample.domain.model.cargo.TransportStatus.*;
5 8
 import se.citerus.dddsample.domain.model.carrier.Voyage;
6 9
 import se.citerus.dddsample.domain.model.handling.HandlingEvent;
7 10
 import se.citerus.dddsample.domain.model.location.Location;
11
+import se.citerus.dddsample.domain.shared.DomainObjectUtils;
8 12
 
9
-import java.util.*;
13
+import java.util.ArrayList;
14
+import java.util.Collection;
15
+import static java.util.Collections.EMPTY_SET;
16
+import static java.util.Collections.sort;
17
+import java.util.HashSet;
18
+import java.util.List;
10 19
 
11 20
 /**
12 21
  * The actual result of the cargo transportation, as opposed to
@@ -15,94 +24,117 @@ import java.util.*;
15 24
  */
16 25
 public class Delivery implements ValueObject<Delivery> {
17 26
 
18
-  private Set<HandlingEvent> events;
27
+  public static final Delivery EMPTY_DELIVERY = Delivery.derivedFrom(EMPTY_SET);
19 28
 
20
-  public static final Delivery EMPTY_DELIVERY = new Delivery(Collections.EMPTY_SET);
29
+  private TransportStatus transportStatus;
30
+  private Location lastKnownLocation;
31
+  private Voyage currentVoyage;
32
+  private HandlingEvent lastEvent;
21 33
 
22
-  Delivery(final Collection<HandlingEvent> events) {
23
-    this.events = new HashSet<HandlingEvent>(events);
34
+  /**
35
+   * @return Transport status
36
+   */
37
+  public TransportStatus transportStatus() {
38
+    return transportStatus;
39
+  }
40
+
41
+  /**
42
+   * @return Last known location of the cargo, or Location.UNKNOWN if the delivery history is empty.
43
+   */
44
+  public Location lastKnownLocation() {
45
+    return DomainObjectUtils.nullSafe(lastKnownLocation, Location.UNKNOWN);
46
+  }
47
+
48
+  /**
49
+   * @return Current voyage.
50
+   */
51
+  public Voyage currentVoyage() {
52
+    return DomainObjectUtils.nullSafe(currentVoyage, Voyage.NONE);
24 53
   }
25 54
 
26 55
   /**
27
-   * @return An <b>unmodifiable</b> list of handling events, ordered by the time the events occured.
56
+   * @param handlingEvents handling events
57
+   * @return An up to date Delivery derived from this collection of handling events.
28 58
    */
29
-  public List<HandlingEvent> history() {
30
-    final List<HandlingEvent> eventList = new ArrayList<HandlingEvent>(events);
31
-    Collections.sort(eventList, HandlingEvent.BY_COMPLETION_TIME_COMPARATOR);
32
-    return Collections.unmodifiableList(eventList);
59
+  static Delivery derivedFrom(final Collection<HandlingEvent> handlingEvents) {
60
+    Validate.notNull(handlingEvents, "Handling events are required");
61
+    
62
+    final List<HandlingEvent> eventsByCompletionTime =
63
+      new ArrayList<HandlingEvent>(new HashSet<HandlingEvent>(handlingEvents));
64
+    sort(eventsByCompletionTime, HandlingEvent.BY_COMPLETION_TIME_COMPARATOR);
65
+
66
+    final Delivery delivery = new Delivery();
67
+    delivery.calculateLastEvent(eventsByCompletionTime);
68
+    delivery.calculateTransportStatus();
69
+    delivery.calculateLastKnownLocation();
70
+    delivery.calculateCurrentVoyage();
71
+    return delivery;
33 72
   }
34 73
 
35 74
   /**
36 75
    * @return The last event of the delivery history, or null is history is empty.
37 76
    */
38
-  public HandlingEvent lastEvent() {
39
-    if (events.isEmpty()) {
40
-      return null;
77
+  HandlingEvent lastEvent() {
78
+    return lastEvent;
79
+  }
80
+
81
+  private void calculateLastEvent(final List<HandlingEvent> handlingEvents) {
82
+    if (handlingEvents.isEmpty()) {
83
+      lastEvent =  null;
41 84
     } else {
42
-      final List<HandlingEvent> orderedEvents = history();
43
-      return orderedEvents.get(orderedEvents.size() - 1);
85
+      lastEvent = handlingEvents.get(handlingEvents.size() - 1);
44 86
     }
45 87
   }
46 88
 
47
-  /**
48
-   * @return
49
-   */
50
-  public TransportStatus transportStatus() {
51
-    if (lastEvent() == null)
52
-      return NOT_RECEIVED;
89
+  private void calculateTransportStatus() {
90
+    if (lastEvent == null) {
91
+      transportStatus = NOT_RECEIVED;
92
+      return;
93
+    }
53 94
 
54
-    final HandlingEvent.Type type = lastEvent().type();
55
-    
56
-    switch (type) {
95
+    switch (lastEvent.type()) {
57 96
       case LOAD:
58
-        return ONBOARD_CARRIER;
59
-
97
+        transportStatus = ONBOARD_CARRIER;
98
+        break;
60 99
       case UNLOAD:
61 100
       case RECEIVE:
62 101
       case CUSTOMS:
63
-        return IN_PORT;
64
-
102
+        transportStatus = IN_PORT;
103
+        break;
65 104
       case CLAIM:
66
-        return CLAIMED;
67
-
105
+        transportStatus = CLAIMED;
106
+        break;
68 107
       default:
69
-        return null;
108
+        transportStatus = UNKNOWN;
70 109
     }
71 110
   }
72 111
 
73
-  /**
74
-   * @return Last known location of the cargo, or Location.UNKNOWN if the delivery history is empty.
75
-   */
76
-  public Location lastKnownLocation() {
77
-    final HandlingEvent lastEvent = lastEvent();
112
+  private void calculateLastKnownLocation() {
78 113
     if (lastEvent != null) {
79
-      return lastEvent.location();
114
+      lastKnownLocation = lastEvent.location();
80 115
     } else {
81
-      return Location.UNKNOWN;
116
+      lastKnownLocation = null;
82 117
     }
83 118
   }
84 119
 
85
-  /**
86
-   * @return Current voyage.
87
-   */
88
-  public Voyage currentVoyage() {
89
-    if (transportStatus().equals(ONBOARD_CARRIER)) {
90
-      return lastEvent().voyage();
120
+  // TODO add currentCarrierMovement
121
+
122
+  private void calculateCurrentVoyage() {
123
+    if (transportStatus().equals(ONBOARD_CARRIER) && lastEvent != null) {
124
+      currentVoyage = lastEvent.voyage();
91 125
     } else {
92
-      return Voyage.NONE;
126
+      currentVoyage = null;
93 127
     }
94 128
   }
95 129
 
96 130
   @Override
97 131
   public boolean sameValueAs(Delivery other) {
98
-    return other != null && events.equals(other.events);
99
-  }
100
-
101
-  @Override
102
-  public Delivery copy() {
103
-    final Set<HandlingEvent> eventsCopy = new HashSet<HandlingEvent>(events);
104
-
105
-    return new Delivery(eventsCopy);
132
+    return other != null && new EqualsBuilder().
133
+      append(this.transportStatus, other.transportStatus).
134
+      append(this.lastKnownLocation, other.lastKnownLocation).
135
+      append(this.currentVoyage, other.currentVoyage).
136
+      append(this.lastEvent, other.lastEvent).
137
+      isEquals();
106 138
   }
107 139
 
108 140
   @Override
@@ -117,10 +149,16 @@ public class Delivery implements ValueObject<Delivery> {
117 149
 
118 150
   @Override
119 151
   public int hashCode() {
120
-    return events.hashCode();
152
+    return new HashCodeBuilder().
153
+      append(transportStatus).
154
+      append(lastKnownLocation).
155
+      append(currentVoyage).
156
+      append(lastEvent).
157
+      toHashCode();
121 158
   }
122 159
 
123 160
   Delivery() {
124 161
     // Needed by Hibernate
125 162
   }
163
+
126 164
 }

+ 3
- 13
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Itinerary.java 查看文件

@@ -5,14 +5,13 @@ import se.citerus.dddsample.domain.model.ValueObject;
5 5
 import se.citerus.dddsample.domain.model.handling.HandlingEvent;
6 6
 import se.citerus.dddsample.domain.model.location.Location;
7 7
 
8
-import java.util.ArrayList;
9 8
 import java.util.Collections;
10 9
 import java.util.Date;
11 10
 import java.util.List;
12 11
 
13 12
 /**
14 13
  * An itinerary.
15
- * 
14
+ *
16 15
  */
17 16
 public class Itinerary implements ValueObject<Itinerary> {
18 17
 
@@ -29,7 +28,7 @@ public class Itinerary implements ValueObject<Itinerary> {
29 28
   public Itinerary(final List<Leg> legs) {
30 29
     Validate.notEmpty(legs);
31 30
     Validate.noNullElements(legs);
32
-    
31
+
33 32
     this.legs = legs;
34 33
   }
35 34
 
@@ -116,7 +115,7 @@ public class Itinerary implements ValueObject<Itinerary> {
116 115
     final Leg lastLeg = lastLeg();
117 116
 
118 117
     if (lastLeg == null) {
119
-      return END_OF_DAYS;
118
+      return new Date(END_OF_DAYS.getTime());
120 119
     } else {
121 120
       return lastLeg.unloadTime();
122 121
     }
@@ -143,15 +142,6 @@ public class Itinerary implements ValueObject<Itinerary> {
143 142
   }
144 143
 
145 144
   @Override
146
-  public Itinerary copy() {
147
-    final List<Leg> legsCopy = new ArrayList<Leg>(legs.size());
148
-    for (Leg leg : legs) {
149
-      legsCopy.add(leg.copy());
150
-    }
151
-    return new Itinerary(legsCopy);
152
-  }
153
-
154
-  @Override
155 145
   public boolean equals(final Object o) {
156 146
     if (this == o) return true;
157 147
     if (o == null || getClass() != o.getClass()) return false;

+ 0
- 5
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/Leg.java 查看文件

@@ -62,11 +62,6 @@ public class Leg implements ValueObject<Leg> {
62 62
   }
63 63
 
64 64
   @Override
65
-  public Leg copy() {
66
-    return new Leg(voyage(), loadLocation(), unloadLocation(), loadTime(), unloadTime());
67
-  }
68
-
69
-  @Override
70 65
   public boolean equals(final Object o) {
71 66
     if (this == o) return true;
72 67
     if (o == null || getClass() != o.getClass()) return false;

+ 2
- 7
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/RouteSpecification.java 查看文件

@@ -29,7 +29,7 @@ public class RouteSpecification extends AbstractSpecification<Itinerary> impleme
29 29
     Validate.notNull(origin, "Origin is required");
30 30
     Validate.notNull(destination, "Destination is required");
31 31
     Validate.notNull(arrivalDeadline, "Arrival deadline is required");
32
-    Validate.isTrue(origin.equals(destination), "Origin and destination can't be the same: " + origin);
32
+    Validate.isTrue(!origin.sameIdentityAs(destination), "Origin and destination can't be the same: " + origin);
33 33
 
34 34
     this.origin = origin;
35 35
     this.destination = destination;
@@ -75,16 +75,11 @@ public class RouteSpecification extends AbstractSpecification<Itinerary> impleme
75 75
   }
76 76
 
77 77
   @Override
78
-  public RouteSpecification copy() {
79
-    return new RouteSpecification(origin, destination, arrivalDeadline);
80
-  }
81
-
82
-  @Override
83 78
   public boolean equals(final Object o) {
84 79
     if (this == o) return true;
85 80
     if (o == null || getClass() != o.getClass()) return false;
86 81
 
87
-    RouteSpecification that = (RouteSpecification) o;
82
+    final RouteSpecification that = (RouteSpecification) o;
88 83
 
89 84
     return sameValueAs(that);
90 85
   }

+ 11
- 3
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/RoutingStatus.java 查看文件

@@ -1,8 +1,16 @@
1 1
 package se.citerus.dddsample.domain.model.cargo;
2 2
 
3
+import se.citerus.dddsample.domain.model.ValueObject;
4
+
3 5
 /**
4
- * 
6
+ * Routing status. 
5 7
  */
6
-public enum RoutingStatus {
7
-  NOT_ROUTED, ROUTED, MISROUTED
8
+public enum RoutingStatus implements ValueObject<RoutingStatus> {
9
+  NOT_ROUTED, ROUTED, MISROUTED;
10
+
11
+  @Override
12
+  public boolean sameValueAs(final RoutingStatus other) {
13
+    return this.equals(other);
14
+  }
15
+  
8 16
 }

+ 0
- 5
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/TrackingId.java 查看文件

@@ -49,11 +49,6 @@ public final class TrackingId implements ValueObject<TrackingId> {
49 49
   }
50 50
 
51 51
   @Override
52
-  public TrackingId copy() {
53
-    return new TrackingId(id);
54
-  }
55
-
56
-  @Override
57 52
   public String toString() {
58 53
     return id;
59 54
   }

+ 9
- 2
dddsample/src/main/java/se/citerus/dddsample/domain/model/cargo/TransportStatus.java 查看文件

@@ -1,8 +1,15 @@
1 1
 package se.citerus.dddsample.domain.model.cargo;
2 2
 
3
+import se.citerus.dddsample.domain.model.ValueObject;
4
+
3 5
 /**
4 6
  * Represents the different transport statuses for a cargo.
5 7
  */
6
-public enum TransportStatus {
7
-  NOT_RECEIVED, IN_PORT, ONBOARD_CARRIER, CLAIMED, UNKNOWN
8
+public enum TransportStatus implements ValueObject<TransportStatus> {
9
+  NOT_RECEIVED, IN_PORT, ONBOARD_CARRIER, CLAIMED, UNKNOWN;
10
+
11
+  @Override
12
+  public boolean sameValueAs(final TransportStatus other) {
13
+    return this.equals(other);
14
+  }
8 15
 }

+ 0
- 10
dddsample/src/main/java/se/citerus/dddsample/domain/model/carrier/CarrierMovement.java 查看文件

@@ -103,16 +103,6 @@ public final class CarrierMovement implements ValueObject<CarrierMovement> {
103 103
       isEquals();
104 104
   }
105 105
 
106
-  @Override
107
-  public CarrierMovement copy() {
108
-    return new CarrierMovement(
109
-      departureLocation(),
110
-      arrivalLocation(),
111
-      departureTime(),
112
-      arrivalTime()
113
-    );
114
-  }
115
-
116 106
   CarrierMovement() {
117 107
     // Needed by Hibernate
118 108
   }

+ 3
- 14
dddsample/src/main/java/se/citerus/dddsample/domain/model/carrier/Schedule.java 查看文件

@@ -4,7 +4,6 @@ import org.apache.commons.lang.Validate;
4 4
 import org.apache.commons.lang.builder.HashCodeBuilder;
5 5
 import se.citerus.dddsample.domain.model.ValueObject;
6 6
 
7
-import java.util.ArrayList;
8 7
 import java.util.Collections;
9 8
 import java.util.List;
10 9
 
@@ -18,7 +17,7 @@ public class Schedule implements ValueObject<Schedule> {
18 17
 
19 18
   public static final Schedule EMPTY = new Schedule();
20 19
 
21
-  Schedule(List<CarrierMovement> carrierMovements) {
20
+  Schedule(final List<CarrierMovement> carrierMovements) {
22 21
     Validate.notNull(carrierMovements);
23 22
     Validate.noNullElements(carrierMovements);
24 23
     Validate.notEmpty(carrierMovements);
@@ -34,22 +33,12 @@ public class Schedule implements ValueObject<Schedule> {
34 33
   }
35 34
 
36 35
   @Override
37
-  public boolean sameValueAs(Schedule other) {
36
+  public boolean sameValueAs(final Schedule other) {
38 37
     return other != null && this.carrierMovements.equals(other.carrierMovements);
39 38
   }
40 39
 
41 40
   @Override
42
-  public Schedule copy() {
43
-    final List<CarrierMovement> copyCarrierMovements = new ArrayList(carrierMovements.size());
44
-    for (CarrierMovement carrierMovement : carrierMovements) {
45
-      copyCarrierMovements.add(carrierMovement.copy());
46
-    }
47
-
48
-    return new Schedule(copyCarrierMovements);
49
-  }
50
-
51
-  @Override
52
-  public boolean equals(Object o) {
41
+  public boolean equals(final Object o) {
53 42
     if (this == o) return true;
54 43
     if (o == null || getClass() != o.getClass()) return false;
55 44
 

+ 0
- 5
dddsample/src/main/java/se/citerus/dddsample/domain/model/carrier/VoyageNumber.java 查看文件

@@ -39,11 +39,6 @@ public class VoyageNumber implements ValueObject<VoyageNumber> {
39 39
   }
40 40
 
41 41
   @Override
42
-  public VoyageNumber copy() {
43
-    return new VoyageNumber(number);
44
-  }
45
-
46
-  @Override
47 42
   public String toString() {
48 43
     return number;
49 44
   }

+ 23
- 13
dddsample/src/main/java/se/citerus/dddsample/domain/model/handling/HandlingEvent.java 查看文件

@@ -91,10 +91,6 @@ public final class HandlingEvent implements DomainEvent<HandlingEvent> {
91 91
       return other != null && this.equals(other);
92 92
     }
93 93
 
94
-    @Override
95
-    public Type copy() {
96
-      return this;
97
-    }
98 94
   }
99 95
 
100 96
   /**
@@ -105,16 +101,23 @@ public final class HandlingEvent implements DomainEvent<HandlingEvent> {
105 101
    * @param location         where the event took place
106 102
    * @param voyage           the voyage
107 103
    */
108
-  public HandlingEvent(Cargo cargo, Date completionTime, Date registrationTime, Type type,
109
-                       Location location, Voyage voyage) {
110
-    Validate.noNullElements(new Object[] {cargo, completionTime, registrationTime, type, location, voyage});
104
+  public HandlingEvent(final Cargo cargo,
105
+                       final Date completionTime,
106
+                       final Date registrationTime,
107
+                       final Type type,
108
+                       final Location location,
109
+                       final Voyage voyage) {
110
+    Validate.noNullElements(new Object[]
111
+      {cargo, completionTime, registrationTime, type, location, voyage}
112
+    );
113
+
111 114
     if (type.prohibitsVoyage()) {
112 115
       throw new IllegalArgumentException("Voyage is not allowed with event type " + type);
113 116
     }
114 117
 
115 118
     this.voyage = voyage;
116
-    this.completionTime = completionTime;
117
-    this.registrationTime = registrationTime;
119
+    this.completionTime = (Date) completionTime.clone();
120
+    this.registrationTime = (Date) registrationTime.clone();
118 121
     this.type = type;
119 122
     this.location = location;
120 123
     this.cargo = cargo;
@@ -127,14 +130,21 @@ public final class HandlingEvent implements DomainEvent<HandlingEvent> {
127 130
    * @param type             type of event
128 131
    * @param location         where the event took place
129 132
    */
130
-  public HandlingEvent(Cargo cargo, Date completionTime, Date registrationTime, Type type, Location location) {
131
-    Validate.noNullElements(new Object[] {cargo, completionTime, registrationTime, type, location});
133
+  public HandlingEvent(final Cargo cargo,
134
+                       final Date completionTime,
135
+                       final Date registrationTime,
136
+                       final Type type,
137
+                       final Location location) {
138
+    Validate.noNullElements(new Object[]
139
+      {cargo, completionTime, registrationTime, type, location}
140
+    );
141
+
132 142
     if (type.requiresVoyage()) {
133 143
       throw new IllegalArgumentException("Voyage is required for event type " + type);
134 144
     }
135 145
 
136
-    this.completionTime = completionTime;
137
-    this.registrationTime = registrationTime;
146
+    this.completionTime = (Date) completionTime.clone();
147
+    this.registrationTime = (Date) registrationTime.clone();
138 148
     this.type = type;
139 149
     this.location = location;
140 150
     this.cargo = cargo;

+ 0
- 5
dddsample/src/main/java/se/citerus/dddsample/domain/model/location/UnLocode.java 查看文件

@@ -61,11 +61,6 @@ public final class UnLocode implements ValueObject<UnLocode> {
61 61
   }
62 62
 
63 63
   @Override
64
-  public UnLocode copy() {
65
-    return new UnLocode(unlocode);
66
-  }
67
-
68
-  @Override
69 64
   public String toString() {
70 65
     return idString();
71 66
   }

+ 18
- 4
dddsample/src/main/resources/se/citerus/dddsample/infrastructure/persistence/hibernate/Cargo.hbm.xml 查看文件

@@ -13,15 +13,29 @@
13 13
 
14 14
     <many-to-one name="origin" column="origin_id" not-null="false" cascade="none" update="false" foreign-key="origin_fk"/>
15 15
 
16
+    <property name="misdirected" column="is_misdirected" not-null="true"/>
17
+
18
+    <property name="routingStatus" column="routing_status" not-null="true">
19
+      <type name="org.hibernate.type.EnumType">
20
+        <param name="enumClass">se.citerus.dddsample.domain.model.cargo.RoutingStatus</param>
21
+        <param name="type">12</param><!-- 12 is java.sql.Types.VARCHAR -->
22
+      </type>
23
+    </property>
24
+
16 25
     <component name="trackingId" unique="true" update="false">
17 26
       <property name="id" column="tracking_id"/>
18 27
     </component>
19 28
 
20 29
     <component name="delivery" lazy="true" update="false">
21
-      <set name="events" lazy="true" cascade="none">
22
-        <key column="cargo_id"/>
23
-        <one-to-many class="se.citerus.dddsample.domain.model.handling.HandlingEvent"/>
24
-      </set>
30
+      <property name="transportStatus" column="transport_status" not-null="true">
31
+        <type name="org.hibernate.type.EnumType">
32
+          <param name="enumClass">se.citerus.dddsample.domain.model.cargo.TransportStatus</param>
33
+          <param name="type">12</param><!-- 12 is java.sql.Types.VARCHAR -->
34
+        </type>
35
+      </property>
36
+      <many-to-one name="currentVoyage" column="current_voyage_id" not-null="false" cascade="none" foreign-key="current_voyage_fk"/>
37
+      <many-to-one name="lastKnownLocation" column="last_known_location_id" not-null="false" cascade="none" foreign-key="last_known_location_fk"/>
38
+      <many-to-one name="lastEvent" column="last_event_id" not-null="false" cascade="none" foreign-key="last_event_fk"/>
25 39
     </component>
26 40
 
27 41
     <component name="routeSpecification">