瀏覽代碼

Introduced a cargo tracking view adapter.

peter_backlund 18 年之前
父節點
當前提交
fe1d1cc858

+ 15
- 9
dddsample/src/main/java/se/citerus/dddsample/application/web/CargoTrackingController.java 查看文件

@@ -1,8 +1,10 @@
1 1
 package se.citerus.dddsample.application.web;
2 2
 
3
+import org.springframework.context.MessageSource;
3 4
 import org.springframework.validation.BindException;
4 5
 import org.springframework.web.servlet.ModelAndView;
5 6
 import org.springframework.web.servlet.mvc.SimpleFormController;
7
+import org.springframework.web.servlet.support.RequestContextUtils;
6 8
 import se.citerus.dddsample.application.web.command.TrackCommand;
7 9
 import se.citerus.dddsample.domain.model.cargo.Cargo;
8 10
 import se.citerus.dddsample.domain.model.cargo.TrackingId;
@@ -11,6 +13,7 @@ import se.citerus.dddsample.domain.service.TrackingService;
11 13
 import javax.servlet.http.HttpServletRequest;
12 14
 import javax.servlet.http.HttpServletResponse;
13 15
 import java.util.HashMap;
16
+import java.util.Locale;
14 17
 import java.util.Map;
15 18
 
16 19
 /**
@@ -18,14 +21,15 @@ import java.util.Map;
18 21
  * domain layer, unlike the booking interface which has a a remote facade and supporting
19 22
  * DTOs in between.
20 23
  * <p/>
21
- * This approach represents the least amount of transfer object overhead, but is
22
- * also somewhat awkward when working with domain model classes in the view layer,
23
- * since those classes do not follow the JavaBean conventions for example.
24
+ * An adapter class, designed for the tracking use case, is used to wrap the domain model
25
+ * to make it easier to work with in a web page rendering context. We do not want to apply
26
+ * view rendering constraints to the design of our domain model, and the adapter
27
+ * helps us shield the domain model classes. 
24 28
  * <p/>
25
- * Note that DDD strongly urges you to keep your domain model free from user interface
26
- * interference and demands, so this approach should be used with caution.
27 29
  *
30
+ * @eee se.citerus.dddsample.application.web.CargoTrackingViewAdapter
28 31
  * @see se.citerus.dddsample.application.web.CargoAdminController
32
+ *
29 33
  */
30 34
 public final class CargoTrackingController extends SimpleFormController {
31 35
 
@@ -40,12 +44,14 @@ public final class CargoTrackingController extends SimpleFormController {
40 44
                                   final Object command, final BindException errors) throws Exception {
41 45
 
42 46
     final TrackCommand trackCommand = (TrackCommand) command;
43
-    final String tidStr = trackCommand.getTrackingId();
44
-    final Cargo cargo = trackingService.track(new TrackingId(tidStr));
47
+    final String trackingIdString = trackCommand.getTrackingId();
48
+    final Cargo cargo = trackingService.track(new TrackingId(trackingIdString));
45 49
 
46
-    final Map<String, Cargo> model = new HashMap<String, Cargo>();
50
+    final Map<String, CargoTrackingViewAdapter> model = new HashMap();
47 51
     if (cargo != null) {
48
-      model.put("cargo", cargo);
52
+      final MessageSource messageSource = getApplicationContext();
53
+      final Locale locale = RequestContextUtils.getLocale(request);
54
+      model.put("cargo", new CargoTrackingViewAdapter(cargo, messageSource, locale));
49 55
     } else {
50 56
       errors.rejectValue("trackingId", "cargo.unknown_id", new Object[]{trackCommand.getTrackingId()},
51 57
         "Unknown tracking id");

+ 168
- 0
dddsample/src/main/java/se/citerus/dddsample/application/web/CargoTrackingViewAdapter.java 查看文件

@@ -0,0 +1,168 @@
1
+package se.citerus.dddsample.application.web;
2
+
3
+import org.springframework.context.MessageSource;
4
+import se.citerus.dddsample.domain.model.cargo.Cargo;
5
+import se.citerus.dddsample.domain.model.cargo.DeliveryHistory;
6
+import se.citerus.dddsample.domain.model.carrier.CarrierMovement;
7
+import se.citerus.dddsample.domain.model.handling.HandlingEvent;
8
+import se.citerus.dddsample.domain.model.location.Location;
9
+
10
+import java.text.SimpleDateFormat;
11
+import java.util.ArrayList;
12
+import java.util.Collections;
13
+import java.util.List;
14
+import java.util.Locale;
15
+
16
+/**
17
+ * View adapter for displaying a cargo in a tracking context.
18
+ */
19
+public final class CargoTrackingViewAdapter {
20
+
21
+  private final Cargo cargo;
22
+  private final MessageSource messageSource;
23
+  private final Locale locale;
24
+  private final List<HandlingEventViewAdapter> events;
25
+
26
+  /**
27
+   * Constructor.
28
+   *
29
+   * @param cargo
30
+   * @param messageSource
31
+   * @param locale
32
+   */
33
+  public CargoTrackingViewAdapter(Cargo cargo, MessageSource messageSource, Locale locale) {
34
+    this.messageSource = messageSource;
35
+    this.locale = locale;
36
+    this.cargo = cargo;
37
+
38
+    final List<HandlingEvent> handlingEvents = cargo.deliveryHistory().eventsOrderedByCompletionTime();
39
+    this.events = new ArrayList<HandlingEventViewAdapter>(handlingEvents.size());
40
+    for (HandlingEvent handlingEvent : handlingEvents) {
41
+      events.add(new HandlingEventViewAdapter(handlingEvent));
42
+    }
43
+  }
44
+
45
+  /**
46
+   * @param location a location
47
+   * @return A formatted string for displaying the location.
48
+   */
49
+  private String getDisplayText(Location location) {
50
+    return location.unLocode().idString() + " (" + location.name() + ")";
51
+  }
52
+
53
+  /**
54
+   * @return An unmodifiable list of handling event view adapters.
55
+   */
56
+  public List<HandlingEventViewAdapter> getEvents() {
57
+    return Collections.unmodifiableList(events);
58
+  }
59
+
60
+  /**
61
+   * @return A translated string describing the cargo status. 
62
+   */
63
+  public String getStatusText() {
64
+    final DeliveryHistory deliveryHistory = cargo.deliveryHistory();
65
+    final String code = "cargo.status." + deliveryHistory.status().name();
66
+
67
+    final Object[] args;
68
+    switch (deliveryHistory.status()) {
69
+      case IN_PORT:
70
+        args = new Object[] {getDisplayText(deliveryHistory.currentLocation())};
71
+        break;
72
+      case ONBOARD_CARRIER:
73
+        args = new Object[] {deliveryHistory.currentCarrierMovement().carrierMovementId().idString()};
74
+        break;
75
+      case CLAIMED:
76
+      case NOT_RECEIVED:
77
+      case UNKNOWN:
78
+      default:
79
+        args = null;
80
+        break;
81
+    }
82
+    
83
+    return messageSource.getMessage(code, args, "[Unknown status]", locale);
84
+  }
85
+
86
+  /**
87
+   * @return Cargo destination location.
88
+   */
89
+  public String getDestination() {
90
+    return getDisplayText(cargo.destination());
91
+  }
92
+
93
+  /**
94
+   * @return Cargo osigin location.
95
+   */
96
+  public String getOrigin() {
97
+    return getDisplayText(cargo.origin());
98
+  }
99
+
100
+  /**
101
+   * @return Cargo tracking id.
102
+   */
103
+  public String getTrackingId() {
104
+    return cargo.trackingId().idString();
105
+  }
106
+
107
+  /**
108
+   * @return True if cargo is misdirected.
109
+   */
110
+  public boolean isMisdirected() {
111
+    return cargo.isMisdirected();
112
+  }
113
+
114
+  /**
115
+   * Handling event view adapter component.
116
+   */
117
+  public final class HandlingEventViewAdapter {
118
+
119
+    private final HandlingEvent handlingEvent;
120
+    private final String FORMAT = "yyyy-MM-dd hh:mm";
121
+
122
+    /**
123
+     * Constructor.
124
+     *
125
+     * @param handlingEvent handling event
126
+     */
127
+    public HandlingEventViewAdapter(HandlingEvent handlingEvent) {
128
+      this.handlingEvent = handlingEvent;
129
+    }
130
+
131
+    /**
132
+     * @return Location where the event occurred.
133
+     */
134
+    public String getLocation() {
135
+      return handlingEvent.location().unLocode().idString();
136
+    }
137
+
138
+    /**
139
+     * @return Time when the event was completed.
140
+     */
141
+    public String getTime() {
142
+      return new SimpleDateFormat(FORMAT).format(handlingEvent.completionTime());
143
+    }
144
+
145
+    /**
146
+     * @return Type of event.
147
+     */
148
+    public String getType() {
149
+      return handlingEvent.type().toString();
150
+    }
151
+
152
+    /**
153
+     * @return Carrier movement id, or empty string if not applicable.
154
+     */
155
+    public String getCarrierMovement() {
156
+      final CarrierMovement cm = handlingEvent.carrierMovement();
157
+      return cm != null ? cm.carrierMovementId().toString() : "";
158
+    }
159
+
160
+    /**
161
+     * @return True if the event was expected, according to the cargo's itinerary.
162
+     */
163
+    public boolean isExpected() {
164
+      return cargo.itinerary().isExpected(handlingEvent);
165
+    }
166
+
167
+  }
168
+}

+ 2
- 2
dddsample/src/main/resources/messages_en.properties 查看文件

@@ -1,5 +1,5 @@
1 1
 cargo.status.NOT_RECEIVED=Not received
2
-cargo.status.IN_PORT=In port
3
-cargo.status.ONBOARD_CARRIER=Onboard carrier
2
+cargo.status.IN_PORT=In port {0}
3
+cargo.status.ONBOARD_CARRIER=Onboard carrier {0}
4 4
 cargo.status.CLAIMED=Claimed
5 5
 cargo.status.UNKNOWN=Unknown

+ 32
- 41
dddsample/src/main/webapp/WEB-INF/jsp/cargo/track.jsp 查看文件

@@ -1,9 +1,6 @@
1
-<%@ page import="se.citerus.dddsample.domain.model.cargo.Cargo" %>
2
-<%@ page import="se.citerus.dddsample.domain.model.cargo.DeliveryHistory" %>
3
-<%@ page import="se.citerus.dddsample.domain.model.handling.HandlingEvent" %>
4 1
 <html>
5 2
 <head>
6
-  <title>Cargo search</title>
3
+  <title>Tracking cargo</title>
7 4
 </head>
8 5
 <body>
9 6
 <div id="container">
@@ -33,47 +30,41 @@
33 30
   </form:form>
34 31
   </div>
35 32
 
36
-  <% final Cargo cargo = (Cargo) request.getAttribute("cargo"); %>
37
-
38
-  <% if (cargo != null) { %>
39
-    <% final DeliveryHistory dh = cargo.deliveryHistory(); %>
33
+  <c:if test="${cargo != null}">
40 34
     <div id="result">
41
-    <h2>
42
-      <c:set var="statusMessageCode"><%="cargo.status." + dh.status()%></c:set>
43
-      Status: <spring:message code="${statusMessageCode}"/>
44
-      &nbsp;
45
-      <%= dh.currentLocation() != null ?
46
-          dh.currentLocation().name() : "" %>
47
-      &nbsp;
48
-      <%= dh.currentCarrierMovement() != null ?
49
-          dh.currentCarrierMovement().carrierMovementId().idString() : "" %>
50
-    </h2>
51
-    <% if (cargo.isMisdirected()) { %>
35
+    <h2>Status: ${cargo.statusText}</h2>
36
+    <c:if test="${cargo.misdirected}">
52 37
       <p class="notify"><img src="${rc.contextPath}/images/error.png" alt="" />Cargo is misdirected</p>
53
-    <% } %>
54
-    <h3>Delivery History</h3>
55
-    <table cellspacing="4">
56
-      <thead>
57
-        <tr>
58
-          <td>Event</td>
59
-          <td>Location</td>
60
-          <td>Time</td>
61
-          <td></td>
62
-        </tr>
63
-      </thead>
64
-      <tbody>
65
-        <% for (HandlingEvent event : dh.eventsOrderedByCompletionTime()) { %>
66
-          <tr class="event-type-<%=event.type()%>">
67
-            <td><%=event.type()%></td>
68
-            <td><%=event.location().name()%></td>
69
-            <td><%=event.completionTime()%></td>
70
-            <td><img src="${rc.contextPath}/images/<%=cargo.itinerary().isExpected(event) ? "tick" : "cross"%>.png" alt=""/></td>
38
+    </c:if>
39
+    <c:if test="${not empty cargo.events}">
40
+      <h3>Delivery History</h3>
41
+      <table cellspacing="4">
42
+        <thead>
43
+          <tr>
44
+            <td>Event</td>
45
+            <td>Location</td>
46
+            <td>Time</td>
47
+            <td>Carrier Movement</td>
48
+            <td></td>
71 49
           </tr>
72
-        <% } %>
73
-      </tbody>
74
-    </table>
50
+        </thead>
51
+        <tbody>
52
+          <c:forEach items="${cargo.events}" var="event">
53
+            <tr class="event-type-${event.type}">
54
+              <td>${event.type}</td>
55
+              <td>${event.location}</td>
56
+              <td>${event.time}</td>
57
+              <td>${event.carrierMovement}</td>
58
+              <td>
59
+                <img src="${rc.contextPath}/images/${event.expected ? "tick" : "cross"}.png" alt=""/>
60
+              </td>
61
+            </tr>
62
+          </c:forEach>
63
+        </tbody>
64
+      </table>
65
+    </c:if>
75 66
   </div>
76
-  <% } %>
67
+  </c:if>
77 68
 
78 69
 </div>
79 70
 <script type="text/javascript" charset="UTF-8">

+ 5
- 2
dddsample/src/test/java/se/citerus/dddsample/application/web/CargoTrackingControllerTest.java 查看文件

@@ -1,6 +1,7 @@
1 1
 package se.citerus.dddsample.application.web;
2 2
 
3 3
 import junit.framework.TestCase;
4
+import org.springframework.context.support.StaticApplicationContext;
4 5
 import org.springframework.mock.web.MockHttpServletRequest;
5 6
 import org.springframework.mock.web.MockHttpServletResponse;
6 7
 import org.springframework.mock.web.MockHttpSession;
@@ -36,6 +37,8 @@ public class CargoTrackingControllerTest extends TestCase {
36 37
     request.setSession(session);
37 38
 
38 39
     controller = new CargoTrackingController();
40
+    StaticApplicationContext applicationContext = new StaticApplicationContext();
41
+    controller.setApplicationContext(applicationContext);
39 42
     controller.setFormView("test-form");
40 43
     controller.setSuccessView("test-success");
41 44
     controller.setCommandName("test-command-name");
@@ -79,8 +82,8 @@ public class CargoTrackingControllerTest extends TestCase {
79 82
     assertEquals("test-form", mav.getViewName());
80 83
     // Errors, command are two standard map attributes, the third should be the cargo object
81 84
     assertEquals(3, mav.getModel().size());
82
-    Cargo cargo = (Cargo) mav.getModel().get("cargo");
83
-    assertEquals(HONGKONG, cargo.deliveryHistory().currentLocation());
85
+    CargoTrackingViewAdapter cargo = (CargoTrackingViewAdapter) mav.getModel().get("cargo");
86
+    assertEquals("JKL456", cargo.getTrackingId());
84 87
   }
85 88
 
86 89
   public void testUnknownCargo() throws Exception {

+ 64
- 0
dddsample/src/test/java/se/citerus/dddsample/application/web/CargoTrackingViewAdapterTest.java 查看文件

@@ -0,0 +1,64 @@
1
+package se.citerus.dddsample.application.web;
2
+
3
+import junit.framework.TestCase;
4
+import org.springframework.context.support.StaticApplicationContext;
5
+import se.citerus.dddsample.domain.model.cargo.Cargo;
6
+import se.citerus.dddsample.domain.model.cargo.CargoTestHelper;
7
+import se.citerus.dddsample.domain.model.cargo.TrackingId;
8
+import se.citerus.dddsample.domain.model.carrier.CarrierMovement;
9
+import se.citerus.dddsample.domain.model.carrier.CarrierMovementId;
10
+import se.citerus.dddsample.domain.model.handling.HandlingEvent;
11
+import static se.citerus.dddsample.domain.model.location.SampleLocations.*;
12
+
13
+import java.util.*;
14
+
15
+public class CargoTrackingViewAdapterTest extends TestCase {
16
+
17
+  public void testCreate() {
18
+    Cargo cargo = new Cargo(new TrackingId("XYZ"), HANGZOU, HELSINKI);
19
+
20
+    List<HandlingEvent> events = new ArrayList<HandlingEvent>();
21
+    events.add(new HandlingEvent(cargo, new Date(1), new Date(2), HandlingEvent.Type.RECEIVE, HANGZOU, null));
22
+
23
+    CarrierMovement cm001 = new CarrierMovement(new CarrierMovementId("CM001"), HANGZOU, GOTHENBURG);
24
+    events.add(new HandlingEvent(cargo, new Date(3), new Date(4), HandlingEvent.Type.LOAD, HANGZOU, cm001));
25
+    events.add(new HandlingEvent(cargo, new Date(5), new Date(6), HandlingEvent.Type.UNLOAD, HELSINKI, cm001));
26
+
27
+    CargoTestHelper.setDeliveryHistory(cargo, events);
28
+
29
+    StaticApplicationContext applicationContext = new StaticApplicationContext();
30
+    applicationContext.addMessage("cargo.status.IN_PORT", Locale.GERMAN, "In port {0}");
31
+    applicationContext.refresh();
32
+
33
+    CargoTrackingViewAdapter adapter = new CargoTrackingViewAdapter(cargo, applicationContext, Locale.GERMAN);
34
+
35
+    assertEquals("XYZ", adapter.getTrackingId());
36
+    assertEquals("CNHGH (Hangzhou)", adapter.getOrigin());
37
+    assertEquals("FIHEL (Helsinki)", adapter.getDestination());
38
+    assertEquals("In port FIHEL (Helsinki)", adapter.getStatusText());
39
+
40
+    Iterator<CargoTrackingViewAdapter.HandlingEventViewAdapter> it = adapter.getEvents().iterator();
41
+
42
+    CargoTrackingViewAdapter.HandlingEventViewAdapter event = it.next();
43
+    assertEquals("RECEIVE", event.getType());
44
+    assertEquals("CNHGH", event.getLocation());
45
+    assertEquals("1970-01-01 01:00", event.getTime());
46
+    assertEquals("", event.getCarrierMovement());
47
+    assertTrue(event.isExpected());
48
+
49
+    event = it.next();
50
+    assertEquals("LOAD", event.getType());
51
+    assertEquals("CNHGH", event.getLocation());
52
+    assertEquals("1970-01-01 01:00", event.getTime());
53
+    assertEquals("CM001", event.getCarrierMovement());
54
+    assertTrue(event.isExpected());
55
+
56
+    event = it.next();
57
+    assertEquals("UNLOAD", event.getType());
58
+    assertEquals("FIHEL", event.getLocation());
59
+    assertEquals("1970-01-01 01:00", event.getTime());
60
+    assertEquals("CM001", event.getCarrierMovement());
61
+    assertTrue(event.isExpected());
62
+  }
63
+
64
+}