Browse Source

Added CSV file upload directory scanning and parsing for registering handling events, and introduced the HandlingReport concept for the web service to receive. It's all a bit sketchy atm.

peter_backlund 18 years ago
parent
commit
ba71015792

+ 0
- 25
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/HandlingEventServiceEndpoint.java View File

@@ -1,25 +0,0 @@
1
-package se.citerus.dddsample.interfaces.handling;
2
-
3
-import javax.jws.WebService;
4
-
5
-/**
6
- * Web service endpoint for handling event registration.
7
- */
8
-@WebService
9
-public interface HandlingEventServiceEndpoint {
10
-
11
-  /**
12
-   * Register an cargo handling event.
13
-   *
14
-   * @param completionTime    time when event occured, for example a the loading of cargo was completed
15
-   * @param trackingId        tracking id of the cargo
16
-   * @param carrierMovementId carrier movement id, if applicable
17
-   * @param unlocode          United Nations Location Code for the location where the event occured
18
-   * @param eventType         type of event
19
-   */
20
-  void register(String completionTime, String trackingId, String carrierMovementId, String unlocode, String eventType) throws RegistrationFailure;
21
-
22
-  // TODO structured class that holds these fields, and/or a batching method that accepts a list of those
23
-  // TODO contract-first instead of code-first (?)
24
-  
25
-}

+ 1
- 1
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/RegistrationFailure.java View File

@@ -6,7 +6,7 @@ import java.util.List;
6 6
 public class RegistrationFailure extends Exception {
7 7
   private final String[] errors;
8 8
 
9
-  RegistrationFailure(final List<String> errors) {
9
+  public RegistrationFailure(final List<String> errors) {
10 10
     this.errors = errors.toArray(new String[errors.size()]);
11 11
   }
12 12
 

dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/HandlingEventServiceEndpointImpl.java → dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/RegistrationParser.java View File

@@ -1,108 +1,99 @@
1
-package se.citerus.dddsample.interfaces.handling;
2
-
3
-import org.apache.commons.lang.StringUtils;
4
-import org.apache.commons.logging.Log;
5
-import org.apache.commons.logging.LogFactory;
6
-import se.citerus.dddsample.application.HandlingEventRegistrationAttempt;
7
-import se.citerus.dddsample.application.SystemEvents;
8
-import se.citerus.dddsample.domain.model.cargo.TrackingId;
9
-import se.citerus.dddsample.domain.model.carrier.VoyageNumber;
10
-import se.citerus.dddsample.domain.model.handling.HandlingEvent;
11
-import se.citerus.dddsample.domain.model.location.UnLocode;
12
-
13
-import javax.jws.WebService;
14
-import java.text.ParseException;
15
-import java.text.SimpleDateFormat;
16
-import java.util.ArrayList;
17
-import java.util.Arrays;
18
-import java.util.Date;
19
-import java.util.List;
20
-
21
-/**
22
- * This web service endpoint implementation performs basic validation and parsing
23
- * of incoming data, and in case of a valid registration attempt, sends an asynchronous message
24
- * with the informtion to the handling event registration system for proper registration.
25
- *  
26
- */
27
-@WebService(endpointInterface = "se.citerus.dddsample.interfaces.handling.HandlingEventServiceEndpoint")
28
-public class HandlingEventServiceEndpointImpl implements HandlingEventServiceEndpoint {
29
-
30
-  private SystemEvents systemEvents;
31
-  private static final Log logger = LogFactory.getLog(HandlingEventServiceEndpointImpl.class);
32
-  
33
-  public static final String ISO_8601_FORMAT = "yyyy-mm-dd HH:MM:SS.SSS";
34
-
35
-  public void register(final String completionTime, final String trackingId, final String voyageNumberString,
36
-                       final String unlocode, final String eventType) throws RegistrationFailure {
37
-    final List<String> errors = new ArrayList<String>();
38
-
39
-    final Date date = parseDate(completionTime, errors);
40
-    final TrackingId tid = parseTrackingId(trackingId, errors);
41
-    final VoyageNumber voyageNumber = parseVoyageNumber(voyageNumberString, errors);
42
-    final HandlingEvent.Type type = parseEventType(eventType, errors);
43
-    final UnLocode ul = parseUnLocode(unlocode, errors);
44
-
45
-    if (errors.isEmpty()) {
46
-      final HandlingEventRegistrationAttempt attempt = new HandlingEventRegistrationAttempt(new Date(), date, tid, voyageNumber, type, ul);
47
-      systemEvents.receivedHandlingEventRegistrationAttempt(attempt);
48
-    } else {
49
-      logger.warn("Handling event registration attempt failed: " + errors);
50
-      throw new RegistrationFailure(errors);
51
-    }
52
-  }
53
-
54
-  private UnLocode parseUnLocode(final String unlocode, final List<String> errors) {
55
-    try {
56
-      return new UnLocode(unlocode);
57
-    } catch (IllegalArgumentException e) {
58
-      errors.add(e.getMessage());
59
-      return null;
60
-    }
61
-  }
62
-
63
-  private TrackingId parseTrackingId(final String trackingId, final List<String> errors) {
64
-    try {
65
-      return new TrackingId(trackingId);
66
-    } catch (IllegalArgumentException e) {
67
-      errors.add(e.getMessage());
68
-      return null;
69
-    }
70
-  }
71
-
72
-  private VoyageNumber parseVoyageNumber(final String voyageNumber, final List<String> errors) {
73
-    if (StringUtils.isNotEmpty(voyageNumber)) {
74
-      try {
75
-        return new VoyageNumber(voyageNumber);
76
-      } catch (IllegalArgumentException e) {
77
-        errors.add(e.getMessage());
78
-        return null;
79
-      }
80
-    } else {
81
-      return null;
82
-    }
83
-  }
84
-
85
-  private Date parseDate(final String completionTime, final List<String> errors) {
86
-    Date date;
87
-    try {
88
-      date = new SimpleDateFormat(ISO_8601_FORMAT).parse(completionTime);
89
-    } catch (ParseException e) {
90
-      errors.add("Invalid date format: " + completionTime + ", must be on ISO 8601 format: " + ISO_8601_FORMAT);
91
-      date = null;
92
-    }
93
-    return date;
94
-  }
95
-
96
-  private HandlingEvent.Type parseEventType(final String eventType, final List<String> errors) {
97
-    try {
98
-      return HandlingEvent.Type.valueOf(eventType);
99
-    } catch (IllegalArgumentException e) {
100
-      errors.add(eventType + " is not a valid handling event type. Valid types are: " + Arrays.toString(HandlingEvent.Type.values()));
101
-      return null;      
102
-    }
103
-  }
104
-
105
-  public void setSystemEvents(SystemEvents systemEvents) {
106
-    this.systemEvents = systemEvents;
107
-  }
108
-}
1
+package se.citerus.dddsample.interfaces.handling;
2
+
3
+import org.apache.commons.lang.StringUtils;
4
+import org.apache.commons.logging.Log;
5
+import org.apache.commons.logging.LogFactory;
6
+import se.citerus.dddsample.application.HandlingEventRegistrationAttempt;
7
+import se.citerus.dddsample.application.SystemEvents;
8
+import se.citerus.dddsample.domain.model.cargo.TrackingId;
9
+import se.citerus.dddsample.domain.model.carrier.VoyageNumber;
10
+import se.citerus.dddsample.domain.model.handling.HandlingEvent;
11
+import se.citerus.dddsample.domain.model.location.UnLocode;
12
+import se.citerus.dddsample.interfaces.handling.ws.HandlingReportServiceImpl;
13
+
14
+import java.text.ParseException;
15
+import java.text.SimpleDateFormat;
16
+import java.util.ArrayList;
17
+import java.util.Arrays;
18
+import java.util.Date;
19
+import java.util.List;
20
+
21
+public class RegistrationParser {
22
+
23
+  private SystemEvents systemEvents;
24
+  
25
+  private static final Log logger = LogFactory.getLog(RegistrationParser.class);
26
+
27
+  public void convertAndSend(String completionTime, String trackingId, String voyageNumberString, String unlocode, String eventType) throws RegistrationFailure {
28
+    final List<String> errors = new ArrayList<String>();
29
+
30
+    final Date date = parseDate(completionTime, errors);
31
+    final TrackingId tid = parseTrackingId(trackingId, errors);
32
+    final VoyageNumber voyageNumber = parseVoyageNumber(voyageNumberString, errors);
33
+    final HandlingEvent.Type type = parseEventType(eventType, errors);
34
+    final UnLocode ul = parseUnLocode(unlocode, errors);
35
+
36
+    if (errors.isEmpty()) {
37
+      final HandlingEventRegistrationAttempt attempt = new HandlingEventRegistrationAttempt(new Date(), date, tid, voyageNumber, type, ul);
38
+      systemEvents.receivedHandlingEventRegistrationAttempt(attempt);
39
+    } else {
40
+      logger.warn("Handling event registration attempt failed: " + errors);
41
+      throw new RegistrationFailure(errors);
42
+    }
43
+  }
44
+
45
+  private UnLocode parseUnLocode(final String unlocode, final List<String> errors) {
46
+    try {
47
+      return new UnLocode(unlocode);
48
+    } catch (IllegalArgumentException e) {
49
+      errors.add(e.getMessage());
50
+      return null;
51
+    }
52
+  }
53
+
54
+  private TrackingId parseTrackingId(final String trackingId, final List<String> errors) {
55
+    try {
56
+      return new TrackingId(trackingId);
57
+    } catch (IllegalArgumentException e) {
58
+      errors.add(e.getMessage());
59
+      return null;
60
+    }
61
+  }
62
+
63
+  private VoyageNumber parseVoyageNumber(final String voyageNumber, final List<String> errors) {
64
+    if (StringUtils.isNotEmpty(voyageNumber)) {
65
+      try {
66
+        return new VoyageNumber(voyageNumber);
67
+      } catch (IllegalArgumentException e) {
68
+        errors.add(e.getMessage());
69
+        return null;
70
+      }
71
+    } else {
72
+      return null;
73
+    }
74
+  }
75
+
76
+  private Date parseDate(final String completionTime, final List<String> errors) {
77
+    Date date;
78
+    try {
79
+      date = new SimpleDateFormat(HandlingReportServiceImpl.ISO_8601_FORMAT).parse(completionTime);
80
+    } catch (ParseException e) {
81
+      errors.add("Invalid date format: " + completionTime + ", must be on ISO 8601 format: " + HandlingReportServiceImpl.ISO_8601_FORMAT);
82
+      date = null;
83
+    }
84
+    return date;
85
+  }
86
+
87
+  private HandlingEvent.Type parseEventType(final String eventType, final List<String> errors) {
88
+    try {
89
+      return HandlingEvent.Type.valueOf(eventType);
90
+    } catch (IllegalArgumentException e) {
91
+      errors.add(eventType + " is not a valid handling event type. Valid types are: " + Arrays.toString(HandlingEvent.Type.values()));
92
+      return null;
93
+    }
94
+  }
95
+
96
+  public void setSystemEvents(SystemEvents systemEvents) {
97
+    this.systemEvents = systemEvents;
98
+  }
99
+}

+ 17
- 0
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/file/PartialRegistrationFailureException.java View File

@@ -0,0 +1,17 @@
1
+package se.citerus.dddsample.interfaces.handling.file;
2
+
3
+import java.util.List;
4
+
5
+public class PartialRegistrationFailureException extends Exception {
6
+
7
+  private final List<String> rejectedLines;
8
+
9
+  public PartialRegistrationFailureException(List<String> rejectedLines) {
10
+    this.rejectedLines = rejectedLines;
11
+  }
12
+
13
+  public List<String> getRejectedLines() {
14
+    return rejectedLines;
15
+  }
16
+  
17
+}

+ 125
- 0
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/file/UploadDirectoryScanner.java View File

@@ -0,0 +1,125 @@
1
+package se.citerus.dddsample.interfaces.handling.file;
2
+
3
+import org.apache.commons.io.FileUtils;
4
+import org.apache.commons.logging.Log;
5
+import org.apache.commons.logging.LogFactory;
6
+import org.springframework.beans.factory.InitializingBean;
7
+import se.citerus.dddsample.interfaces.handling.RegistrationFailure;
8
+import se.citerus.dddsample.interfaces.handling.RegistrationParser;
9
+
10
+import java.io.File;
11
+import java.io.IOException;
12
+import java.util.ArrayList;
13
+import java.util.List;
14
+import java.util.TimerTask;
15
+
16
+/**
17
+ * Periodically scans a certain directory for files and attempts
18
+ * to parse handling event registrations from the contents.
19
+ *
20
+ * Files that fail to parse are moved into a separate directory,
21
+ * succesful files are deleted.
22
+ */
23
+public class UploadDirectoryScanner extends TimerTask implements InitializingBean {
24
+
25
+  private File uploadDirectory;
26
+  private File parseFailureDirectory;
27
+  private RegistrationParser registrationParser;
28
+
29
+  private final static Log logger = LogFactory.getLog(UploadDirectoryScanner.class);
30
+
31
+  @Override
32
+  public void run() {
33
+    for (File file : uploadDirectory.listFiles()) {
34
+      try {
35
+        parse(file);
36
+        delete(file);
37
+        logger.info("Import of " + file.getName() + " complete");
38
+      } catch (Exception e) {
39
+        logger.error(e, e);
40
+        move(file);
41
+      }
42
+    }
43
+  }
44
+
45
+  private void parse(final File file) throws IOException, RegistrationFailure {
46
+    final List<String> lines = FileUtils.readLines(file);
47
+    final List<String> rejectedLines = new ArrayList<String>();
48
+    for (String line : lines) {
49
+      try {
50
+        parseLine(line);
51
+      } catch (Exception e) {
52
+        logger.error("Rejected line '" + line + "'. Reason is: " + e, e);
53
+        rejectedLines.add(line);
54
+      }
55
+    }
56
+    if (!rejectedLines.isEmpty()) {
57
+      writeRejectedLinesToFile(toRejectedFilename(file), rejectedLines);
58
+    }
59
+  }
60
+
61
+  private String toRejectedFilename(File file) {
62
+    return file.getName() + ".reject";
63
+  }
64
+
65
+  private void writeRejectedLinesToFile(String filename, List<String> rejectedLines) throws IOException {
66
+    FileUtils.writeLines(
67
+        new File(parseFailureDirectory, filename), rejectedLines
68
+    );
69
+  }
70
+
71
+  private void parseLine(final String line) throws RegistrationFailure {
72
+    final String[] columns = line.split("\t");
73
+    if (columns.length == 5) {
74
+      registrationParser.convertAndSend(
75
+        columns[0],
76
+        columns[1],
77
+        columns[2],
78
+        columns[3],
79
+        columns[4]
80
+      );
81
+    } else if (columns.length == 4) {
82
+      registrationParser.convertAndSend(
83
+        columns[0],
84
+        columns[1],
85
+        "",
86
+        columns[2],
87
+        columns[3]
88
+      );
89
+    } else {
90
+      throw new IllegalArgumentException("Format error on line: " + line);
91
+    }
92
+  }
93
+
94
+  private void delete(File file) {
95
+    if (!file.delete()) {
96
+      logger.error("Could not delete " + file.getName());  
97
+    }
98
+  }
99
+
100
+  private void move(File file) {
101
+    final File destination = new File(parseFailureDirectory, file.getName());
102
+    final boolean result = file.renameTo(destination);
103
+    if (!result) {
104
+      logger.error("Could not move " + file.getName() + " to " + destination.getAbsolutePath());
105
+    }
106
+  }
107
+
108
+  public void setUploadDirectory(File uploadDirectory) {
109
+    this.uploadDirectory = uploadDirectory;
110
+  }
111
+
112
+  public void setParseFailureDirectory(File parseFailureDirectory) {
113
+    this.parseFailureDirectory = parseFailureDirectory;
114
+  }
115
+
116
+  public void setRegistrationParser(RegistrationParser registrationParser) {
117
+    this.registrationParser = registrationParser;
118
+  }
119
+
120
+  public void afterPropertiesSet() throws Exception {
121
+    if (uploadDirectory.equals(parseFailureDirectory)) {
122
+      throw new Exception("Upload and parse failed directories must not be the same directory: " + uploadDirectory);
123
+    }
124
+  }
125
+}

+ 91
- 0
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/ws/HandlingReport.java View File

@@ -0,0 +1,91 @@
1
+package se.citerus.dddsample.interfaces.handling.ws;
2
+
3
+import javax.xml.bind.annotation.XmlElement;
4
+import javax.xml.bind.annotation.XmlSchemaType;
5
+import javax.xml.datatype.XMLGregorianCalendar;
6
+
7
+/**
8
+ * Data type for registering handling events
9
+ * using the web service api. Focus here is interoperability
10
+ * and maintaining a stable api, and possibly things like
11
+ * backwards compatibility with a system that's being replaced,
12
+ * or adhering to an industry standard.
13
+ *
14
+ * We do not want this to constrain our modeling in any way.
15
+ * 
16
+ */
17
+public class HandlingReport {
18
+
19
+  @XmlElement(required = true)
20
+  private String[] trackingIds;
21
+
22
+  @XmlElement(required = true)
23
+  private String unLocode;
24
+
25
+  @XmlElement(required = false)
26
+  private String voyageNumber;
27
+
28
+  @XmlSchemaType(name="dateTime")
29
+  @XmlElement(required = true)
30
+  private XMLGregorianCalendar completionTime;
31
+
32
+  @XmlElement(required = true)
33
+  private String type;
34
+
35
+  /**
36
+   * @return tracking ids of cargos that have been handled (the same way)
37
+   */
38
+  public String[] getTrackingIds() {
39
+    return trackingIds;
40
+  }
41
+
42
+  public void setTrackingIds(String[] trackingIds) {
43
+    this.trackingIds = trackingIds;
44
+  }
45
+
46
+  /**
47
+   * @return United Nations Location Code for the location where the event occured
48
+   */
49
+  public String getUnLocode() {
50
+    return unLocode;
51
+  }
52
+
53
+  public void setUnLocode(String unLocode) {
54
+    this.unLocode = unLocode;
55
+  }
56
+
57
+  /**
58
+   * Not all events are associated with a voyage (customs handling etc).
59
+   * 
60
+   * @return voyage number, if applicable
61
+   */
62
+  public String getVoyageNumber() {
63
+    return voyageNumber;
64
+  }
65
+
66
+  public void setVoyageNumber(String voyageNumber) {
67
+    this.voyageNumber = voyageNumber;
68
+  }
69
+
70
+  /**
71
+   * @return time when event occured, for example a the loading of cargo was completed
72
+   */
73
+  public XMLGregorianCalendar getCompletionTime() {
74
+    return completionTime;
75
+  }
76
+
77
+  public void setCompletionTime(XMLGregorianCalendar completionTime) {
78
+    this.completionTime = completionTime;
79
+  }
80
+
81
+  /**
82
+   * @return type of event
83
+   */
84
+  public String getType() {
85
+    return type;
86
+  }
87
+
88
+  public void setType(String type) {
89
+    this.type = type;
90
+  }
91
+}

+ 21
- 0
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/ws/HandlingReportService.java View File

@@ -0,0 +1,21 @@
1
+package se.citerus.dddsample.interfaces.handling.ws;
2
+
3
+import se.citerus.dddsample.interfaces.handling.RegistrationFailure;
4
+
5
+import javax.jws.WebService;
6
+
7
+/**
8
+ * Web service endpoint for handling event registration.
9
+ */
10
+@WebService
11
+public interface HandlingReportService {
12
+
13
+  /**
14
+   * Submits a report of handled cargos.
15
+   *
16
+   * @param handlingReport
17
+   * @throws se.citerus.dddsample.interfaces.handling.RegistrationFailure
18
+   */
19
+  void submitReport(HandlingReport handlingReport) throws RegistrationFailure;
20
+
21
+}

+ 39
- 0
dddsample/src/main/java/se/citerus/dddsample/interfaces/handling/ws/HandlingReportServiceImpl.java View File

@@ -0,0 +1,39 @@
1
+package se.citerus.dddsample.interfaces.handling.ws;
2
+
3
+import org.apache.commons.logging.Log;
4
+import org.apache.commons.logging.LogFactory;
5
+import se.citerus.dddsample.interfaces.handling.RegistrationFailure;
6
+import se.citerus.dddsample.interfaces.handling.RegistrationParser;
7
+
8
+import javax.jws.WebService;
9
+import java.util.Date;
10
+
11
+/**
12
+ * This web service endpoint implementation performs basic validation and parsing
13
+ * of incoming data, and in case of a valid registration attempt, sends an asynchronous message
14
+ * with the informtion to the handling event registration system for proper registration.
15
+ *  
16
+ */
17
+@WebService(endpointInterface = "se.citerus.dddsample.interfaces.handling.ws.HandlingReportService")
18
+public class HandlingReportServiceImpl implements HandlingReportService {
19
+
20
+  private RegistrationParser registrationParser;
21
+  private static final Log logger = LogFactory.getLog(HandlingReportServiceImpl.class);
22
+  
23
+  public static final String ISO_8601_FORMAT = "yyyy-mm-dd HH:MM:SS.SSS";
24
+
25
+  @Override
26
+  public void submitReport(HandlingReport handlingReport) throws RegistrationFailure {
27
+    Date date = handlingReport.getCompletionTime().toGregorianCalendar().getTime();
28
+    for (String trackingId : handlingReport.getTrackingIds()) {
29
+      registrationParser.convertAndSend(
30
+        "", trackingId, handlingReport.getVoyageNumber(), handlingReport.getUnLocode(), handlingReport.getType()
31
+      );
32
+    }
33
+  }
34
+
35
+  public void setRegistrationParser(RegistrationParser registrationParser) {
36
+    this.registrationParser = registrationParser;
37
+  }
38
+
39
+}

+ 30
- 3
dddsample/src/main/resources/context-remote.xml View File

@@ -10,7 +10,7 @@
10 10
         http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
11 11
 
12 12
   <!-- Handling event registration web service -->
13
-  
13
+
14 14
   <wss:bindings id="jax-ws.http">
15 15
     <wss:bindings>
16 16
       <wss:binding url="/ws/RegisterEvent">
@@ -21,12 +21,39 @@
21 21
     </wss:bindings>
22 22
   </wss:bindings>
23 23
 
24
-  <bean id="handlingEventServiceEndpoint" class="se.citerus.dddsample.interfaces.handling.HandlingEventServiceEndpointImpl">
24
+  <bean id="handlingEventServiceEndpoint" class="se.citerus.dddsample.interfaces.handling.ws.HandlingReportServiceImpl">
25
+    <property name="registrationParser" ref="registrationParser"/>
26
+  </bean>
27
+
28
+  <!-- File upload directory scanner -->
29
+
30
+  <bean id="scheduledTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
31
+    <property name="period" value="5000"/>
32
+    <property name="delay" value="3000"/>
33
+    <property name="timerTask" ref="uploadDirectoryScanner"/>
34
+  </bean>
35
+
36
+  <bean id="uploadDirectoryScanner" class="se.citerus.dddsample.interfaces.handling.file.UploadDirectoryScanner">
37
+    <property name="uploadDirectory" value="/tmp/upload"/>
38
+    <property name="parseFailureDirectory" value="/tmp/failed"/>
39
+    <property name="registrationParser" ref="registrationParser"/>
40
+  </bean>
41
+
42
+  <bean id="registrationParser" class="se.citerus.dddsample.interfaces.handling.RegistrationParser">
25 43
     <property name="systemEvents" ref="systemEvents"/>
26 44
   </bean>
27 45
 
28
-  <!-- RMI exposed booking service facade -->
46
+
47
+  <bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
48
+    <property name="scheduledTimerTasks">
49
+      <list>
50
+        <ref bean="scheduledTask"/>
51
+      </list>
52
+    </property>
53
+  </bean>
29 54
   
55
+  <!-- RMI exposed booking service facade -->
56
+
30 57
   <bean id="rmiBookingServiceFacade" class="org.springframework.remoting.rmi.RmiServiceExporter">
31 58
     <property name="serviceInterface" value="se.citerus.dddsample.interfaces.booking.facade.BookingServiceFacade"/>
32 59
     <property name="service" ref="bookingServiceFacade"/>