Skip to content

Commit fe1d1cc

Browse files
committed
Introduced a cargo tracking view adapter.
1 parent df1f13a commit fe1d1cc

File tree

6 files changed

+286
-54
lines changed

6 files changed

+286
-54
lines changed

dddsample/src/main/java/se/citerus/dddsample/application/web/CargoTrackingController.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package se.citerus.dddsample.application.web;
22

3+
import org.springframework.context.MessageSource;
34
import org.springframework.validation.BindException;
45
import org.springframework.web.servlet.ModelAndView;
56
import org.springframework.web.servlet.mvc.SimpleFormController;
7+
import org.springframework.web.servlet.support.RequestContextUtils;
68
import se.citerus.dddsample.application.web.command.TrackCommand;
79
import se.citerus.dddsample.domain.model.cargo.Cargo;
810
import se.citerus.dddsample.domain.model.cargo.TrackingId;
@@ -11,21 +13,23 @@
1113
import javax.servlet.http.HttpServletRequest;
1214
import javax.servlet.http.HttpServletResponse;
1315
import java.util.HashMap;
16+
import java.util.Locale;
1417
import java.util.Map;
1518

1619
/**
1720
* Controller for tracking cargo. This interface sits immediately on top of the
1821
* domain layer, unlike the booking interface which has a a remote facade and supporting
1922
* DTOs in between.
2023
* <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.
2428
* <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.
2729
*
30+
* @eee se.citerus.dddsample.application.web.CargoTrackingViewAdapter
2831
* @see se.citerus.dddsample.application.web.CargoAdminController
32+
*
2933
*/
3034
public final class CargoTrackingController extends SimpleFormController {
3135

@@ -40,12 +44,14 @@ protected ModelAndView onSubmit(final HttpServletRequest request, final HttpServ
4044
final Object command, final BindException errors) throws Exception {
4145

4246
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));
4549

46-
final Map<String, Cargo> model = new HashMap<String, Cargo>();
50+
final Map<String, CargoTrackingViewAdapter> model = new HashMap();
4751
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));
4955
} else {
5056
errors.rejectValue("trackingId", "cargo.unknown_id", new Object[]{trackCommand.getTrackingId()},
5157
"Unknown tracking id");
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
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}
44
cargo.status.CLAIMED=Claimed
55
cargo.status.UNKNOWN=Unknown

dddsample/src/main/webapp/WEB-INF/jsp/cargo/track.jsp

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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" %>
41
<html>
52
<head>
6-
<title>Cargo search</title>
3+
<title>Tracking cargo</title>
74
</head>
85
<body>
96
<div id="container">
@@ -33,47 +30,41 @@
3330
</form:form>
3431
</div>
3532

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}">
4034
<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}">
5237
<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>
7149
</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>
7566
</div>
76-
<% } %>
67+
</c:if>
7768

7869
</div>
7970
<script type="text/javascript" charset="UTF-8">

dddsample/src/test/java/se/citerus/dddsample/application/web/CargoTrackingControllerTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package se.citerus.dddsample.application.web;
22

33
import junit.framework.TestCase;
4+
import org.springframework.context.support.StaticApplicationContext;
45
import org.springframework.mock.web.MockHttpServletRequest;
56
import org.springframework.mock.web.MockHttpServletResponse;
67
import org.springframework.mock.web.MockHttpSession;
@@ -36,6 +37,8 @@ protected void setUp() throws Exception {
3637
request.setSession(session);
3738

3839
controller = new CargoTrackingController();
40+
StaticApplicationContext applicationContext = new StaticApplicationContext();
41+
controller.setApplicationContext(applicationContext);
3942
controller.setFormView("test-form");
4043
controller.setSuccessView("test-success");
4144
controller.setCommandName("test-command-name");
@@ -79,8 +82,8 @@ public void testHandlePost() throws Exception {
7982
assertEquals("test-form", mav.getViewName());
8083
// Errors, command are two standard map attributes, the third should be the cargo object
8184
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());
8487
}
8588

8689
public void testUnknownCargo() throws Exception {

0 commit comments

Comments
 (0)