├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── pipeline.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENSE.md ├── README.md ├── api-docs.yaml ├── dddsample.drawio.png ├── dddsample.drawio.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── config │ ├── checkstyle.xml │ └── dddsample-format-eclipse.pref ├── java │ ├── com │ │ └── pathfinder │ │ │ ├── api │ │ │ ├── GraphTraversalService.java │ │ │ ├── TransitEdge.java │ │ │ ├── TransitPath.java │ │ │ └── package.html │ │ │ ├── config │ │ │ └── PathfinderApplicationContext.java │ │ │ ├── internal │ │ │ ├── GraphDAO.java │ │ │ ├── GraphDAOStub.java │ │ │ ├── GraphTraversalServiceImpl.java │ │ │ └── package.html │ │ │ └── package.html │ └── se │ │ └── citerus │ │ └── dddsample │ │ ├── Application.java │ │ ├── application │ │ ├── ApplicationEvents.java │ │ ├── BookingService.java │ │ ├── CargoInspectionService.java │ │ ├── HandlingEventService.java │ │ ├── impl │ │ │ ├── BookingServiceImpl.java │ │ │ ├── CargoInspectionServiceImpl.java │ │ │ ├── HandlingEventServiceImpl.java │ │ │ └── package.html │ │ ├── package.html │ │ └── util │ │ │ ├── DateUtils.java │ │ │ └── package.html │ │ ├── config │ │ └── DDDSampleApplicationContext.java │ │ ├── domain │ │ ├── model │ │ │ ├── cargo │ │ │ │ ├── Cargo.java │ │ │ │ ├── CargoFactory.java │ │ │ │ ├── CargoRepository.java │ │ │ │ ├── Delivery.java │ │ │ │ ├── HandlingActivity.java │ │ │ │ ├── Itinerary.java │ │ │ │ ├── Leg.java │ │ │ │ ├── RouteSpecification.java │ │ │ │ ├── RoutingStatus.java │ │ │ │ ├── TrackingId.java │ │ │ │ ├── TransportStatus.java │ │ │ │ └── package.html │ │ │ ├── handling │ │ │ │ ├── CannotCreateHandlingEventException.java │ │ │ │ ├── HandlingEvent.java │ │ │ │ ├── HandlingEventFactory.java │ │ │ │ ├── HandlingEventRepository.java │ │ │ │ ├── HandlingHistory.java │ │ │ │ ├── UnknownCargoException.java │ │ │ │ ├── UnknownLocationException.java │ │ │ │ ├── UnknownVoyageException.java │ │ │ │ └── package.html │ │ │ ├── location │ │ │ │ ├── Location.java │ │ │ │ ├── LocationRepository.java │ │ │ │ ├── UnLocode.java │ │ │ │ └── package.html │ │ │ ├── package.html │ │ │ └── voyage │ │ │ │ ├── CarrierMovement.java │ │ │ │ ├── Schedule.java │ │ │ │ ├── Voyage.java │ │ │ │ ├── VoyageNumber.java │ │ │ │ ├── VoyageRepository.java │ │ │ │ └── package.html │ │ ├── package.html │ │ ├── service │ │ │ ├── RoutingService.java │ │ │ └── package.html │ │ └── shared │ │ │ ├── AbstractSpecification.java │ │ │ ├── AndSpecification.java │ │ │ ├── DomainEntity.java │ │ │ ├── DomainEvent.java │ │ │ ├── NotSpecification.java │ │ │ ├── OrSpecification.java │ │ │ ├── Specification.java │ │ │ ├── ValueObject.java │ │ │ └── package.html │ │ ├── infrastructure │ │ ├── messaging │ │ │ └── jms │ │ │ │ ├── CargoHandledConsumer.java │ │ │ │ ├── HandlingEventRegistrationAttemptConsumer.java │ │ │ │ ├── InfrastructureMessagingJmsConfig.java │ │ │ │ ├── JmsApplicationEventsImpl.java │ │ │ │ ├── SimpleLoggingConsumer.java │ │ │ │ └── package.html │ │ ├── persistence │ │ │ ├── hibernate │ │ │ │ └── CargoRepositoryHibernate.java │ │ │ └── jpa │ │ │ │ ├── CargoRepositoryJPA.java │ │ │ │ ├── HandlingEventRepositoryJPA.java │ │ │ │ ├── LocationRepositoryJPA.java │ │ │ │ ├── VoyageRepositoryJPA.java │ │ │ │ └── package.html │ │ ├── routing │ │ │ ├── ExternalRoutingService.java │ │ │ └── package.html │ │ └── sampledata │ │ │ ├── SampleDataGenerator.java │ │ │ ├── SampleLocations.java │ │ │ └── SampleVoyages.java │ │ └── interfaces │ │ ├── InterfacesApplicationContext.java │ │ ├── booking │ │ ├── facade │ │ │ ├── BookingServiceFacade.java │ │ │ ├── dto │ │ │ │ ├── CargoRoutingDTO.java │ │ │ │ ├── LegDTO.java │ │ │ │ ├── LocationDTO.java │ │ │ │ ├── RouteCandidateDTO.java │ │ │ │ └── package.html │ │ │ ├── internal │ │ │ │ ├── BookingServiceFacadeImpl.java │ │ │ │ ├── assembler │ │ │ │ │ ├── CargoRoutingDTOAssembler.java │ │ │ │ │ ├── ItineraryCandidateDTOAssembler.java │ │ │ │ │ ├── LocationDTOAssembler.java │ │ │ │ │ └── package.html │ │ │ │ └── package.html │ │ │ └── package.html │ │ └── web │ │ │ ├── CargoAdminController.java │ │ │ ├── RegistrationCommand.java │ │ │ ├── RouteAssignmentCommand.java │ │ │ └── package.html │ │ ├── handling │ │ ├── HandlingEventRegistrationAttempt.java │ │ ├── HandlingReportParser.java │ │ ├── file │ │ │ ├── UploadDirectoryScanner.java │ │ │ └── package.html │ │ ├── package.html │ │ └── ws │ │ │ ├── HandlingReport.java │ │ │ ├── HandlingReportService.java │ │ │ ├── HandlingReportServiceImpl.java │ │ │ └── package.html │ │ └── tracking │ │ ├── CargoTrackingController.java │ │ ├── CargoTrackingViewAdapter.java │ │ ├── TrackCommand.java │ │ ├── TrackCommandValidator.java │ │ ├── package.html │ │ └── ws │ │ ├── CargoTrackingDTO.java │ │ ├── CargoTrackingDTOConverter.java │ │ ├── CargoTrackingRestService.java │ │ └── HandlingEventDTO.java └── resources │ ├── application.yml │ ├── messages_en.properties │ ├── static │ ├── admin.css │ ├── calendar.css │ ├── css │ │ ├── bootstrap-datepicker.css │ │ └── bootstrap.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── images │ │ ├── calendarTrigger.gif │ │ ├── cross.png │ │ ├── dddsample_logotype.png │ │ ├── dddsample_logotype_small.png │ │ ├── error.png │ │ ├── shade.png │ │ ├── tick.png │ │ └── web_logo.png │ ├── index.html │ ├── js │ │ ├── bootstrap-datepicker.js │ │ └── jquery-2.1.4.js │ └── style.css │ └── templates │ ├── admin │ ├── list.html │ ├── pickNewDestination.html │ ├── registrationForm.html │ ├── selectItinerary.html │ └── show.html │ ├── adminDecorator.html │ └── track.html ├── site ├── apt │ ├── architecture.apt │ ├── changelog.apt │ ├── characterization.apt │ ├── download.apt │ ├── handlingEventRegistration.apt │ ├── index.apt │ ├── patterns-reference.apt │ └── roadmap.apt ├── resources │ ├── css │ │ └── site.css │ └── images │ │ ├── banner-left.png │ │ ├── eclipse1.png │ │ ├── eclipse2.png │ │ ├── eclipse3.png │ │ ├── eclipse4.png │ │ ├── frontpage.jpeg │ │ └── layers.jpg ├── site.xml └── xdoc │ └── screencast.xml └── test ├── java └── se │ └── citerus │ └── dddsample │ ├── acceptance │ ├── AbstractAcceptanceTest.java │ ├── AdminAcceptanceTest.java │ ├── CustomerAcceptanceTest.java │ └── pages │ │ ├── AdminPage.java │ │ ├── CargoBookingPage.java │ │ ├── CargoDestinationPage.java │ │ ├── CargoDetailsPage.java │ │ ├── CargoRoutingPage.java │ │ └── CustomerPage.java │ ├── application │ ├── BookingServiceTest.java │ └── HandlingEventServiceTest.java │ ├── domain │ ├── model │ │ ├── cargo │ │ │ ├── CargoTest.java │ │ │ ├── ItineraryTest.java │ │ │ ├── LegTest.java │ │ │ ├── RouteSpecificationTest.java │ │ │ └── TrackingIdTest.java │ │ ├── handling │ │ │ ├── HandlingEventFactoryTest.java │ │ │ ├── HandlingEventTest.java │ │ │ └── HandlingHistoryTest.java │ │ ├── location │ │ │ ├── LocationTest.java │ │ │ └── UnLocodeTest.java │ │ └── voyage │ │ │ └── CarrierMovementTest.java │ └── shared │ │ ├── AlwaysFalseSpec.java │ │ ├── AlwaysTrueSpec.java │ │ ├── AndSpecificationTest.java │ │ ├── NotSpecificationTest.java │ │ └── OrSpecificationTest.java │ ├── infrastructure │ ├── messaging │ │ └── stub │ │ │ └── SynchronousApplicationEventsStub.java │ ├── persistence │ │ ├── inmemory │ │ │ ├── CargoRepositoryInMem.java │ │ │ ├── HandlingEventRepositoryInMem.java │ │ │ ├── LocationRepositoryInMem.java │ │ │ └── VoyageRepositoryInMem.java │ │ └── jpa │ │ │ ├── CargoRepositoryTest.java │ │ │ ├── CarrierMovementRepositoryTest.java │ │ │ ├── HandlingEventRepositoryTest.java │ │ │ ├── LocationRepositoryTest.java │ │ │ └── TestRepositoryConfig.java │ └── routing │ │ └── ExternalRoutingServiceTest.java │ ├── interfaces │ ├── booking │ │ ├── facade │ │ │ └── internal │ │ │ │ └── assembler │ │ │ │ ├── CargoRoutingDTOAssemblerTest.java │ │ │ │ ├── ItineraryCandidateDTOAssemblerTest.java │ │ │ │ └── LocationDTOAssemblerTest.java │ │ └── web │ │ │ └── ItinerarySelectionCommandTest.java │ ├── handling │ │ ├── HandlingReportIntegrationTest.java │ │ ├── HandlingReportParserTest.java │ │ └── file │ │ │ └── UploadDirectoryScannerTest.java │ └── tracking │ │ ├── CargoTrackingControllerTest.java │ │ ├── CargoTrackingViewAdapterTest.java │ │ ├── TrackCommandValidatorTest.java │ │ └── ws │ │ ├── CargoTrackingDTOConverterTest.java │ │ └── CargoTrackingRestServiceIntegrationTest.java │ └── scenario │ └── CargoLifecycleScenarioTest.java └── resources ├── config └── application.yml ├── handling_events.csv ├── sampleCargoTrackingResponse.json ├── sampleHandlingReport.json ├── sampleHandlingReportFile.csv └── sampleInvalidHandlingReportFile.csv /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: ["dependencies"] 8 | open-pull-requests-limit: 3 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Why 2 | 3 | ### What 4 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | pull_request: 8 | branches: [ "*" ] 9 | push: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | cache: maven 23 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 24 | settings-path: ${{ github.workspace }} # location for the settings.xml file 25 | - name: Build with Maven 26 | run: mvn -B compile 27 | - name: Run tests 28 | run: mvn -B test 29 | # Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 30 | - name: Update dependency graph 31 | uses: advanced-security/maven-dependency-submission-action@v4 32 | continue-on-error: true # optional as this step consistently fails for forked repos by design 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dddsample.iml 3 | .idea 4 | target 5 | /build/ 6 | *~ 7 | /.gradle/ 8 | .DS_Store -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2022 Citerus AB and Domain Language, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDDSample 2 | [![Java CI with Maven](https://github.com/citerus/dddsample-core/actions/workflows/pipeline.yml/badge.svg)](https://github.com/citerus/dddsample-core/actions/workflows/pipeline.yml) 3 | 4 | This is the new home of the original DDD Sample app hosted at SourceForge. 5 | 6 | Our intention is to move everything from SourceForge to GitHub in due time while starting upgrading both the technical aspects as well as the DDD aspects of the DDD Sample. 7 | 8 | This project is a joint effort by Eric Evans' company [Domain Language](https://www.domainlanguage.com/) and the [Swedish software consulting company Citerus](https://www.citerus.se/). 9 | 10 | The application uses Spring Boot. To start it go to the root directory and type `mvn spring-boot:run` or run the `main` method of the `Application` class from your IDE. 11 | Then open http://localhost:8080/dddsample in your browser (and make sure that no firewall is blocking the communication and that Javascript for localhost is not blocked). 12 | 13 | Discussion group: https://groups.google.com/forum/#!forum/dddsample 14 | 15 | Development blog: https://citerus.github.io/dddsample-core/ 16 | 17 | Trello board: https://trello.com/b/PTDFRyxd 18 | 19 | ## How to build 20 | 21 | Using Maven (we recommend using the included Maven wrapper), run this command to compile and run all the tests: 22 | 23 | ./mvnw verify 24 | 25 | If you want to compile without running the tests, run the following command: 26 | 27 | ./mvnw compile 28 | 29 | For Windows users, use the included `mvnw.cmd` file instead, without the `./` but using the same arguments. 30 | 31 | Use this command to generate the site's documentation 32 | 33 | ./mvnw site:run 34 | 35 | The site can then be accessed at [`http://localhost:8080`](http://localhost:8080). 36 | 37 | ## How to run 38 | 39 | To start the app using the included application server and in-process HSQL database, run this command: 40 | 41 | ./mvnw spring-boot:run 42 | 43 | For Windows users, use the included `mvnw.cmd` file instead, without the `./` but using the same arguments. 44 | 45 | ## Entity relationships 46 | 47 | ![](./dddsample.drawio.png) 48 | 49 | The diagram was created with diagrams.net (formerly draw.io). 50 | 51 | ## Using the HandlingReport REST API 52 | 53 | The HandlingReport API has one endpoint that takes a JSON request body: 54 | 55 | POST /dddsample/handlingReport 56 | 57 | You can use cURL to send the request using an JSON file for the body: 58 | 59 | curl --data-binary "@/path/to/project/src/test/resources/sampleHandlingReport.json" \ 60 | -H 'Content-Type: application/json;charset=UTF-8' \ 61 | http://localhost:8080/dddsample/handlingReport 62 | 63 | See the [api-docs.yaml](/api-docs.yaml) file for a complete API definition. 64 | -------------------------------------------------------------------------------- /api-docs.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: OpenAPI definition 4 | version: v0 5 | servers: 6 | - url: http://localhost:8080/dddsample 7 | description: Generated server url 8 | paths: 9 | /handlingReport: 10 | post: 11 | tags: 12 | - handling-report-service-impl 13 | operationId: submitReport 14 | requestBody: 15 | content: 16 | application/json: 17 | schema: 18 | $ref: '#/components/schemas/HandlingReport' 19 | required: true 20 | responses: 21 | 200: 22 | description: OK 23 | content: 24 | application/json: 25 | schema: 26 | type: object 27 | components: 28 | schemas: 29 | HandlingReport: 30 | required: 31 | - completionTime 32 | - trackingIds 33 | - type 34 | - unLocode 35 | type: object 36 | properties: 37 | completionTime: 38 | type: string 39 | format: date-time 40 | trackingIds: 41 | type: array 42 | items: 43 | type: string 44 | type: 45 | type: string 46 | unLocode: 47 | type: string 48 | voyageNumber: 49 | type: string 50 | -------------------------------------------------------------------------------- /dddsample.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/dddsample.drawio.png -------------------------------------------------------------------------------- /dddsample.drawio.xml: -------------------------------------------------------------------------------- 1 | 2 | 7Vxbc5s4FP41fmxGiPtj66btdpKZzGZm2zyqoNh0MfLIcmz3168IkkE4gGIwSnboS6zD0QWdc75zkejMnq/2XylaL29JjNMZBPF+Zn+eQWhBx+Z/csqhoAQuLAgLmsSCqSTcJ3+wIAJB3SYx3iiMjJCUJWuVGJEswxFTaIhSslPZHkmqzrpGC3xCuI9Qekr9kcRsKd8ClPRvOFks5cwWEE9WSDILwmaJYrKrkOzrmT2nhLDi12o/x2m+eXJfin5fGp4eF0ZxxnQ6fN+C3ebH7Qf4YD18n1vz6xv49QN0xOLYQb4xjvkGiCahbEkWJEPpdUn9RMk2i3E+LOCtkueGkDUnWpz4GzN2ENJEW0Y4aclWqXiK9wn7mf++AsAX7Yd8uCvfd0X7816M/9w4yEbG6OFntVH0c2Wz7PbcOlQGucM0WWGGqaAVb56/buOOSo1DdIFZyzZazlGg3BIw4dPQA+9IcYpY8qROgIRKLo58x653JOFTQyDMB/rgKgyKXsJ+oAfVUTZkSyMsOpbi5z8qKylJz0rxGgVxTSoIVwi3oh/WRXRDXw+KvW7bLV19AT31RRHzq2VajPuE0q2Yac4XTE4EvdklqxRluUQfScakzPM9i5ZJGt+gA9nmr7lhKPpXtj4tCU3+cH4kpckfUyZkbQOF4z7vKcakeMN57uS2WzXSLdorjDdow+RqSJqi9Sb59by+vOOKv1CSfSKMkZUi5idMGd63C/pULrKDDRRjtHzR3pWuwXIEbam6hUvJ0jdrn7Bqn0DTPkuTPFr1OPbp6dqnZ9I+HatVpjj9RXbN4vy9Xa0lK6JRTw+teufznLM/IgJrSzgcwmN/pBQdKgzr3A1vmh26B1UAcS1Q05ZixEFduNuuTjoQMahO1Zz6eaABBlOpTlXpiwUNsV1NFWBQcxENgV2zyg0d+rn9Q78LYhH8H2GRb9LbuP0jiGHh4TyXcyl4GE7KrgmPU3c4Ip/7oscuA94mdrceENsj+DPvJH3hIRHfQGELUwbTlsG4qsCcQDOD8S6VwNhmK1DvzafIRLIbbgITcOP4qn7ZQQeAgH78juPOLg44cs8riPMXSzJM0QQ53ZBj13yErVs0uRzkeCYhZ5D0Z8SaieWMFeD0EqpcZsVI/+amhe/XOEoek4gvgWSTtXZZqyML/se0NDRsrZZ7ItgbMolTU5xeLUAPNcG3Xo0YDnwDs+B7XsEamsourbedXvoqWsj0oil8c8JW/guFb6cJ4zeUxWmSLT5GfGsSNkVx3UDycpxuzi3A0CSODHIu0pU2npbVhgQWXxdY+p6U9DsLAybFrNYizywPwHciZ6Phu9s/J7tgFVq7CB36oSr8MAg6pP/cOv9ukqYCDH9+5flXwA3Lf7W8wQ+vgF157KkTFOr4itOtl1dh1wrWsv7UWN/2/Db+CwUgfmMAcv2ExQZM0UdbDakuZtd0+GG/pXtxmmmMpULTqGXr4E2nMZ6lopfttcNI/R7Ha/nhGHmP066iY14jGuQeZ2ccZURfjUbHcpnV2hheTP6ky5/Ie1dHfwKM+5P+l7R62ef7OpIINa2zSQ1Gymn6565v4TJnrfg5nJxHuqT5wiGyavxOzahfffNOaxq3ozbq12IQd5TaaHjiQP4hh/ybqMmHdPgQL6ipkWfch8DJh+h/qqMLQhCa9CFy4IqB3kdLHG/TyUS7TbR+G8n81RP73ZlorWowZhYGoW6gYLR2LZepflBHE0xvyRNeTRW+M0wVmj9gNGqqaqz+9s4XRyrkvfRJtKonmsF7d8DMm+XH+AV7+V8a2Nf/AQ== -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/api/GraphTraversalService.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.api; 2 | 3 | import java.rmi.Remote; 4 | import java.util.List; 5 | import java.util.Properties; 6 | 7 | /** 8 | * Part of the external graph traversal API exposed by the routing team 9 | * and used by us (booking and tracking team). 10 | * 11 | */ 12 | public interface GraphTraversalService extends Remote { 13 | 14 | /** 15 | * @param origin origin point 16 | * @param destination destination point 17 | * @param limitations restrictions on the path selection, as key-value according to some API specification 18 | * @return A list of transit paths 19 | */ 20 | List findShortestPath(String origin, 21 | String destination, 22 | Properties limitations); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/api/TransitEdge.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.api; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | 6 | /** 7 | * Represents an edge in a path through a graph, 8 | * describing the route of a cargo. 9 | * 10 | */ 11 | public final class TransitEdge implements Serializable { 12 | 13 | private final String edge; 14 | private final String fromNode; 15 | private final String toNode; 16 | private final Instant fromDate; 17 | private final Instant toDate; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param edge 23 | * @param fromNode 24 | * @param toNode 25 | * @param fromDate 26 | * @param toDate 27 | */ 28 | public TransitEdge(final String edge, 29 | final String fromNode, 30 | final String toNode, 31 | final Instant fromDate, 32 | final Instant toDate) { 33 | this.edge = edge; 34 | this.fromNode = fromNode; 35 | this.toNode = toNode; 36 | this.fromDate = fromDate; 37 | this.toDate = toDate; 38 | } 39 | 40 | public String getEdge() { 41 | return edge; 42 | } 43 | 44 | public String getFromNode() { 45 | return fromNode; 46 | } 47 | 48 | public String getToNode() { 49 | return toNode; 50 | } 51 | 52 | public Instant getFromDate() { 53 | return fromDate; 54 | } 55 | 56 | public Instant getToDate() { 57 | return toDate; 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/api/TransitPath.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.api; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | /** 8 | * 9 | */ 10 | public final class TransitPath implements Serializable { 11 | 12 | private final List transitEdges; 13 | 14 | /** 15 | * Constructor. 16 | * 17 | * @param transitEdges The legs for this itinerary. 18 | */ 19 | public TransitPath(final List transitEdges) { 20 | this.transitEdges = transitEdges; 21 | } 22 | 23 | /** 24 | * @return An unmodifiable list DTOs. 25 | */ 26 | public List getTransitEdges() { 27 | return Collections.unmodifiableList(transitEdges); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/api/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Public API for the pathfinder application. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/config/PathfinderApplicationContext.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.config; 2 | 3 | import com.pathfinder.api.GraphTraversalService; 4 | import com.pathfinder.internal.GraphDAO; 5 | import com.pathfinder.internal.GraphDAOStub; 6 | import com.pathfinder.internal.GraphTraversalServiceImpl; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class PathfinderApplicationContext { 12 | 13 | private GraphDAO graphDAO() { 14 | return new GraphDAOStub(); 15 | } 16 | 17 | @Bean 18 | public GraphTraversalService graphTraversalService() { 19 | return new GraphTraversalServiceImpl(graphDAO()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/internal/GraphDAO.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.internal; 2 | 3 | import java.util.List; 4 | 5 | public interface GraphDAO { 6 | List listAllNodes(); 7 | String getTransitEdge(String from, String to); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/internal/GraphDAOStub.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.internal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Random; 6 | 7 | public class GraphDAOStub implements GraphDAO{ 8 | 9 | private static final Random random = new Random(); 10 | 11 | public List listAllNodes() { 12 | return new ArrayList(List.of( 13 | "CNHKG", "AUMEL", "SESTO", "FIHEL", "USCHI", "JNTKO", "DEHAM", "CNSHA", "NLRTM", "SEGOT", "CNHGH", "USNYC", "USDAL" 14 | )); 15 | } 16 | 17 | public String getTransitEdge(String from, String to) { 18 | final int i = random.nextInt(5); 19 | if (i == 0) return "0100S"; 20 | if (i == 1) return "0200T"; 21 | if (i == 2) return "0300A"; 22 | if (i == 3) return "0301S"; 23 | return "0400S"; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/internal/GraphTraversalServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.pathfinder.internal; 2 | 3 | import com.pathfinder.api.GraphTraversalService; 4 | import com.pathfinder.api.TransitEdge; 5 | import com.pathfinder.api.TransitPath; 6 | 7 | import java.time.Instant; 8 | import java.time.temporal.ChronoUnit; 9 | import java.util.*; 10 | 11 | public class GraphTraversalServiceImpl implements GraphTraversalService { 12 | 13 | private GraphDAO dao; 14 | private Random random; 15 | private static final long ONE_MIN_MS = 1000 * 60; 16 | private static final long ONE_DAY_MS = ONE_MIN_MS * 60 * 24; 17 | 18 | public GraphTraversalServiceImpl(GraphDAO dao) { 19 | this.dao = dao; 20 | this.random = new Random(); 21 | } 22 | 23 | public List findShortestPath( 24 | final String originNode, final String destinationNode, final Properties limitations) { 25 | List allVertices = dao.listAllNodes(); 26 | allVertices.remove(originNode); 27 | allVertices.remove(destinationNode); 28 | 29 | int candidateCount = getRandomNumberOfCandidates(); 30 | List candidates = new ArrayList<>(candidateCount); 31 | 32 | for (int i = 0; i < candidateCount; i++) { 33 | allVertices = getRandomChunkOfNodes(allVertices); 34 | List transitEdges = new ArrayList<>(allVertices.size() - 1); 35 | String fromNode = originNode; 36 | Instant date = Instant.now(); 37 | 38 | for (int j = 0; j <= allVertices.size(); ++j) { 39 | Instant fromDate = nextDate(date); 40 | Instant toDate = nextDate(fromDate); 41 | String toNode = (j >= allVertices.size() ? destinationNode : allVertices.get(j)); 42 | transitEdges.add( 43 | new TransitEdge( 44 | dao.getTransitEdge(fromNode, toNode), fromNode, toNode, fromDate, toDate)); 45 | fromNode = toNode; 46 | date = nextDate(toDate); 47 | } 48 | candidates.add(new TransitPath(transitEdges)); 49 | } 50 | return candidates; 51 | } 52 | 53 | private Instant nextDate(Instant date) { 54 | return date.plus(1, ChronoUnit.DAYS).plus((random.nextInt(1000) - 500), ChronoUnit.MINUTES); 55 | } 56 | 57 | private int getRandomNumberOfCandidates() { 58 | return 3 + random.nextInt(3); 59 | } 60 | 61 | private List getRandomChunkOfNodes(List allNodes) { 62 | Collections.shuffle(allNodes); 63 | final int total = allNodes.size(); 64 | final int chunk = total > 4 ? 1 + random.nextInt(5) : total; 65 | return allNodes.subList(0, chunk); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/internal/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Internal parts of the pathfinder application. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/pathfinder/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | This is the pathfinder application context, which is separate from "our" application and context. 5 | Our domain model with cargo, itinerary, handling event etc does not exist here. 6 | The routing domain service implementation works against the API exposed by 7 | this context. 8 |

9 |

10 | It is not related to the core application at all, and is only part of this source tree for 11 | developer convenience. 12 |

13 | 14 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/Application.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample; 2 | 3 | import com.pathfinder.config.PathfinderApplicationContext; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Import; 7 | import se.citerus.dddsample.config.DDDSampleApplicationContext; 8 | 9 | @Import({DDDSampleApplicationContext.class, 10 | PathfinderApplicationContext.class}) 11 | @SpringBootApplication 12 | public class Application { 13 | 14 | public static void main(String[] args) throws Exception { 15 | SpringApplication.run(Application.class, args); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/ApplicationEvents.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.Cargo; 4 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 5 | import se.citerus.dddsample.interfaces.handling.HandlingEventRegistrationAttempt; 6 | 7 | /** 8 | * This interface provides a way to let other parts 9 | * of the system know about events that have occurred. 10 | * 11 | * It may be implemented synchronously or asynchronously, using 12 | * for example JMS. 13 | */ 14 | public interface ApplicationEvents { 15 | 16 | /** 17 | * A cargo has been handled. 18 | * 19 | * @param event handling event 20 | */ 21 | void cargoWasHandled(HandlingEvent event); 22 | 23 | /** 24 | * A cargo has been misdirected. 25 | * 26 | * @param cargo cargo 27 | */ 28 | void cargoWasMisdirected(Cargo cargo); 29 | 30 | /** 31 | * A cargo has arrived at its final destination. 32 | * 33 | * @param cargo cargo 34 | */ 35 | void cargoHasArrived(Cargo cargo); 36 | 37 | /** 38 | * A handling event regitration attempt is received. 39 | * 40 | * @param attempt handling event registration attempt 41 | */ 42 | void receivedHandlingEventRegistrationAttempt(HandlingEventRegistrationAttempt attempt); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/BookingService.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.Itinerary; 4 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 5 | import se.citerus.dddsample.domain.model.location.UnLocode; 6 | 7 | import java.time.Instant; 8 | import java.util.List; 9 | 10 | /** 11 | * Cargo booking service. 12 | */ 13 | public interface BookingService { 14 | 15 | /** 16 | * Registers a new cargo in the tracking system, not yet routed. 17 | * 18 | * @param origin cargo origin 19 | * @param destination cargo destination 20 | * @param arrivalDeadline arrival deadline 21 | * @return Cargo tracking id 22 | */ 23 | TrackingId bookNewCargo(UnLocode origin, UnLocode destination, Instant arrivalDeadline); 24 | 25 | /** 26 | * Requests a list of itineraries describing possible routes for this cargo. 27 | * 28 | * @param trackingId cargo tracking id 29 | * @return A list of possible itineraries for this cargo 30 | */ 31 | List requestPossibleRoutesForCargo(TrackingId trackingId); 32 | 33 | /** 34 | * @param itinerary itinerary describing the selected route 35 | * @param trackingId cargo tracking id 36 | */ 37 | void assignCargoToRoute(Itinerary itinerary, TrackingId trackingId); 38 | 39 | /** 40 | * Changes the destination of a cargo. 41 | * 42 | * @param trackingId cargo tracking id 43 | * @param unLocode UN locode of new destination 44 | */ 45 | void changeDestination(TrackingId trackingId, UnLocode unLocode); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/CargoInspectionService.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 4 | 5 | /** 6 | * Cargo inspection service. 7 | */ 8 | public interface CargoInspectionService { 9 | 10 | /** 11 | * Inspect cargo and send relevant notifications to interested parties, 12 | * for example if a cargo has been misdirected, or unloaded 13 | * at the final destination. 14 | * 15 | * @param trackingId cargo tracking id 16 | */ 17 | void inspectCargo(TrackingId trackingId); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/HandlingEventService.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 4 | import se.citerus.dddsample.domain.model.handling.CannotCreateHandlingEventException; 5 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 6 | import se.citerus.dddsample.domain.model.location.UnLocode; 7 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 8 | 9 | import java.time.Instant; 10 | 11 | 12 | /** 13 | * Handling event service. 14 | */ 15 | public interface HandlingEventService { 16 | 17 | /** 18 | * Registers a handling event in the system, and notifies interested 19 | * parties that a cargo has been handled. 20 | * 21 | * @param completionTime when the event was completed 22 | * @param trackingId cargo tracking id 23 | * @param voyageNumber voyage number 24 | * @param unLocode UN locode for the location where the event occurred 25 | * @param type type of event 26 | * @throws se.citerus.dddsample.domain.model.handling.CannotCreateHandlingEventException 27 | * if a handling event that represents an actual event that's relevant to a cargo we're tracking 28 | * can't be created from the parameters 29 | */ 30 | void registerHandlingEvent(Instant completionTime, 31 | TrackingId trackingId, 32 | VoyageNumber voyageNumber, 33 | UnLocode unLocode, 34 | HandlingEvent.Type type) throws CannotCreateHandlingEventException; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/impl/CargoInspectionServiceImpl.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application.impl; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import se.citerus.dddsample.application.ApplicationEvents; 7 | import se.citerus.dddsample.application.CargoInspectionService; 8 | import se.citerus.dddsample.domain.model.cargo.Cargo; 9 | import se.citerus.dddsample.domain.model.cargo.CargoRepository; 10 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 11 | import se.citerus.dddsample.domain.model.handling.HandlingEventRepository; 12 | import se.citerus.dddsample.domain.model.handling.HandlingHistory; 13 | 14 | import java.lang.invoke.MethodHandles; 15 | import java.util.Objects; 16 | 17 | public class CargoInspectionServiceImpl implements CargoInspectionService { 18 | 19 | private final ApplicationEvents applicationEvents; 20 | private final CargoRepository cargoRepository; 21 | private final HandlingEventRepository handlingEventRepository; 22 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 23 | 24 | public CargoInspectionServiceImpl(final ApplicationEvents applicationEvents, 25 | final CargoRepository cargoRepository, 26 | final HandlingEventRepository handlingEventRepository) { 27 | this.applicationEvents = applicationEvents; 28 | this.cargoRepository = cargoRepository; 29 | this.handlingEventRepository = handlingEventRepository; 30 | } 31 | 32 | @Override 33 | @Transactional 34 | public void inspectCargo(final TrackingId trackingId) { 35 | Objects.requireNonNull(trackingId, "Tracking ID is required"); 36 | 37 | final Cargo cargo = cargoRepository.find(trackingId); 38 | if (cargo == null) { 39 | logger.warn("Can't inspect non-existing cargo {}", trackingId); 40 | return; 41 | } 42 | 43 | final HandlingHistory handlingHistory = handlingEventRepository.lookupHandlingHistoryOfCargo(trackingId); 44 | 45 | cargo.deriveDeliveryProgress(handlingHistory); 46 | 47 | if (cargo.delivery().isMisdirected()) { 48 | applicationEvents.cargoWasMisdirected(cargo); 49 | } 50 | 51 | if (cargo.delivery().isUnloadedAtDestination()) { 52 | applicationEvents.cargoHasArrived(cargo); 53 | } 54 | 55 | cargoRepository.store(cargo); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/impl/HandlingEventServiceImpl.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application.impl; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import se.citerus.dddsample.application.ApplicationEvents; 7 | import se.citerus.dddsample.application.HandlingEventService; 8 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 9 | import se.citerus.dddsample.domain.model.handling.CannotCreateHandlingEventException; 10 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 11 | import se.citerus.dddsample.domain.model.handling.HandlingEventFactory; 12 | import se.citerus.dddsample.domain.model.handling.HandlingEventRepository; 13 | import se.citerus.dddsample.domain.model.location.UnLocode; 14 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 15 | 16 | import java.lang.invoke.MethodHandles; 17 | import java.time.Instant; 18 | 19 | public class HandlingEventServiceImpl implements HandlingEventService { 20 | 21 | private final ApplicationEvents applicationEvents; 22 | private final HandlingEventRepository handlingEventRepository; 23 | private final HandlingEventFactory handlingEventFactory; 24 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 25 | 26 | public HandlingEventServiceImpl(final HandlingEventRepository handlingEventRepository, 27 | final ApplicationEvents applicationEvents, 28 | final HandlingEventFactory handlingEventFactory) { 29 | this.handlingEventRepository = handlingEventRepository; 30 | this.applicationEvents = applicationEvents; 31 | this.handlingEventFactory = handlingEventFactory; 32 | } 33 | 34 | @Override 35 | @Transactional(rollbackFor = CannotCreateHandlingEventException.class) 36 | public void registerHandlingEvent(final Instant completionTime, 37 | final TrackingId trackingId, 38 | final VoyageNumber voyageNumber, 39 | final UnLocode unLocode, 40 | final HandlingEvent.Type type) throws CannotCreateHandlingEventException { 41 | final Instant registrationTime = Instant.now(); 42 | /* Using a factory to create a HandlingEvent (aggregate). This is where 43 | it is determined whether the incoming data, the attempt, actually is capable 44 | of representing a real handling event. */ 45 | final HandlingEvent event = handlingEventFactory.createHandlingEvent( 46 | registrationTime, completionTime, trackingId, voyageNumber, unLocode, type 47 | ); 48 | 49 | /* Store the new handling event, which updates the persistent 50 | state of the handling event aggregate (but not the cargo aggregate - 51 | that happens asynchronously!) 52 | */ 53 | handlingEventRepository.store(event); 54 | 55 | /* Publish an event stating that a cargo has been handled. */ 56 | applicationEvents.cargoWasHandled(event); 57 | 58 | logger.info("Registered handling event: {}", event); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/impl/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Implementation of the application layer. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | This is the application layer: code that's needed for the application to 5 | performs its tasks. It defines, or is defined by, use cases. 6 |

7 |

8 | It is thin in terms of knowledge of domain business logic, 9 | although it may be large in terms of lines of code. 10 | It coordinates the domain layer objects to perform the actual tasks. 11 |

12 |

13 | This layer is suitable for spanning transactions, security checks and high-level logging. 14 |

15 | 16 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/util/DateUtils.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application.util; 2 | 3 | import java.time.Instant; 4 | import java.time.format.DateTimeParseException; 5 | 6 | /** 7 | * A few utils for working with Date in tests. 8 | * 9 | */ 10 | public final class DateUtils { 11 | 12 | /** 13 | * @param date date string as yyyy-MM-dd 14 | * @return Date representation 15 | */ 16 | public static Instant toDate(final String date) { 17 | return toDate(date, "00:00"); 18 | } 19 | 20 | /** 21 | * @param date date string as yyyy-MM-dd 22 | * @param time time string as HH:mm 23 | * @return Date representation 24 | */ 25 | public static Instant toDate(final String date, final String time) { 26 | try { 27 | return Instant.parse(date + "T" + time + ":00Z"); 28 | } catch (DateTimeParseException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | /** 34 | * Prevent instantiation. 35 | */ 36 | private DateUtils() { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/application/util/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Various utilities. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/CargoFactory.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import se.citerus.dddsample.domain.model.location.Location; 4 | import se.citerus.dddsample.domain.model.location.LocationRepository; 5 | import se.citerus.dddsample.domain.model.location.UnLocode; 6 | 7 | import java.time.Instant; 8 | 9 | 10 | public class CargoFactory { 11 | private final LocationRepository locationRepository; 12 | private final CargoRepository cargoRepository; 13 | 14 | public CargoFactory(LocationRepository locationRepository, CargoRepository cargoRepository) { 15 | this.locationRepository = locationRepository; 16 | this.cargoRepository = cargoRepository; 17 | } 18 | 19 | public Cargo createCargo(UnLocode originUnLoCode, UnLocode destinationUnLoCode, Instant arrivalDeadline) { 20 | final TrackingId trackingId = cargoRepository.nextTrackingId(); 21 | final Location origin = locationRepository.find(originUnLoCode); 22 | final Location destination = locationRepository.find(destinationUnLoCode); 23 | final RouteSpecification routeSpecification = new RouteSpecification(origin, destination, arrivalDeadline); 24 | 25 | return new Cargo(trackingId, routeSpecification); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/CargoRepository.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import java.util.List; 4 | 5 | public interface CargoRepository { 6 | 7 | /** 8 | * Finds a cargo using given id. 9 | * 10 | * @param trackingId Id 11 | * @return Cargo if found, else {@code null} 12 | */ 13 | Cargo find(TrackingId trackingId); 14 | 15 | /** 16 | * Finds all cargo. 17 | * 18 | * @return All cargo. 19 | */ 20 | List getAll(); 21 | 22 | /** 23 | * Saves given cargo. 24 | * 25 | * @param cargo cargo to save 26 | */ 27 | void store(Cargo cargo); 28 | 29 | /** 30 | * @return A unique, generated tracking Id. 31 | */ 32 | TrackingId nextTrackingId(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/HandlingActivity.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import jakarta.persistence.*; 4 | import org.apache.commons.lang3.builder.EqualsBuilder; 5 | import org.apache.commons.lang3.builder.HashCodeBuilder; 6 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 7 | import se.citerus.dddsample.domain.model.location.Location; 8 | import se.citerus.dddsample.domain.model.voyage.Voyage; 9 | import se.citerus.dddsample.domain.shared.ValueObject; 10 | 11 | import java.util.Objects; 12 | 13 | /** 14 | * A handling activity represents how and where a cargo can be handled, 15 | * and can be used to express predictions about what is expected to 16 | * happen to a cargo in the future. 17 | * 18 | */ 19 | @Embeddable 20 | public class HandlingActivity implements ValueObject { 21 | 22 | // TODO make HandlingActivity a part of HandlingEvent too? There is some overlap. 23 | 24 | @Enumerated(value = EnumType.STRING) 25 | @Column(name = "next_expected_handling_event_type") 26 | public HandlingEvent.Type type; 27 | 28 | @ManyToOne() 29 | @JoinColumn(name = "next_expected_location_id") 30 | public Location location; 31 | 32 | @ManyToOne 33 | @JoinColumn(name = "next_expected_voyage_id") 34 | public Voyage voyage; 35 | 36 | public HandlingActivity(final HandlingEvent.Type type, final Location location) { 37 | Objects.requireNonNull(type, "Handling event type is required"); 38 | Objects.requireNonNull(location, "Location is required"); 39 | 40 | this.type = type; 41 | this.location = location; 42 | } 43 | 44 | public HandlingActivity(final HandlingEvent.Type type, final Location location, final Voyage voyage) { 45 | Objects.requireNonNull(type, "Handling event type is required"); 46 | Objects.requireNonNull(location, "Location is required"); 47 | Objects.requireNonNull(location, "Voyage is required"); 48 | 49 | this.type = type; 50 | this.location = location; 51 | this.voyage = voyage; 52 | } 53 | 54 | public HandlingEvent.Type type() { 55 | return type; 56 | } 57 | 58 | public Location location() { 59 | return location; 60 | } 61 | 62 | public Voyage voyage() { 63 | return voyage; 64 | } 65 | 66 | @Override 67 | public boolean sameValueAs(final HandlingActivity other) { 68 | return other != null && new EqualsBuilder(). 69 | append(this.type, other.type). 70 | append(this.location, other.location). 71 | append(this.voyage, other.voyage). 72 | isEquals(); 73 | } 74 | 75 | @Override 76 | public int hashCode() { 77 | return new HashCodeBuilder(). 78 | append(this.type). 79 | append(this.location). 80 | append(this.voyage). 81 | toHashCode(); 82 | } 83 | 84 | @Override 85 | public boolean equals(final Object obj) { 86 | if (obj == this) return true; 87 | if (obj == null) return false; 88 | if (obj.getClass() != this.getClass()) return false; 89 | 90 | HandlingActivity other = (HandlingActivity) obj; 91 | 92 | return sameValueAs(other); 93 | } 94 | 95 | protected HandlingActivity() { 96 | // Needed by Hibernate 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/Leg.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import jakarta.persistence.*; 4 | import org.apache.commons.lang3.Validate; 5 | import org.apache.commons.lang3.builder.EqualsBuilder; 6 | import org.apache.commons.lang3.builder.HashCodeBuilder; 7 | import se.citerus.dddsample.domain.model.location.Location; 8 | import se.citerus.dddsample.domain.model.voyage.Voyage; 9 | import se.citerus.dddsample.domain.shared.ValueObject; 10 | 11 | import java.time.Instant; 12 | 13 | /** 14 | * An itinerary consists of one or more legs. 15 | */ 16 | @Entity(name = "Leg") 17 | @Table(name = "Leg") 18 | public class Leg implements ValueObject { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.AUTO) 22 | private long id; 23 | 24 | @ManyToOne 25 | @JoinColumn(name="voyage_id") 26 | private Voyage voyage; 27 | 28 | @ManyToOne 29 | @JoinColumn(name = "load_location_id") 30 | private Location loadLocation; 31 | 32 | @Column(name = "load_time") 33 | private Instant loadTime; 34 | 35 | @ManyToOne 36 | @JoinColumn(name = "unload_location_id") 37 | private Location unloadLocation; 38 | 39 | @Column(name = "unload_time") 40 | private Instant unloadTime; 41 | 42 | public Leg(Voyage voyage, Location loadLocation, Location unloadLocation, Instant loadTime, Instant unloadTime) { 43 | Validate.noNullElements(new Object[] {voyage, loadLocation, unloadLocation, loadTime, unloadTime}); 44 | 45 | this.voyage = voyage; 46 | this.loadLocation = loadLocation; 47 | this.unloadLocation = unloadLocation; 48 | this.loadTime = loadTime; 49 | this.unloadTime = unloadTime; 50 | } 51 | 52 | public Voyage voyage() { 53 | return voyage; 54 | } 55 | 56 | public Location loadLocation() { 57 | return loadLocation; 58 | } 59 | 60 | public Location unloadLocation() { 61 | return unloadLocation; 62 | } 63 | 64 | public Instant loadTime() { 65 | return loadTime; 66 | } 67 | 68 | public Instant unloadTime() { 69 | return unloadTime; 70 | } 71 | 72 | @Override 73 | public boolean sameValueAs(final Leg other) { 74 | return other != null && new EqualsBuilder(). 75 | append(this.voyage, other.voyage). 76 | append(this.loadLocation, other.loadLocation). 77 | append(this.unloadLocation, other.unloadLocation). 78 | append(this.loadTime, other.loadTime). 79 | append(this.unloadTime, other.unloadTime). 80 | isEquals(); 81 | } 82 | 83 | @Override 84 | public boolean equals(final Object o) { 85 | if (this == o) return true; 86 | if (o == null || getClass() != o.getClass()) return false; 87 | 88 | Leg leg = (Leg) o; 89 | 90 | return sameValueAs(leg); 91 | } 92 | 93 | @Override 94 | public int hashCode() { 95 | return new HashCodeBuilder(). 96 | append(voyage). 97 | append(loadLocation). 98 | append(unloadLocation). 99 | append(loadTime). 100 | append(unloadTime). 101 | toHashCode(); 102 | } 103 | 104 | protected Leg() { 105 | // Needed by Hibernate 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/RoutingStatus.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import se.citerus.dddsample.domain.shared.ValueObject; 4 | 5 | /** 6 | * Routing status. 7 | */ 8 | public enum RoutingStatus implements ValueObject { 9 | NOT_ROUTED, ROUTED, MISROUTED; 10 | 11 | @Override 12 | public boolean sameValueAs(final RoutingStatus other) { 13 | return this.equals(other); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/TrackingId.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import org.apache.commons.lang3.Validate; 4 | import se.citerus.dddsample.domain.shared.ValueObject; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * Uniquely identifies a particular cargo. Automatically generated by the application. 10 | * 11 | */ 12 | public final class TrackingId implements ValueObject { 13 | 14 | private String id; 15 | 16 | /** 17 | * Constructor. 18 | * 19 | * @param id Id string. 20 | */ 21 | public TrackingId(final String id) { 22 | Objects.requireNonNull(id); 23 | Validate.notEmpty(id); 24 | this.id = id; 25 | } 26 | 27 | /** 28 | * @return String representation of this tracking id. 29 | */ 30 | public String idString() { 31 | return id; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | 39 | TrackingId other = (TrackingId) o; 40 | 41 | return sameValueAs(other); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return id.hashCode(); 47 | } 48 | 49 | @Override 50 | public boolean sameValueAs(TrackingId other) { 51 | return other != null && this.id.equals(other.id); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return id; 57 | } 58 | 59 | TrackingId() { 60 | // Needed by Hibernate 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/TransportStatus.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import se.citerus.dddsample.domain.shared.ValueObject; 4 | 5 | /** 6 | * Represents the different transport statuses for a cargo. 7 | */ 8 | public enum TransportStatus implements ValueObject { 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 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/cargo/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | The cargo aggregate. Cargo is the aggregate root. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/CannotCreateHandlingEventException.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | /** 4 | * If a {@link se.citerus.dddsample.domain.model.handling.HandlingEvent} can't be 5 | * created from a given set of parameters. 6 | * 7 | * It is a checked exception because it's not a programming error, but rather a 8 | * special case that the application is built to handle. It can occur during normal 9 | * program execution. 10 | */ 11 | public class CannotCreateHandlingEventException extends Exception { 12 | public CannotCreateHandlingEventException(Exception e) { 13 | super(e); 14 | } 15 | 16 | public CannotCreateHandlingEventException() { 17 | super(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/HandlingEventRepository.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 4 | 5 | /** 6 | * Handling event repository. 7 | */ 8 | public interface HandlingEventRepository { 9 | 10 | /** 11 | * Stores a (new) handling event. 12 | * 13 | * @param event handling event to save 14 | */ 15 | void store(HandlingEvent event); 16 | 17 | 18 | /** 19 | * @param trackingId cargo tracking id 20 | * @return The handling history of this cargo 21 | */ 22 | HandlingHistory lookupHandlingHistoryOfCargo(TrackingId trackingId); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/HandlingHistory.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 4 | import se.citerus.dddsample.domain.shared.ValueObject; 5 | 6 | import java.util.*; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * The handling history of a cargo. 11 | */ 12 | public class HandlingHistory implements ValueObject { 13 | 14 | private final List handlingEvents; 15 | 16 | public static final HandlingHistory EMPTY = new HandlingHistory(Collections.emptyList()); 17 | 18 | public HandlingHistory(Collection handlingEvents) { 19 | Objects.requireNonNull(handlingEvents, "Handling events are required"); 20 | 21 | this.handlingEvents = new ArrayList<>(handlingEvents); 22 | } 23 | 24 | /** 25 | * @return A distinct list (no duplicate registrations) of handling events, ordered by completion time. 26 | */ 27 | public List distinctEventsByCompletionTime() { 28 | final List ordered = new ArrayList<>( 29 | new HashSet<>(handlingEvents) 30 | ); 31 | ordered.sort(BY_COMPLETION_TIME_COMPARATOR); 32 | return Collections.unmodifiableList(ordered); 33 | } 34 | 35 | /** 36 | * @return Most recently completed event, or null if the delivery history is empty. 37 | */ 38 | public HandlingEvent mostRecentlyCompletedEvent() { 39 | final List distinctEvents = distinctEventsByCompletionTime(); 40 | if (distinctEvents.isEmpty()) { 41 | return null; 42 | } else { 43 | return distinctEvents.get(distinctEvents.size() - 1); 44 | } 45 | } 46 | 47 | /** 48 | * Filters handling history events to remove events for unrelated cargo. 49 | * @param trackingId the trackingId of the cargo to filter events for. 50 | * @return A new handling history with events matching the supplied tracking id. 51 | */ 52 | public HandlingHistory filterOnCargo(TrackingId trackingId) { 53 | List events = handlingEvents.stream() 54 | .filter(he -> he.cargo().trackingId().sameValueAs(trackingId)) 55 | .collect(Collectors.toList()); 56 | return new HandlingHistory(events); 57 | } 58 | 59 | @Override 60 | public boolean sameValueAs(HandlingHistory other) { 61 | return other != null && this.handlingEvents.equals(other.handlingEvents); 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) return true; 67 | if (o == null || getClass() != o.getClass()) return false; 68 | 69 | final HandlingHistory other = (HandlingHistory) o; 70 | return sameValueAs(other); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | return handlingEvents.hashCode(); 76 | } 77 | 78 | private static final Comparator BY_COMPLETION_TIME_COMPARATOR = 79 | Comparator.comparing(HandlingEvent::completionTime); 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/UnknownCargoException.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 4 | 5 | /** 6 | * Thrown when trying to register an event with an unknown tracking id. 7 | */ 8 | public final class UnknownCargoException extends CannotCreateHandlingEventException { 9 | 10 | private final TrackingId trackingId; 11 | 12 | /** 13 | * @param trackingId cargo tracking id 14 | */ 15 | public UnknownCargoException(final TrackingId trackingId) { 16 | this.trackingId = trackingId; 17 | } 18 | 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | @Override 23 | public String getMessage() { 24 | return "No cargo with tracking id " + trackingId.idString() + " exists in the system"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/UnknownLocationException.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | import se.citerus.dddsample.domain.model.location.UnLocode; 4 | 5 | public class UnknownLocationException extends CannotCreateHandlingEventException { 6 | 7 | private final UnLocode unlocode; 8 | 9 | public UnknownLocationException(final UnLocode unlocode) { 10 | this.unlocode = unlocode; 11 | } 12 | 13 | @Override 14 | public String getMessage() { 15 | return "No location with UN locode " + unlocode.idString() + " exists in the system"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/UnknownVoyageException.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 4 | 5 | /** 6 | * Thrown when trying to register an event with an unknown carrier movement id. 7 | */ 8 | public class UnknownVoyageException extends CannotCreateHandlingEventException { 9 | 10 | private final VoyageNumber voyageNumber; 11 | 12 | public UnknownVoyageException(VoyageNumber voyageNumber) { 13 | this.voyageNumber = voyageNumber; 14 | } 15 | 16 | @Override 17 | public String getMessage() { 18 | return "No voyage with number " + voyageNumber.idString() + " exists in the system"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/handling/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | The handling aggregate. HandlingEvent is the aggregate root. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/location/Location.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.location; 2 | 3 | import jakarta.persistence.*; 4 | import se.citerus.dddsample.domain.shared.DomainEntity; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * A location is our model is stops on a journey, such as cargo 10 | * origin or destination, or carrier movement endpoints. 11 | * It is uniquely identified by a UN Locode. 12 | * 13 | */ 14 | @Entity(name = "Location") 15 | @Table(name = "Location") 16 | public final class Location implements DomainEntity { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | private long id; 21 | 22 | @Column(nullable = false, unique = true, updatable = false) 23 | private String unlocode; 24 | 25 | @Column(nullable = false) 26 | private String name; 27 | 28 | /** 29 | * Special Location object that marks an unknown location. 30 | */ 31 | public static final Location UNKNOWN = new Location( 32 | new UnLocode("XXXXX"), "Unknown location" 33 | ); 34 | 35 | /** 36 | * Package-level constructor, visible for test and sample data purposes. 37 | * 38 | * @param unLocode UN Locode 39 | * @param name location name 40 | * @throws IllegalArgumentException if the UN Locode or name is null 41 | */ 42 | public Location(final UnLocode unLocode, final String name) { 43 | Objects.requireNonNull(unLocode); 44 | Objects.requireNonNull(name); 45 | 46 | this.unlocode = unLocode.idString(); 47 | this.name = name; 48 | } 49 | 50 | // Used by JPA 51 | public Location(String unloCode, String name) { 52 | this.unlocode = unloCode; 53 | this.name = name; 54 | } 55 | 56 | /** 57 | * @return UN Locode for this location. 58 | */ 59 | public UnLocode unLocode() { 60 | return new UnLocode(unlocode); 61 | } 62 | 63 | /** 64 | * @return Actual name of this location, e.g. "Stockholm". 65 | */ 66 | public String name() { 67 | return name; 68 | } 69 | 70 | public String code() { 71 | return unlocode; 72 | } 73 | 74 | public long id() { 75 | return id; 76 | } 77 | 78 | /** 79 | * @param object to compare 80 | * @return Since this is an entiy this will be true iff UN locodes are equal. 81 | */ 82 | @Override 83 | public boolean equals(final Object object) { 84 | if (object == null) { 85 | return false; 86 | } 87 | if (this == object) { 88 | return true; 89 | } 90 | if (!(object instanceof Location other)) { 91 | return false; 92 | } 93 | return sameIdentityAs(other); 94 | } 95 | 96 | @Override 97 | public boolean sameIdentityAs(final Location other) { 98 | return this.unlocode.equals(other.unlocode); 99 | } 100 | 101 | /** 102 | * @return Hash code of UN locode. 103 | */ 104 | @Override 105 | public int hashCode() { 106 | return unlocode.hashCode(); 107 | } 108 | 109 | @Override 110 | public String toString() { 111 | return name + " [" + unlocode + "]"; 112 | } 113 | 114 | Location() { 115 | // Needed by Hibernate 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/location/LocationRepository.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.location; 2 | 3 | import java.util.List; 4 | 5 | public interface LocationRepository { 6 | 7 | /** 8 | * Finds a location using given unlocode. 9 | * 10 | * @param unLocode UNLocode. 11 | * @return Location. 12 | */ 13 | Location find(UnLocode unLocode); 14 | 15 | /** 16 | * Finds all locations. 17 | * 18 | * @return All locations. 19 | */ 20 | List getAll(); 21 | 22 | Location store(Location location); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/location/UnLocode.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.location; 2 | 3 | import org.apache.commons.lang3.Validate; 4 | import se.citerus.dddsample.domain.shared.ValueObject; 5 | 6 | import java.util.Objects; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * United nations location code. 11 | * 12 | * http://www.unece.org/cefact/locode/ 13 | * http://www.unece.org/cefact/locode/DocColumnDescription.htm#LOCODE 14 | */ 15 | public final class UnLocode implements ValueObject { 16 | 17 | private String unlocode; 18 | 19 | // Country code is exactly two letters. 20 | // Location code is usually three letters, but may contain the numbers 2-9 as well 21 | private static final Pattern VALID_PATTERN = Pattern.compile("[a-zA-Z]{2}[a-zA-Z2-9]{3}"); 22 | 23 | /** 24 | * Constructor. 25 | * 26 | * @param countryAndLocation Location string. 27 | */ 28 | public UnLocode(final String countryAndLocation) { 29 | Objects.requireNonNull(countryAndLocation, "Country and location may not be null"); 30 | Validate.isTrue(VALID_PATTERN.matcher(countryAndLocation).matches(), 31 | countryAndLocation + " is not a valid UN/LOCODE (does not match pattern)"); 32 | 33 | this.unlocode = countryAndLocation.toUpperCase(); 34 | } 35 | 36 | /** 37 | * @return country code and location code concatenated, always upper case. 38 | */ 39 | public String idString() { 40 | return unlocode; 41 | } 42 | 43 | @Override 44 | public boolean equals(final Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | 48 | UnLocode other = (UnLocode) o; 49 | 50 | return sameValueAs(other); 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return unlocode.hashCode(); 56 | } 57 | 58 | @Override 59 | public boolean sameValueAs(UnLocode other) { 60 | return other != null && this.unlocode.equals(other.unlocode); 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return idString(); 66 | } 67 | 68 | UnLocode() { 69 | // Needed by Hibernate 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/location/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | The location aggregate. Location is the aggregate root. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | The domain model. This is the heart of the application. Each aggregate is contained in 5 | its own subpackage, along with the repository interface, factories and exceptions where 6 | applicable. 7 |

8 | 9 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/voyage/Schedule.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.voyage; 2 | 3 | import org.apache.commons.lang3.Validate; 4 | import org.apache.commons.lang3.builder.HashCodeBuilder; 5 | import se.citerus.dddsample.domain.shared.ValueObject; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | /** 12 | * A voyage schedule. 13 | * 14 | */ 15 | public class Schedule implements ValueObject { 16 | 17 | private List carrierMovements = Collections.emptyList(); 18 | 19 | public static final Schedule EMPTY = new Schedule(); 20 | 21 | public Schedule(final List carrierMovements) { 22 | Objects.requireNonNull(carrierMovements); 23 | Validate.noNullElements(carrierMovements); 24 | Validate.notEmpty(carrierMovements); 25 | 26 | this.carrierMovements = carrierMovements; 27 | } 28 | 29 | /** 30 | * @return Carrier movements. 31 | */ 32 | public List carrierMovements() { 33 | return Collections.unmodifiableList(carrierMovements); 34 | } 35 | 36 | @Override 37 | public boolean sameValueAs(final Schedule other) { 38 | return other != null && this.carrierMovements.equals(other.carrierMovements); 39 | } 40 | 41 | @Override 42 | public boolean equals(final Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | 46 | final Schedule that = (Schedule) o; 47 | 48 | return sameValueAs(that); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return new HashCodeBuilder().append(this.carrierMovements).toHashCode(); 54 | } 55 | 56 | Schedule() { 57 | // Needed by Hibernate 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/voyage/VoyageNumber.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.voyage; 2 | 3 | import se.citerus.dddsample.domain.shared.ValueObject; 4 | 5 | import java.util.Objects; 6 | 7 | /** 8 | * Identifies a voyage. 9 | * 10 | */ 11 | public class VoyageNumber implements ValueObject { 12 | 13 | private String number; 14 | 15 | public VoyageNumber(String number) { 16 | Objects.requireNonNull(number); 17 | 18 | this.number = number; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null) return false; 25 | if (!(o instanceof VoyageNumber)) return false; 26 | 27 | final VoyageNumber other = (VoyageNumber) o; 28 | 29 | return sameValueAs(other); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return number.hashCode(); 35 | } 36 | 37 | @Override 38 | public boolean sameValueAs(VoyageNumber other) { 39 | return other != null && this.number.equals(other.number); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return number; 45 | } 46 | 47 | public String idString() { 48 | return number; 49 | } 50 | 51 | VoyageNumber() { 52 | // Needed by Hibernate 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/voyage/VoyageRepository.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.voyage; 2 | 3 | public interface VoyageRepository { 4 | 5 | /** 6 | * Finds a voyage using voyage number. 7 | * 8 | * @param voyageNumber voyage number 9 | * @return The voyage, or null if not found. 10 | */ 11 | Voyage find(VoyageNumber voyageNumber); 12 | 13 | void store(Voyage voyage); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/model/voyage/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | The voyage aggregate. Voyage is the aggregate root. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | The domain model, services and repository interfaces. This is the central part of the 5 | application. The ubiquitous language is used in classes, interfaces and method signatures, 6 | and every concept in here is familiar to a expert in the cargo shiping domain. 7 |

8 |

9 | There is no infrastructure or user interface related code here, except for things like 10 | transactional and security metadata which is likely to be relevant to a domain expert 11 | ("Either all of foo succeeds or none of it does", "In order to do bar you need to be a Supervisor", and so on). 12 |

13 | 14 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/service/RoutingService.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.service; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.Itinerary; 4 | import se.citerus.dddsample.domain.model.cargo.RouteSpecification; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Routing service. 10 | * 11 | */ 12 | public interface RoutingService { 13 | 14 | /** 15 | * @param routeSpecification route specification 16 | * @return A list of itineraries that satisfy the specification. May be an empty list if no route is found. 17 | */ 18 | List fetchRoutesForSpecification(RouteSpecification routeSpecification); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/service/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Domain services. Those services that may be implemented purely using the domain layer 5 | have their implementations here, other implementations may be part of the aplication 6 | or infrastructure layers. 7 |

8 | 9 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/AbstractSpecification.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | 4 | /** 5 | * Abstract base implementation of composite {@link Specification} with default 6 | * implementations for {@code and}, {@code or} and {@code not}. 7 | */ 8 | public abstract class AbstractSpecification implements Specification { 9 | 10 | /** 11 | * {@inheritDoc} 12 | */ 13 | public abstract boolean isSatisfiedBy(T t); 14 | 15 | /** 16 | * {@inheritDoc} 17 | */ 18 | public Specification and(final Specification specification) { 19 | return new AndSpecification(this, specification); 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public Specification or(final Specification specification) { 26 | return new OrSpecification(this, specification); 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public Specification not(final Specification specification) { 33 | return new NotSpecification(specification); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/AndSpecification.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | /** 4 | * AND specification, used to create a new specifcation that is the AND of two other specifications. 5 | */ 6 | public class AndSpecification extends AbstractSpecification { 7 | 8 | private Specification spec1; 9 | private Specification spec2; 10 | 11 | /** 12 | * Create a new AND specification based on two other spec. 13 | * 14 | * @param spec1 Specification one. 15 | * @param spec2 Specification two. 16 | */ 17 | public AndSpecification(final Specification spec1, final Specification spec2) { 18 | this.spec1 = spec1; 19 | this.spec2 = spec2; 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public boolean isSatisfiedBy(final T t) { 26 | return spec1.isSatisfiedBy(t) && spec2.isSatisfiedBy(t); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/DomainEntity.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | /** 4 | * An entity, as explained in the DDD book. 5 | * 6 | */ 7 | public interface DomainEntity { 8 | 9 | /** 10 | * Entities compare by identity, not by attributes. 11 | * 12 | * @param other The other entity. 13 | * @return true if the identities are the same, regardless of other attributes. 14 | */ 15 | boolean sameIdentityAs(T other); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | /** 4 | * A domain event is something that is unique, but does not have a lifecycle. 5 | * The identity may be explicit, for example the sequence number of a payment, 6 | * or it could be derived from various aspects of the event such as where, when and what 7 | * has happened. 8 | */ 9 | public interface DomainEvent { 10 | 11 | /** 12 | * @param other The other domain event. 13 | * @return true if the given domain event and this event are regarded as being the same event. 14 | */ 15 | boolean sameEventAs(T other); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/NotSpecification.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | /** 4 | * NOT decorator, used to create a new specifcation that is the inverse (NOT) of the given spec. 5 | */ 6 | public class NotSpecification extends AbstractSpecification { 7 | 8 | private Specification spec1; 9 | 10 | /** 11 | * Create a new NOT specification based on another spec. 12 | * 13 | * @param spec1 Specification instance to not. 14 | */ 15 | public NotSpecification(final Specification spec1) { 16 | this.spec1 = spec1; 17 | } 18 | 19 | /** 20 | * {@inheritDoc} 21 | */ 22 | public boolean isSatisfiedBy(final T t) { 23 | return !spec1.isSatisfiedBy(t); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/OrSpecification.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | /** 4 | * OR specification, used to create a new specifcation that is the OR of two other specifications. 5 | */ 6 | public class OrSpecification extends AbstractSpecification { 7 | 8 | private Specification spec1; 9 | private Specification spec2; 10 | 11 | /** 12 | * Create a new OR specification based on two other spec. 13 | * 14 | * @param spec1 Specification one. 15 | * @param spec2 Specification two. 16 | */ 17 | public OrSpecification(final Specification spec1, final Specification spec2) { 18 | this.spec1 = spec1; 19 | this.spec2 = spec2; 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public boolean isSatisfiedBy(final T t) { 26 | return spec1.isSatisfiedBy(t) || spec2.isSatisfiedBy(t); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/Specification.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | /** 4 | * Specificaiton interface. 5 | * 6 | * Use {@link se.citerus.dddsample.domain.shared.AbstractSpecification} as base for creating specifications, and 7 | * only the method {@link #isSatisfiedBy(Object)} must be implemented. 8 | */ 9 | public interface Specification { 10 | 11 | /** 12 | * Check if {@code t} is satisfied by the specification. 13 | * 14 | * @param t Object to test. 15 | * @return {@code true} if {@code t} satisfies the specification. 16 | */ 17 | boolean isSatisfiedBy(T t); 18 | 19 | /** 20 | * Create a new specification that is the AND operation of {@code this} specification and another specification. 21 | * @param specification Specification to AND. 22 | * @return A new specification. 23 | */ 24 | Specification and(Specification specification); 25 | 26 | /** 27 | * Create a new specification that is the OR operation of {@code this} specification and another specification. 28 | * @param specification Specification to OR. 29 | * @return A new specification. 30 | */ 31 | Specification or(Specification specification); 32 | 33 | /** 34 | * Create a new specification that is the NOT operation of {@code this} specification. 35 | * @param specification Specification to NOT. 36 | * @return A new specification. 37 | */ 38 | Specification not(Specification specification); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/ValueObject.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * A value object, as described in the DDD book. 7 | * 8 | */ 9 | public interface ValueObject extends Serializable { 10 | 11 | /** 12 | * Value objects compare by the values of their attributes, they don't have an identity. 13 | * 14 | * @param other The other value object. 15 | * @return true if the given value object's and this value object's attributes are the same. 16 | */ 17 | boolean sameValueAs(T other); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/domain/shared/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Pattern interfaces and support code for the domain layer. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/messaging/jms/CargoHandledConsumer.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.messaging.jms; 2 | 3 | import jakarta.jms.Message; 4 | import jakarta.jms.MessageListener; 5 | import jakarta.jms.TextMessage; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import se.citerus.dddsample.application.CargoInspectionService; 9 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 10 | 11 | import java.lang.invoke.MethodHandles; 12 | 13 | /** 14 | * Consumes JMS messages and delegates notification of misdirected 15 | * cargo to the tracking service. 16 | * 17 | * This is a programmatic hook into the JMS infrastructure to 18 | * make cargo inspection message-driven. 19 | */ 20 | public class CargoHandledConsumer implements MessageListener { 21 | 22 | private final CargoInspectionService cargoInspectionService; 23 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 24 | 25 | public CargoHandledConsumer(CargoInspectionService cargoInspectionService) { 26 | this.cargoInspectionService = cargoInspectionService; 27 | } 28 | 29 | @Override 30 | public void onMessage(final Message message) { 31 | try { 32 | final TextMessage textMessage = (TextMessage) message; 33 | final String trackingidString = textMessage.getText(); 34 | 35 | cargoInspectionService.inspectCargo(new TrackingId(trackingidString)); 36 | } catch (Exception e) { 37 | logger.error("Error consuming CargoHandled message", e); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/messaging/jms/HandlingEventRegistrationAttemptConsumer.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.messaging.jms; 2 | 3 | import jakarta.jms.Message; 4 | import jakarta.jms.MessageListener; 5 | import jakarta.jms.ObjectMessage; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import se.citerus.dddsample.application.HandlingEventService; 9 | import se.citerus.dddsample.interfaces.handling.HandlingEventRegistrationAttempt; 10 | 11 | import java.lang.invoke.MethodHandles; 12 | 13 | /** 14 | * Consumes handling event registration attempt messages and delegates to 15 | * proper registration. 16 | * 17 | */ 18 | public class HandlingEventRegistrationAttemptConsumer implements MessageListener { 19 | 20 | private final HandlingEventService handlingEventService; 21 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 22 | 23 | public HandlingEventRegistrationAttemptConsumer(HandlingEventService handlingEventService) { 24 | this.handlingEventService = handlingEventService; 25 | } 26 | 27 | @Override 28 | public void onMessage(final Message message) { 29 | try { 30 | final ObjectMessage om = (ObjectMessage) message; 31 | HandlingEventRegistrationAttempt attempt = (HandlingEventRegistrationAttempt) om.getObject(); 32 | handlingEventService.registerHandlingEvent( 33 | attempt.getCompletionTime(), 34 | attempt.getTrackingId(), 35 | attempt.getVoyageNumber(), 36 | attempt.getUnLocode(), 37 | attempt.getType() 38 | ); 39 | } catch (Exception e) { 40 | logger.error("Error consuming HandlingEventRegistrationAttempt message", e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/messaging/jms/JmsApplicationEventsImpl.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.messaging.jms; 2 | 3 | import jakarta.jms.Destination; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.jms.core.JmsOperations; 7 | import se.citerus.dddsample.application.ApplicationEvents; 8 | import se.citerus.dddsample.domain.model.cargo.Cargo; 9 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 10 | import se.citerus.dddsample.interfaces.handling.HandlingEventRegistrationAttempt; 11 | 12 | import java.lang.invoke.MethodHandles; 13 | 14 | /** 15 | * JMS based implementation. 16 | */ 17 | public final class JmsApplicationEventsImpl implements ApplicationEvents { 18 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 19 | 20 | private final JmsOperations jmsOperations; 21 | private final Destination cargoHandledQueue; 22 | private final Destination misdirectedCargoQueue; 23 | private final Destination deliveredCargoQueue; 24 | private final Destination rejectedRegistrationAttemptsQueue; // TODO why is this unused? 25 | private final Destination handlingEventQueue; 26 | 27 | public JmsApplicationEventsImpl(JmsOperations jmsOperations, Destination cargoHandledQueue, Destination misdirectedCargoQueue, Destination deliveredCargoQueue, Destination rejectedRegistrationAttemptsQueue, Destination handlingEventQueue) { 28 | this.jmsOperations = jmsOperations; 29 | this.cargoHandledQueue = cargoHandledQueue; 30 | this.misdirectedCargoQueue = misdirectedCargoQueue; 31 | this.deliveredCargoQueue = deliveredCargoQueue; 32 | this.rejectedRegistrationAttemptsQueue = rejectedRegistrationAttemptsQueue; 33 | this.handlingEventQueue = handlingEventQueue; 34 | } 35 | 36 | @Override 37 | public void cargoWasHandled(final HandlingEvent event) { 38 | final Cargo cargo = event.cargo(); 39 | logger.info("Cargo was handled {}", cargo); 40 | jmsOperations.send(cargoHandledQueue, session -> session.createTextMessage(cargo.trackingId().idString())); 41 | } 42 | 43 | @Override 44 | public void cargoWasMisdirected(final Cargo cargo) { 45 | logger.info("Cargo was misdirected {}", cargo); 46 | jmsOperations.send(misdirectedCargoQueue, session -> session.createTextMessage(cargo.trackingId().idString())); 47 | } 48 | 49 | @Override 50 | public void cargoHasArrived(final Cargo cargo) { 51 | logger.info("Cargo has arrived {}", cargo); 52 | jmsOperations.send(deliveredCargoQueue, session -> session.createTextMessage(cargo.trackingId().idString())); 53 | } 54 | 55 | @Override 56 | public void receivedHandlingEventRegistrationAttempt(final HandlingEventRegistrationAttempt attempt) { 57 | logger.info("Received handling event registration attempt {}", attempt); 58 | jmsOperations.send(handlingEventQueue, session -> session.createObjectMessage(attempt)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/messaging/jms/SimpleLoggingConsumer.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.messaging.jms; 2 | 3 | import jakarta.jms.Message; 4 | import jakarta.jms.MessageListener; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.lang.invoke.MethodHandles; 9 | 10 | public class SimpleLoggingConsumer implements MessageListener { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 13 | 14 | @Override 15 | public void onMessage(Message message) { 16 | logger.debug("Received JMS message: {}", message); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/messaging/jms/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Asynchronous messaging implemented using JMS. This is part of the infrastructure. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/persistence/hibernate/CargoRepositoryHibernate.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/java/se/citerus/dddsample/infrastructure/persistence/hibernate/CargoRepositoryHibernate.java -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/persistence/jpa/CargoRepositoryJPA.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import se.citerus.dddsample.domain.model.cargo.Cargo; 6 | import se.citerus.dddsample.domain.model.cargo.CargoRepository; 7 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.StreamSupport; 12 | 13 | /** 14 | * Hibernate implementation of CargoRepository. 15 | */ 16 | public interface CargoRepositoryJPA extends CrudRepository, CargoRepository { 17 | 18 | default Cargo find(TrackingId trackingId) { 19 | return findByTrackingId(trackingId.idString()); 20 | } 21 | 22 | @Query("select c from Cargo c where c.trackingId = :trackingId") 23 | Cargo findByTrackingId(String trackingId); 24 | 25 | default void store(final Cargo cargo) { 26 | save(cargo); 27 | } 28 | 29 | default List getAll() { 30 | return StreamSupport.stream(findAll().spliterator(), false) 31 | .collect(Collectors.toList()); 32 | } 33 | 34 | @Query(value = "SELECT UPPER(SUBSTR(CAST(UUID() AS VARCHAR(38)), 0, 9)) AS id FROM (VALUES(0))", nativeQuery = true) 35 | String nextTrackingIdString(); 36 | 37 | default TrackingId nextTrackingId() { 38 | return new TrackingId(nextTrackingIdString()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/persistence/jpa/HandlingEventRepositoryJPA.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 6 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 7 | import se.citerus.dddsample.domain.model.handling.HandlingEventRepository; 8 | import se.citerus.dddsample.domain.model.handling.HandlingHistory; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * Hibernate implementation of HandlingEventRepository. 14 | * 15 | */ 16 | public interface HandlingEventRepositoryJPA extends CrudRepository, HandlingEventRepository { 17 | 18 | default void store(final HandlingEvent event) { 19 | save(event); 20 | } 21 | 22 | default HandlingHistory lookupHandlingHistoryOfCargo(final TrackingId trackingId) { 23 | return new HandlingHistory(getHandlingHistoryOfCargo(trackingId.idString())); 24 | } 25 | 26 | @Query("select he from HandlingEvent he where he.cargo.trackingId = :trackingId and he.location is not NULL") 27 | List getHandlingHistoryOfCargo(String trackingId); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/persistence/jpa/LocationRepositoryJPA.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import se.citerus.dddsample.domain.model.location.Location; 6 | import se.citerus.dddsample.domain.model.location.LocationRepository; 7 | import se.citerus.dddsample.domain.model.location.UnLocode; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.StreamSupport; 12 | 13 | public interface LocationRepositoryJPA extends CrudRepository, LocationRepository { 14 | 15 | default Location find(final UnLocode unLocode) { 16 | return findByUnLoCode(unLocode.idString()); 17 | } 18 | 19 | @Query("select loc from Location loc where loc.unlocode = :unlocode") 20 | Location findByUnLoCode(String unlocode); 21 | 22 | @Override 23 | default List getAll() { 24 | return StreamSupport.stream(findAll().spliterator(), false) 25 | .collect(Collectors.toList()); 26 | } 27 | 28 | default Location store(Location location) { 29 | return save(location); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/persistence/jpa/VoyageRepositoryJPA.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import se.citerus.dddsample.domain.model.voyage.Voyage; 6 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 7 | import se.citerus.dddsample.domain.model.voyage.VoyageRepository; 8 | 9 | /** 10 | * Hibernate implementation of CarrierMovementRepository. 11 | */ 12 | public interface VoyageRepositoryJPA extends CrudRepository, VoyageRepository { 13 | 14 | default Voyage find(final VoyageNumber voyageNumber) { 15 | return findByVoyageNumber(voyageNumber.idString()); 16 | } 17 | 18 | @Query("select v from Voyage v where v.voyageNumber = :voyageNumber") 19 | Voyage findByVoyageNumber(String voyageNumber); 20 | 21 | @Override 22 | default void store(Voyage voyage) { 23 | save(voyage); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/persistence/jpa/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Hibernate implementations of the repository interfaces. This is part of the infrastructure. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/routing/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Communicates with the Pathfinder external routing service. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/infrastructure/sampledata/SampleLocations.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.sampledata; 2 | 3 | import se.citerus.dddsample.domain.model.location.Location; 4 | import se.citerus.dddsample.domain.model.location.UnLocode; 5 | 6 | import java.lang.reflect.Field; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * Sample locations, for test purposes. 14 | * 15 | */ 16 | public class SampleLocations { 17 | 18 | public static final Location HONGKONG = new Location(new UnLocode("CNHKG"), "Hongkong"); 19 | public static final Location MELBOURNE = new Location(new UnLocode("AUMEL"), "Melbourne"); 20 | public static final Location STOCKHOLM = new Location(new UnLocode("SESTO"), "Stockholm"); 21 | public static final Location HELSINKI = new Location(new UnLocode("FIHEL"), "Helsinki"); 22 | public static final Location CHICAGO = new Location(new UnLocode("USCHI"), "Chicago"); 23 | public static final Location TOKYO = new Location(new UnLocode("JNTKO"), "Tokyo"); 24 | public static final Location HAMBURG = new Location(new UnLocode("DEHAM"), "Hamburg"); 25 | public static final Location SHANGHAI = new Location(new UnLocode("CNSHA"), "Shanghai"); 26 | public static final Location ROTTERDAM = new Location(new UnLocode("NLRTM"), "Rotterdam"); 27 | public static final Location GOTHENBURG = new Location(new UnLocode("SEGOT"), "Göteborg"); 28 | public static final Location HANGZHOU = new Location(new UnLocode("CNHGH"), "Hangzhou"); 29 | public static final Location NEWYORK = new Location(new UnLocode("USNYC"), "New York"); 30 | public static final Location DALLAS = new Location(new UnLocode("USDAL"), "Dallas"); 31 | 32 | public static final Map ALL = new HashMap<>(); 33 | 34 | static { 35 | for (Field field : SampleLocations.class.getDeclaredFields()) { 36 | if (field.getType().equals(Location.class)) { 37 | try { 38 | Location location = (Location) field.get(null); 39 | ALL.put(location.unLocode(), location); 40 | } catch (IllegalAccessException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | } 45 | } 46 | 47 | public static List getAll() { 48 | return new ArrayList<>(ALL.values()); 49 | } 50 | 51 | public static Location lookup(UnLocode unLocode) { 52 | return ALL.get(unLocode); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/BookingServiceFacade.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade; 2 | 3 | import se.citerus.dddsample.interfaces.booking.facade.dto.CargoRoutingDTO; 4 | import se.citerus.dddsample.interfaces.booking.facade.dto.LocationDTO; 5 | import se.citerus.dddsample.interfaces.booking.facade.dto.RouteCandidateDTO; 6 | 7 | import java.rmi.RemoteException; 8 | import java.time.Instant; 9 | import java.util.List; 10 | 11 | /** 12 | * This facade shields the domain layer - model, services, repositories - 13 | * from concerns about such things as the user interface. 14 | */ 15 | public interface BookingServiceFacade { 16 | 17 | String bookNewCargo(String origin, String destination, Instant arrivalDeadline) throws RemoteException; 18 | 19 | CargoRoutingDTO loadCargoForRouting(String trackingId) throws RemoteException; 20 | 21 | void assignCargoToRoute(String trackingId, RouteCandidateDTO route) throws RemoteException; 22 | 23 | void changeDestination(String trackingId, String destinationUnLocode) throws RemoteException; 24 | 25 | List requestPossibleRoutesForCargo(String trackingId) throws RemoteException; 26 | 27 | List listShippingLocations() throws RemoteException; 28 | 29 | List listAllCargos() throws RemoteException; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/dto/CargoRoutingDTO.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.dto; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | import java.time.ZoneOffset; 6 | import java.time.ZonedDateTime; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | /** 12 | * DTO for registering and routing a cargo. 13 | */ 14 | public final class CargoRoutingDTO implements Serializable { 15 | 16 | private final String trackingId; 17 | private final String origin; 18 | private final String finalDestination; 19 | private final Instant arrivalDeadline; 20 | private final boolean misrouted; 21 | private final List legs; 22 | 23 | /** 24 | * Constructor. 25 | * 26 | * @param trackingId 27 | * @param origin 28 | * @param finalDestination 29 | * @param arrivalDeadline 30 | * @param misrouted 31 | */ 32 | public CargoRoutingDTO(String trackingId, String origin, String finalDestination, Instant arrivalDeadline, boolean misrouted) { 33 | this.trackingId = trackingId; 34 | this.origin = origin; 35 | this.finalDestination = finalDestination; 36 | this.arrivalDeadline = arrivalDeadline; 37 | this.misrouted = misrouted; 38 | this.legs = new ArrayList(); 39 | } 40 | 41 | public String getTrackingId() { 42 | return trackingId; 43 | } 44 | 45 | public String getOrigin() { 46 | return origin; 47 | } 48 | 49 | public String getFinalDestination() { 50 | return finalDestination; 51 | } 52 | 53 | public void addLeg(String voyageNumber, String from, String to, Instant loadTime, Instant unloadTime) { 54 | legs.add(new LegDTO(voyageNumber, from, to, loadTime, unloadTime)); 55 | } 56 | 57 | /** 58 | * @return An unmodifiable list DTOs. 59 | */ 60 | public List getLegs() { 61 | return Collections.unmodifiableList(legs); 62 | } 63 | 64 | public boolean isMisrouted() { 65 | return misrouted; 66 | } 67 | 68 | public boolean isRouted() { 69 | return !legs.isEmpty(); 70 | } 71 | 72 | public ZonedDateTime getArrivalDeadline() { 73 | return arrivalDeadline.atZone(ZoneOffset.UTC); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/dto/LegDTO.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.dto; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | 6 | /** 7 | * DTO for a leg in an itinerary. 8 | */ 9 | public final class LegDTO implements Serializable { 10 | 11 | private final String voyageNumber; 12 | private final String from; 13 | private final String to; 14 | private final Instant loadTime; 15 | private final Instant unloadTime; 16 | 17 | /** 18 | * Constructor. 19 | * 20 | * @param voyageNumber 21 | * @param from 22 | * @param to 23 | * @param loadTime 24 | * @param unloadTime 25 | */ 26 | public LegDTO(final String voyageNumber, final String from, final String to, Instant loadTime, Instant unloadTime) { 27 | this.voyageNumber = voyageNumber; 28 | this.from = from; 29 | this.to = to; 30 | this.loadTime = loadTime; 31 | this.unloadTime = unloadTime; 32 | } 33 | 34 | public String getVoyageNumber() { 35 | return voyageNumber; 36 | } 37 | 38 | public String getFrom() { 39 | return from; 40 | } 41 | 42 | public String getTo() { 43 | return to; 44 | } 45 | 46 | public Instant getLoadTime() { 47 | return loadTime; 48 | } 49 | 50 | public Instant getUnloadTime() { 51 | return unloadTime; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/dto/LocationDTO.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.dto; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Location DTO. 7 | */ 8 | public class LocationDTO implements Serializable { 9 | 10 | private final String unLocode; 11 | private final String name; 12 | 13 | public LocationDTO(String unLocode, String name) { 14 | this.unLocode = unLocode; 15 | this.name = name; 16 | } 17 | 18 | public String getUnLocode() { 19 | return unLocode; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/dto/RouteCandidateDTO.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.dto; 2 | 3 | import java.io.Serializable; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | /** 8 | * DTO for presenting and selecting an itinerary from a collection of candidates. 9 | */ 10 | public final class RouteCandidateDTO implements Serializable { 11 | 12 | private final List legs; 13 | 14 | /** 15 | * Constructor. 16 | * 17 | * @param legs The legs for this itinerary. 18 | */ 19 | public RouteCandidateDTO(final List legs) { 20 | this.legs = legs; 21 | } 22 | 23 | /** 24 | * @return An unmodifiable list DTOs. 25 | */ 26 | public List getLegs() { 27 | return Collections.unmodifiableList(legs); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/dto/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

DTOs for the remote booking client API.

4 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/internal/assembler/CargoRoutingDTOAssembler.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.internal.assembler; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.Cargo; 4 | import se.citerus.dddsample.domain.model.cargo.Leg; 5 | import se.citerus.dddsample.domain.model.cargo.RoutingStatus; 6 | import se.citerus.dddsample.interfaces.booking.facade.dto.CargoRoutingDTO; 7 | 8 | /** 9 | * Assembler class for the CargoRoutingDTO. 10 | */ 11 | public class CargoRoutingDTOAssembler { 12 | 13 | /** 14 | * 15 | * @param cargo cargo 16 | * @return A cargo routing DTO 17 | */ 18 | public CargoRoutingDTO toDTO(final Cargo cargo) { 19 | final CargoRoutingDTO dto = new CargoRoutingDTO( 20 | cargo.trackingId().idString(), 21 | cargo.origin().unLocode().idString(), 22 | cargo.routeSpecification().destination().unLocode().idString(), 23 | cargo.routeSpecification().arrivalDeadline(), 24 | cargo.delivery().routingStatus().sameValueAs(RoutingStatus.MISROUTED)); 25 | for (Leg leg : cargo.itinerary().legs()) { 26 | dto.addLeg( 27 | leg.voyage().voyageNumber().idString(), 28 | leg.loadLocation().unLocode().idString(), 29 | leg.unloadLocation().unLocode().idString(), 30 | leg.loadTime(), 31 | leg.unloadTime()); 32 | } 33 | return dto; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/internal/assembler/ItineraryCandidateDTOAssembler.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.internal.assembler; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.Itinerary; 4 | import se.citerus.dddsample.domain.model.cargo.Leg; 5 | import se.citerus.dddsample.domain.model.location.Location; 6 | import se.citerus.dddsample.domain.model.location.LocationRepository; 7 | import se.citerus.dddsample.domain.model.location.UnLocode; 8 | import se.citerus.dddsample.domain.model.voyage.Voyage; 9 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 10 | import se.citerus.dddsample.domain.model.voyage.VoyageRepository; 11 | import se.citerus.dddsample.interfaces.booking.facade.dto.LegDTO; 12 | import se.citerus.dddsample.interfaces.booking.facade.dto.RouteCandidateDTO; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * Assembler class for the ItineraryCandidateDTO. 19 | */ 20 | public class ItineraryCandidateDTOAssembler { 21 | 22 | /** 23 | * @param itinerary itinerary 24 | * @return A route candidate DTO 25 | */ 26 | public RouteCandidateDTO toDTO(final Itinerary itinerary) { 27 | final List legDTOs = new ArrayList(itinerary.legs().size()); 28 | for (Leg leg : itinerary.legs()) { 29 | legDTOs.add(toLegDTO(leg)); 30 | } 31 | return new RouteCandidateDTO(legDTOs); 32 | } 33 | 34 | /** 35 | * @param leg leg 36 | * @return A leg DTO 37 | */ 38 | protected LegDTO toLegDTO(final Leg leg) { 39 | final VoyageNumber voyageNumber = leg.voyage().voyageNumber(); 40 | final UnLocode from = leg.loadLocation().unLocode(); 41 | final UnLocode to = leg.unloadLocation().unLocode(); 42 | return new LegDTO(voyageNumber.idString(), from.idString(), to.idString(), leg.loadTime(), leg.unloadTime()); 43 | } 44 | 45 | /** 46 | * @param routeCandidateDTO route candidate DTO 47 | * @param voyageRepository voyage repository 48 | * @param locationRepository location repository 49 | * @return An itinerary 50 | */ 51 | public Itinerary fromDTO(final RouteCandidateDTO routeCandidateDTO, 52 | final VoyageRepository voyageRepository, 53 | final LocationRepository locationRepository) { 54 | final List legs = new ArrayList(routeCandidateDTO.getLegs().size()); 55 | for (LegDTO legDTO : routeCandidateDTO.getLegs()) { 56 | final VoyageNumber voyageNumber = new VoyageNumber(legDTO.getVoyageNumber()); 57 | final Voyage voyage = voyageRepository.find(voyageNumber); 58 | final Location from = locationRepository.find(new UnLocode(legDTO.getFrom())); 59 | final Location to = locationRepository.find(new UnLocode(legDTO.getTo())); 60 | legs.add(new Leg(voyage, from, to, legDTO.getLoadTime(), legDTO.getUnloadTime())); 61 | } 62 | return new Itinerary(legs); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/internal/assembler/LocationDTOAssembler.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.internal.assembler; 2 | 3 | import se.citerus.dddsample.domain.model.location.Location; 4 | import se.citerus.dddsample.interfaces.booking.facade.dto.LocationDTO; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class LocationDTOAssembler { 10 | 11 | public LocationDTO toDTO(Location location) { 12 | return new LocationDTO(location.unLocode().idString(), location.name()); 13 | } 14 | 15 | public List toDTOList(List allLocations) { 16 | final List dtoList = new ArrayList(allLocations.size()); 17 | for (Location location : allLocations) { 18 | dtoList.add(toDTO(location)); 19 | } 20 | return dtoList; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/internal/assembler/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | DTO assemblers. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/internal/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Internal parts of the remote facade API. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/facade/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Remote service facades with supporting DTO classes and assemblers. Sits on top of the domain service 5 | layer, and forms the boundary of the O/R-mapper unit-of-work scope when sending data to the user interface. 6 |

7 | 8 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/web/RegistrationCommand.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.web; 2 | 3 | /** 4 | * 5 | */ 6 | public final class RegistrationCommand { 7 | 8 | private String originUnlocode; 9 | private String destinationUnlocode; 10 | private String arrivalDeadline; 11 | 12 | public String getOriginUnlocode() { 13 | return originUnlocode; 14 | } 15 | 16 | public void setOriginUnlocode(final String originUnlocode) { 17 | this.originUnlocode = originUnlocode; 18 | } 19 | 20 | public String getDestinationUnlocode() { 21 | return destinationUnlocode; 22 | } 23 | 24 | public void setDestinationUnlocode(final String destinationUnlocode) { 25 | this.destinationUnlocode = destinationUnlocode; 26 | } 27 | 28 | public String getArrivalDeadline() { 29 | return arrivalDeadline; 30 | } 31 | 32 | public void setArrivalDeadline(String arrivalDeadline) { 33 | this.arrivalDeadline = arrivalDeadline; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/web/RouteAssignmentCommand.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.web; 2 | 3 | import java.time.Instant; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class RouteAssignmentCommand { 8 | 9 | private String trackingId; 10 | private List legs = new ArrayList<>(); 11 | 12 | public String getTrackingId() { 13 | return trackingId; 14 | } 15 | 16 | public void setTrackingId(String trackingId) { 17 | this.trackingId = trackingId; 18 | } 19 | 20 | public List getLegs() { 21 | return legs; 22 | } 23 | 24 | public void setLegs(List legs) { 25 | this.legs = legs; 26 | } 27 | 28 | public static final class LegCommand { 29 | private String voyageNumber; 30 | private String fromUnLocode; 31 | private String toUnLocode; 32 | private Instant fromDate; 33 | private Instant toDate; 34 | 35 | public String getVoyageNumber() { 36 | return voyageNumber; 37 | } 38 | 39 | public void setVoyageNumber(final String voyageNumber) { 40 | this.voyageNumber = voyageNumber; 41 | } 42 | 43 | public String getFromUnLocode() { 44 | return fromUnLocode; 45 | } 46 | 47 | public void setFromUnLocode(final String fromUnLocode) { 48 | this.fromUnLocode = fromUnLocode; 49 | } 50 | 51 | public String getToUnLocode() { 52 | return toUnLocode; 53 | } 54 | 55 | public void setToUnLocode(final String toUnLocode) { 56 | this.toUnLocode = toUnLocode; 57 | } 58 | 59 | public Instant getFromDate() { 60 | return fromDate; 61 | } 62 | 63 | public void setFromDate(String fromDate) { 64 | this.fromDate = Instant.parse(fromDate); 65 | } 66 | 67 | public Instant getToDate() { 68 | return toDate; 69 | } 70 | 71 | public void setToDate(String toDate) { 72 | this.toDate = Instant.parse(toDate); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/booking/web/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Web user interfaces for booking, routing and re-routing cargo. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/HandlingEventRegistrationAttempt.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.handling; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | import org.apache.commons.lang3.builder.ToStringStyle; 5 | import org.springframework.lang.NonNull; 6 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 7 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 8 | import se.citerus.dddsample.domain.model.location.UnLocode; 9 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 10 | 11 | import java.io.Serializable; 12 | import java.time.Instant; 13 | 14 | /** 15 | * This is a simple transfer object for passing incoming handling event 16 | * registration attempts to proper the registration procedure. 17 | *

18 | * It is used as a message queue element. 19 | */ 20 | public final class HandlingEventRegistrationAttempt implements Serializable { 21 | 22 | private final Instant registrationTime; 23 | private final Instant completionTime; 24 | private final TrackingId trackingId; 25 | private final VoyageNumber voyageNumber; 26 | private final HandlingEvent.Type type; 27 | private final UnLocode unLocode; 28 | 29 | public HandlingEventRegistrationAttempt(@NonNull Instant registrationTime, 30 | @NonNull Instant completionTime, 31 | @NonNull TrackingId trackingId, 32 | @NonNull VoyageNumber voyageNumber, 33 | @NonNull HandlingEvent.Type type, 34 | @NonNull UnLocode unLocode) { 35 | this.registrationTime = registrationTime; 36 | this.completionTime = completionTime; 37 | this.trackingId = trackingId; 38 | this.voyageNumber = voyageNumber; 39 | this.type = type; 40 | this.unLocode = unLocode; 41 | } 42 | 43 | public Instant getCompletionTime() { 44 | return completionTime; 45 | } 46 | 47 | public TrackingId getTrackingId() { 48 | return trackingId; 49 | } 50 | 51 | public VoyageNumber getVoyageNumber() { 52 | return voyageNumber; 53 | } 54 | 55 | public HandlingEvent.Type getType() { 56 | return type; 57 | } 58 | 59 | public UnLocode getUnLocode() { 60 | return unLocode; 61 | } 62 | 63 | public Instant getRegistrationTime() { 64 | return registrationTime; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/file/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Handles event registration by file upload. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Interfaces for receiving handling events into the system. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/ws/HandlingReport.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.handling.ws; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | 8 | public class HandlingReport { 9 | @JsonProperty(required = true) 10 | public LocalDateTime completionTime; 11 | 12 | @JsonProperty(required = true) 13 | public List trackingIds; 14 | 15 | @JsonProperty(required = true) 16 | public String type; 17 | 18 | @JsonProperty(required = true) 19 | public String unLocode; 20 | 21 | public String voyageNumber; 22 | 23 | public HandlingReport(LocalDateTime completionTime, List trackingIds, String type, String unLocode, String voyageNumber) { 24 | this.completionTime = completionTime; 25 | this.trackingIds = trackingIds; 26 | this.type = type; 27 | this.unLocode = unLocode; 28 | this.voyageNumber = voyageNumber; 29 | } 30 | 31 | public LocalDateTime getCompletionTime() { 32 | return completionTime; 33 | } 34 | 35 | public void setCompletionTime(LocalDateTime completionTime) { 36 | this.completionTime = completionTime; 37 | } 38 | 39 | public List getTrackingIds() { 40 | return trackingIds; 41 | } 42 | 43 | public void setTrackingIds(List trackingIds) { 44 | this.trackingIds = trackingIds; 45 | } 46 | 47 | public String getType() { 48 | return type; 49 | } 50 | 51 | public void setType(String type) { 52 | this.type = type; 53 | } 54 | 55 | public String getUnLocode() { 56 | return unLocode; 57 | } 58 | 59 | public void setUnLocode(String unLocode) { 60 | this.unLocode = unLocode; 61 | } 62 | 63 | public String getVoyageNumber() { 64 | return voyageNumber; 65 | } 66 | 67 | public void setVoyageNumber(String voyageNumber) { 68 | this.voyageNumber = voyageNumber; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/ws/HandlingReportService.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.handling.ws; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | 5 | public interface HandlingReportService { 6 | 7 | ResponseEntity submitReport(HandlingReport handlingReport); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/ws/HandlingReportServiceImpl.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.handling.ws; 2 | 3 | import jakarta.validation.Valid; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import se.citerus.dddsample.application.ApplicationEvents; 11 | import se.citerus.dddsample.interfaces.handling.HandlingEventRegistrationAttempt; 12 | 13 | import java.lang.invoke.MethodHandles; 14 | import java.util.List; 15 | 16 | import static org.springframework.http.HttpStatus.CREATED; 17 | import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; 18 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 19 | import static se.citerus.dddsample.interfaces.handling.HandlingReportParser.parse; 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 information to the handling event registration system for proper registration. 25 | */ 26 | @RestController 27 | public class HandlingReportServiceImpl implements HandlingReportService { 28 | private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 29 | 30 | private final ApplicationEvents applicationEvents; 31 | 32 | public HandlingReportServiceImpl(ApplicationEvents applicationEvents) { 33 | this.applicationEvents = applicationEvents; 34 | } 35 | 36 | @PostMapping(value = "/handlingReport", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE) 37 | @Override 38 | public ResponseEntity submitReport(@Valid @RequestBody HandlingReport handlingReport) { 39 | try { 40 | List attempts = parse(handlingReport); 41 | attempts.forEach(applicationEvents::receivedHandlingEventRegistrationAttempt); 42 | } catch (Exception e) { 43 | logger.error("Unexpected error in submitReport", e); 44 | return ResponseEntity.status(INTERNAL_SERVER_ERROR).body("Internal server error: " + e.getMessage()); 45 | } 46 | return ResponseEntity.status(CREATED).build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/handling/ws/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Web service interface for registering handling events. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/tracking/TrackCommand.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking; 2 | 3 | import org.apache.commons.lang3.builder.ToStringBuilder; 4 | 5 | import static org.apache.commons.lang3.builder.ToStringStyle.MULTI_LINE_STYLE; 6 | 7 | public final class TrackCommand { 8 | 9 | /** 10 | * The tracking id. 11 | */ 12 | private String trackingId; 13 | 14 | public String getTrackingId() { 15 | return trackingId; 16 | } 17 | 18 | public void setTrackingId(final String trackingId) { 19 | this.trackingId = trackingId; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return ToStringBuilder.reflectionToString(this, MULTI_LINE_STYLE); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/tracking/TrackCommandValidator.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking; 2 | 3 | import org.springframework.lang.NonNull; 4 | import org.springframework.validation.Errors; 5 | import org.springframework.validation.ValidationUtils; 6 | import org.springframework.validation.Validator; 7 | 8 | /** 9 | * Validator for {@link TrackCommand}s. 10 | */ 11 | public final class TrackCommandValidator implements Validator { 12 | 13 | public boolean supports(@NonNull final Class clazz) { 14 | return TrackCommand.class.isAssignableFrom(clazz); 15 | } 16 | 17 | public void validate(@NonNull final Object object,@NonNull final Errors errors) { 18 | ValidationUtils.rejectIfEmptyOrWhitespace(errors, "trackingId", "error.required", "Required"); 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/tracking/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Public tracking web interface. 5 |

6 | 7 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/tracking/ws/CargoTrackingDTO.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking.ws; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * A data-transport object class representing a view of a Cargo entity. 7 | * Used by the REST API for public cargo tracking. 8 | */ 9 | public class CargoTrackingDTO { 10 | 11 | public final String trackingId; 12 | public final String statusText; 13 | public final String destination; 14 | public final String eta; 15 | public final String nextExpectedActivity; 16 | public final boolean isMisdirected; 17 | public final List handlingEvents; 18 | 19 | public CargoTrackingDTO(String trackingId, String statusText, String destination, String eta, String nextExpectedActivity, boolean isMisdirected, List handlingEvents) { 20 | this.trackingId = trackingId; 21 | this.statusText = statusText; 22 | this.destination = destination; 23 | this.eta = eta; 24 | this.nextExpectedActivity = nextExpectedActivity; 25 | this.isMisdirected = isMisdirected; 26 | this.handlingEvents = handlingEvents; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/tracking/ws/CargoTrackingRestService.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking.ws; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.context.MessageSource; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import org.springframework.web.servlet.support.RequestContextUtils; 12 | import org.springframework.web.util.UriTemplate; 13 | import se.citerus.dddsample.domain.model.cargo.Cargo; 14 | import se.citerus.dddsample.domain.model.cargo.CargoRepository; 15 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 16 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 17 | import se.citerus.dddsample.domain.model.handling.HandlingEventRepository; 18 | 19 | import java.lang.invoke.MethodHandles; 20 | import java.net.URI; 21 | import java.util.List; 22 | import java.util.Locale; 23 | 24 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 25 | 26 | @RestController 27 | public class CargoTrackingRestService { 28 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 29 | 30 | private final CargoRepository cargoRepository; 31 | private final HandlingEventRepository handlingEventRepository; 32 | private final MessageSource messageSource; 33 | 34 | public CargoTrackingRestService(CargoRepository cargoRepository, HandlingEventRepository handlingEventRepository, MessageSource messageSource) { 35 | this.cargoRepository = cargoRepository; 36 | this.handlingEventRepository = handlingEventRepository; 37 | this.messageSource = messageSource; 38 | } 39 | 40 | @GetMapping(value = "/api/track/{trackingId}", produces = APPLICATION_JSON_VALUE) 41 | public ResponseEntity trackCargo(final HttpServletRequest request, 42 | @PathVariable("trackingId") String trackingId) { 43 | try { 44 | Locale locale = RequestContextUtils.getLocale(request); 45 | TrackingId trkId = new TrackingId(trackingId); 46 | Cargo cargo = cargoRepository.find(trkId); 47 | if (cargo == null) { 48 | URI uri = new UriTemplate(request.getContextPath() + "/api/track/{trackingId}").expand(trackingId); 49 | return ResponseEntity.notFound().location(uri).build(); 50 | } 51 | final List handlingEvents = handlingEventRepository.lookupHandlingHistoryOfCargo(trkId) 52 | .distinctEventsByCompletionTime(); 53 | return ResponseEntity.ok(CargoTrackingDTOConverter.convert(cargo, handlingEvents, messageSource, locale)); 54 | } catch (Exception e) { 55 | log.error("Unexpected error in trackCargo endpoint", e); 56 | return ResponseEntity.status(500).build(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/se/citerus/dddsample/interfaces/tracking/ws/HandlingEventDTO.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking.ws; 2 | 3 | /** 4 | * A data-transport object class represnting a view of a HandlingEvent owned by a Cargo. 5 | * Used by the REST API for public cargo tracking. 6 | */ 7 | public class HandlingEventDTO { 8 | 9 | public final String location; 10 | public final String time; 11 | public final String type; 12 | public final String voyageNumber; 13 | public final boolean isExpected; 14 | public final String description; 15 | 16 | public HandlingEventDTO(String location, String time, String type, String voyageNumber, boolean isExpected, String description) { 17 | this.location = location; 18 | this.time = time; 19 | this.type = type; 20 | this.voyageNumber = voyageNumber; 21 | this.isExpected = isExpected; 22 | this.description = description; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | servlet: 3 | context-path: /dddsample 4 | 5 | logging: 6 | level: 7 | jdbc: debug 8 | 'se.citerus.dddsample': debug 9 | 10 | uploadDirectory: /tmp/upload 11 | parseFailureDirectory: /tmp/failed 12 | 13 | brokerUrl: "vm://localhost?broker.persistent=false&broker.useJmx=false" 14 | 15 | spring: 16 | dataSource: 17 | url: jdbc:hsqldb:mem:dddsample 18 | username: sa 19 | password: "" 20 | 21 | jdbc: 22 | datasource-proxy: 23 | enabled: true 24 | query: 25 | logger-name: jdbc 26 | enable-logging: true 27 | multiline: false 28 | include-parameter-values: true 29 | -------------------------------------------------------------------------------- /src/main/resources/messages_en.properties: -------------------------------------------------------------------------------- 1 | cargo.status.NOT_RECEIVED=Not received 2 | cargo.status.IN_PORT=In port {0} 3 | cargo.status.ONBOARD_CARRIER=Onboard voyage {0} 4 | cargo.status.CLAIMED=Claimed 5 | cargo.status.UNKNOWN=Unknown 6 | 7 | deliveryHistory.eventDescription.NOT_RECEIVED=Cargo has not yet been received. 8 | 9 | deliveryHistory.eventDescription.LOAD=Loaded onto voyage {0} in {1}, at {2}. 10 | deliveryHistory.eventDescription.UNLOAD=Unloaded off voyage {0} in {1}, at {2}. 11 | deliveryHistory.eventDescription.RECEIVE=Received in {0}, at {1}. 12 | deliveryHistory.eventDescription.CLAIM=Claimed in {0}, at {1}. 13 | deliveryHistory.eventDescription.CUSTOMS=Cleared customs in {0}, at {1}. -------------------------------------------------------------------------------- /src/main/resources/static/admin.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font: Verdana, Arial, sans-serif; 7 | color: black; 8 | padding: 10px; 9 | } 10 | 11 | h1 { 12 | font-weight: lighter; 13 | font-variant: small-caps; 14 | } 15 | 16 | ul#menu { 17 | border: 1px solid #ccc; 18 | margin: 40px 0 20px 0; 19 | padding: 10px 0; 20 | background: #eee; 21 | } 22 | 23 | ul#menu li { 24 | margin: 0 0 0 10px; 25 | display: inline; 26 | } 27 | 28 | ul#menu a { 29 | text-decoration: none; 30 | } 31 | 32 | ul#menu a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | table { 37 | } 38 | 39 | table td { 40 | padding: 4px 10px; 41 | } 42 | 43 | table thead { 44 | font-weight: bold; 45 | } 46 | 47 | table caption { 48 | text-align: left; 49 | font-weight: bold; 50 | } 51 | 52 | img#logotype { 53 | float: right; 54 | } 55 | -------------------------------------------------------------------------------- /src/main/resources/static/calendar.css: -------------------------------------------------------------------------------- 1 | .calendarTrigger { 2 | width: 16px; 3 | height: 16px; 4 | cursor: pointer; 5 | vertical-align: top; 6 | margin-top: 1px; 7 | } 8 | 9 | .calendar { 10 | position: absolute; 11 | display: inherit; 12 | 13 | font-family: tahoma, verdana, arial, helvetica, sans-serif; 14 | border: 1px solid blue; 15 | background-color: white; 16 | } 17 | 18 | 19 | .calendar table { 20 | border: 4px solid lightblue; 21 | margin: 0px; 22 | padding: 0px; 23 | border-spacing: 2px; 24 | border-collapse: separate; 25 | } 26 | 27 | .calendar table .goPrevious, 28 | .calendar table .goNext { 29 | width: 12px; 30 | background-repeat: no-repeat; 31 | cursor: pointer; 32 | } 33 | 34 | .calendar table .goPrevious { 35 | background-image: url( 'navigate_left.gif' ); 36 | background-position: left center; 37 | } 38 | 39 | .calendar table .goNext { 40 | background-image: url( 'navigate_right.gif' ); 41 | background-position: right center; 42 | } 43 | 44 | .calendar table thead tr:first-child td { 45 | font-weight: bold; 46 | } 47 | 48 | .calendar table td { 49 | width: 15px; 50 | height: 15px; 51 | font-family: tahoma, verdana, arial, helvetica, sans-serif; 52 | font-size: 8pt; 53 | text-align: center; 54 | padding: 0px 0px 0px 1px; 55 | border: 0px; 56 | vertical-align: middle; 57 | } 58 | .calendar table tbody td { 59 | border: 1px solid gray; 60 | cursor: pointer; 61 | } 62 | .calendar .disabled { 63 | background-color: #BBBBBB; 64 | color: #555555; 65 | } 66 | .calendar .past { 67 | color: #AAAAAA; 68 | } 69 | .calendar .future { 70 | } 71 | .calendar .today { 72 | background-color: pink; 73 | } 74 | .calendar .active { 75 | background-color: #FFAA00; 76 | border: 1px solid black; 77 | } 78 | 79 | .calendar table tbody td:hover { 80 | border: 1px solid blue; 81 | background-color: lightblue; 82 | } 83 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/images/calendarTrigger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/calendarTrigger.gif -------------------------------------------------------------------------------- /src/main/resources/static/images/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/cross.png -------------------------------------------------------------------------------- /src/main/resources/static/images/dddsample_logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/dddsample_logotype.png -------------------------------------------------------------------------------- /src/main/resources/static/images/dddsample_logotype_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/dddsample_logotype_small.png -------------------------------------------------------------------------------- /src/main/resources/static/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/error.png -------------------------------------------------------------------------------- /src/main/resources/static/images/shade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/shade.png -------------------------------------------------------------------------------- /src/main/resources/static/images/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/tick.png -------------------------------------------------------------------------------- /src/main/resources/static/images/web_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/main/resources/static/images/web_logo.png -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DDDSample 7 | 8 | 9 |

10 |

Welcome to the DDDSample application.

11 |

There are two web interfaces available:

12 | 16 |

The Incident Logging application, that is used to register handling events, is a stand-alone application and a separate download.

17 |

Please visit the project website for more information and a screencast demonstration of how the application works.

18 |

This project is a joint effort by Eric Evans' company Domain Language 19 | and the Swedish software consulting company Citerus.

20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Cargo Administration 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 |
All cargos
Tracking IDOriginDestinationRouted
28 | Tracking id 30 | OriginFinal destination
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin/pickNewDestination.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Cargo Administration 8 | 9 | 10 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 |
Change destination for cargo
Current destination 28 |
New destination 32 | 35 |
42 | 43 |
47 |
48 |
49 | 50 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin/registrationForm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Cargo Administration 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 |
Book new cargo
Origin 23 | 28 |
Destination 33 | 38 |
Arrival deadline: 43 | 44 |

45 |
52 | 53 |
57 |
58 |
59 | 60 | 61 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin/selectItinerary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Cargo Administration 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 20 | 21 |
Select route
17 | Cargo is going from to 19 |
22 | 23 |

No routes found that satisfy the route specification. 24 | Try setting an arrival deadline futher into the future (a few weeks at least). 25 |

26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 50 | 51 | 52 | 53 | 55 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | 68 |
Route candidate
VoyageFromTo
54 | 56 |
62 |

63 | 64 |

65 |
69 |
70 | 71 |
72 | 73 | -------------------------------------------------------------------------------- /src/main/resources/templates/admin/show.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | Cargo Administration 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 35 | 36 |
Origin
Destination
28 | Change destination 29 |
Arrival deadline 34 |
37 |

38 | 39 | 40 |

Cargo is misrouted - reroute 41 | this cargo

42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 60 | 61 |
Itinerary
Voyage numberLoadUnload
57 | 59 |
62 |
63 | 64 |

65 | Not routed - Route this cargo 66 |

67 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /src/main/resources/templates/adminDecorator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 |

Cargo Booking and Routing

10 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /src/site/apt/changelog.apt: -------------------------------------------------------------------------------- 1 | --------- 2 | Changelog 3 | --------- 4 | 5 | Changelog 6 | 7 | * 1.1 (March 2009) 8 | 9 | * Remodeled Cargo and HandlingEvent aggregates to make boundaries 10 | and asynchronous updates more explicit. Cargo delivery progress is now 11 | inspected and stored on handling, asynchronously, as an example of 12 | how aggregates may be inconsistent for a (short) period of time. 13 | 14 | * Restructured packages to have a one-to-one mapping between layers and packages. 15 | 16 | * Gathered all aspects of the cargo delivery in the Delivery value object 17 | in the Cargo aggregate. 18 | 19 | * RouteSpecification is now a persistent part of the Cargo aggregate. 20 | 21 | * Made handling event registration completely asynchronous. 22 | 23 | * Implemented "next expected event" for tracking. 24 | 25 | * The old CarrierMovement aggregate is remodeled, and the root entity 26 | is now Voyage. A voyage has a schedule containing a list of carrier 27 | movements. 28 | 29 | * Legs and carrier movements have departure/arrival and load/unload times, respectively. 30 | 31 | * ETA for itineraries 32 | 33 | * Web service for registering handling events is now more clearly 34 | downstream from handling report aggregation service context, 35 | with Java code generated from a given WSDL. 36 | 37 | * There is now a directory scanner routine that picks up and parses CSV 38 | files containing handling event information, as an example of an alternative 39 | interface. 40 | 41 | * Updated framework dependencies. 42 | 43 | * Screencast showing what the application can do. 44 | 45 | * 1.0 (September 2008) 46 | 47 | * Initial public release 48 | 49 | -------------------------------------------------------------------------------- /src/site/apt/handlingEventRegistration.apt: -------------------------------------------------------------------------------- 1 | -------------------------------- 2 | Registration of a handling event 3 | -------------------------------- 4 | 5 | TODO 6 | -------------------------------------------------------------------------------- /src/site/apt/index.apt: -------------------------------------------------------------------------------- 1 | ------------ 2 | Introduction 3 | ------------ 4 | 5 | 6 | 7 | [images/frontpage.jpeg] 8 | 9 | One of the most requested aids to coming up to speed on DDD has been a 10 | running example application. Starting from a simple set of functions 11 | and a model based on the cargo example used in {{{http://www.domaindrivendesign.org/books/index.html#DDD}Eric Evans' book}}, 12 | we have built a running application with which to demonstrate a practical 13 | implementation of the building block patterns as well as illustrate the 14 | impact of aggregates and bounded contexts. 15 | 16 | News 17 | 18 | * 2009-03-25: New public release: <<1.1.0>>. See {{{changelog.html}changelog}} for details. 19 | 20 | * 2009-03-09: Sample application tutorial at the {{{http://qconlondon.com/london-2009/}QCon conference}} in London. 21 | 22 | * 2009-01-27: Sample application tutorial at the {{{http://www.jfokus.se/jfokus/}JFokus conference}} in Stockholm. 23 | 24 | * 2008-09-15: First public release: <<1.0>>. 25 | 26 | Purpose 27 | 28 | * A how-to example for implementing a typical DDD application 29 | 30 | Our sample does not show *the* way to do it, but a decent way. 31 | Eventually, the same design could be reimplemented on various popular platforms, 32 | to give the same assistance to people working on those platforms, 33 | and also help those who must transition between the platforms. 34 | 35 | * Support discussion of implementation practices 36 | 37 | Variations could show trade-offs of alternative approaches, 38 | helping the community to clarify and refine best practices for building DDD applications. 39 | 40 | * Lab mouse for controlled experiments 41 | 42 | There's a lot of interest in new tools or frameworks for DDD. 43 | 44 | Reimplementing the sample app using a new platform will give a side-by-side comparison 45 | with the "conventional" implementation, which can demonstrate the value of the new platform 46 | and provide validation or other feedback to the developers of the platform. 47 | 48 | Caveats 49 | 50 | Domain-driven design is a very broad topic, and contains lots of things that are difficult or impossible 51 | to incorporate into the code base of a sample application. Perhaps most important is communication with the domain 52 | expert, iterative modelling and the discovery of a ubiquitous language. This application is a snapshot in time, 53 | the result of a development effort that you need to imagine has been utilizing domain-driven design, 54 | to show how one can structure an application around an isolated, rich domain model in a realistic environment. 55 | 56 | [] 57 | 58 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/site/apt/patterns-reference.apt: -------------------------------------------------------------------------------- 1 | ------------------------------------ Patterns reference ------------------------------------ Patrik Fredriksson ------------------------------------ March 11, 2009 Patterns reference In {{{http://www.domaindrivendesign.org/books/index.html#DDD}Eric Evans' book}} a number of patterns on Domain-Driven Design are presented. Many of these patterns are implemented in the sample application. Use this patterns reference to find out which patterns are implemented where! A patterns summary can be downloaded at {{{http://www.domaindrivendesign.org/discussion/index.html}domaindrivendesign.org}}. Tactical Design Patterns *Building Blocks of a Model-Driven Design Aggregate [{{{characterization.html#Aggregates}discussion}}] [{{{xref/se/citerus/dddsample/domain/model/cargo/package-summary.html}code example}}] Domain Event [{{{characterization.html#Domain_Event}discussion}}] [{{{xref/se/citerus/dddsample/domain/model/handling/HandlingEvent.html}code example}}] Entity [{{{characterization.html#Entities}discussion}}] [{{{xref/se/citerus/dddsample/domain/model/cargo/Cargo.html}code example}}] Value Object [{{{characterization.html#Value_Objects}discussion}}] [{{{xref/se/citerus/dddsample/domain/model/cargo/Leg.html}code example}}] Repository [{{{characterization.html#Repositories}discussion}}] [{{{xref/se/citerus/dddsample/domain/model/cargo/CargoRepository.html}code example}}] Service [{{{characterization.html#Services}discussion}}] [{{{xref/se/citerus/dddsample/domain/service/RoutingService.html}code example}}] Specification [{{{xref/se/citerus/dddsample/domain/model/cargo/RouteSpecification.html}code example}}] Layered Architecture [discussion] Service Layer [{{{characterization.html#Services}discussion}}] *Supple Design Intention-Revealing Interfaces [discussion] [{{{xref/se/citerus/dddsample/domain/model/cargo/Cargo.html}code example}}] Side-Effect-Free Functions [discussion] [{{{xref/se/citerus/dddsample/domain/model/cargo/Cargo.html}code example}}] Strategic Design Patterns *Maintaining Model Integrity Anti-corruption Layer [discussion] [{{{xref/se/citerus/dddsample/interfaces/handling/ws/HandlingReportServiceImpl.html}code example}}] -------------------------------------------------------------------------------- /src/site/apt/roadmap.apt: -------------------------------------------------------------------------------- 1 | ------- 2 | Roadmap 3 | ------- 4 | 5 | Roadmap 6 | 7 | This application is a work of progress, and these are some ideas for continued development: 8 | 9 | * Ports to C#/.NET and other frameworks 10 | 11 | * Handling voyage delays and intentional rescheduling, how that affects itineraries 12 | 13 | * More one the time aspect: timezones for locations and handling events, 14 | notify on delayed delivery, port to {{{http://timeandmoney.sourceforge.net/}TimeAndMoney}} or similar date/time framework -------------------------------------------------------------------------------- /src/site/resources/css/site.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/css/site.css -------------------------------------------------------------------------------- /src/site/resources/images/banner-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/banner-left.png -------------------------------------------------------------------------------- /src/site/resources/images/eclipse1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/eclipse1.png -------------------------------------------------------------------------------- /src/site/resources/images/eclipse2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/eclipse2.png -------------------------------------------------------------------------------- /src/site/resources/images/eclipse3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/eclipse3.png -------------------------------------------------------------------------------- /src/site/resources/images/eclipse4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/eclipse4.png -------------------------------------------------------------------------------- /src/site/resources/images/frontpage.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/frontpage.jpeg -------------------------------------------------------------------------------- /src/site/resources/images/layers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citerus/dddsample-core/d8b896254ed4e18ba8117dcd5bf9a54708bdeaaf/src/site/resources/images/layers.jpg -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | org.apache.maven.skins 4 | maven-fluido-skin 5 | 2.1.0 6 | 7 | 8 | DDDSample 9 | images/banner-left.png 10 | http://dddsample.sf.net 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/site/xdoc/screencast.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Screencast 8 | 9 | 10 | 11 |
12 |

13 | Here is a 10 minute screencast that shows the different interfaces in action, 14 | and the scope of the application. 15 |

16 |

It shows:

17 |
    18 |
  • The welcome page
  • 19 |
  • Tracking the preloaded cargos
  • 20 |
  • Viewing the preloaded cargos in the admin application
  • 21 |
  • Booking of a new cargo
  • 22 |
  • Routing of the new cargo
  • 23 |
  • Registering of a few expected events for the new cargo, 24 | then tracking to confirm it's on track
  • 25 |
  • Registering of an unexpected event for the new cargo, 26 | then tracking to see that it becomes misdirected
  • 27 |
  • Registering event using the scheduled file import interface
  • 28 |
29 |

30 | It's a little difficult to read the text, but you can use it as a guide for 31 | playing around on your own with the actual application.

32 | 33 | 34 | 35 | 36 | 38 | 39 |

40 | The recording was made using IShowU HD. 41 |

42 |
43 | 44 | 45 |
-------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/AbstractAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.openqa.selenium.WebDriver; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.server.LocalServerPort; 10 | import org.springframework.test.context.junit.jupiter.SpringExtension; 11 | import org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder; 12 | import org.springframework.web.context.WebApplicationContext; 13 | import se.citerus.dddsample.Application; 14 | 15 | @ExtendWith(SpringExtension.class) 16 | @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 17 | properties = {"spring.datasource.url=jdbc:hsqldb:mem:dddsample_acceptance_test"}) 18 | public abstract class AbstractAcceptanceTest { 19 | 20 | @Autowired 21 | private WebApplicationContext context; 22 | 23 | protected WebDriver driver; 24 | 25 | @LocalServerPort 26 | public int port; 27 | 28 | @BeforeEach 29 | public void setup() { 30 | driver = MockMvcHtmlUnitDriverBuilder.webAppContextSetup(context).contextPath("/dddsample").build(); 31 | } 32 | 33 | @AfterEach 34 | public void tearDown() { 35 | driver.quit(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/AdminAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.test.annotation.DirtiesContext; 5 | import se.citerus.dddsample.acceptance.pages.*; 6 | 7 | import java.time.LocalDate; 8 | import java.time.temporal.ChronoUnit; 9 | 10 | import static org.assertj.core.api.Java6Assertions.assertThat; 11 | 12 | public class AdminAcceptanceTest extends AbstractAcceptanceTest { 13 | 14 | @DirtiesContext 15 | @Test 16 | public void adminSiteCargoListContainsCannedCargo() { 17 | AdminPage page = new AdminPage(driver, port); 18 | page.listAllCargo(); 19 | 20 | assertThat(page.listedCargoContains("ABC123")).isTrue() 21 | .withFailMessage("Cargo list doesn't contain ABC123"); 22 | assertThat(page.listedCargoContains("JKL567")).isTrue() 23 | .withFailMessage("Cargo list doesn't contain JKL567"); 24 | } 25 | 26 | @DirtiesContext 27 | @Test 28 | public void adminSiteCanBookNewCargo() { 29 | AdminPage adminPage = new AdminPage(driver, port); 30 | 31 | CargoBookingPage cargoBookingPage = adminPage.bookNewCargo(); 32 | cargoBookingPage.selectOrigin("NLRTM"); 33 | cargoBookingPage.selectDestination("USDAL"); 34 | LocalDate arrivalDeadline = LocalDate.now().plus(3, ChronoUnit.WEEKS); 35 | cargoBookingPage.selectArrivalDeadline(arrivalDeadline); 36 | CargoDetailsPage cargoDetailsPage = cargoBookingPage.book(); 37 | 38 | String newCargoTrackingId = cargoDetailsPage.getTrackingId(); 39 | adminPage = cargoDetailsPage.listAllCargo(); 40 | assertThat(adminPage.listedCargoContains(newCargoTrackingId)).isTrue() 41 | .withFailMessage("Cargo list doesn't contain %s", newCargoTrackingId); 42 | 43 | cargoDetailsPage = adminPage.showDetailsFor(newCargoTrackingId); 44 | cargoDetailsPage.expectOriginOf("NLRTM"); 45 | cargoDetailsPage.expectDestinationOf("USDAL"); 46 | 47 | CargoDestinationPage cargoDestinationPage = cargoDetailsPage.changeDestination(); 48 | cargoDetailsPage = cargoDestinationPage.selectDestinationTo("AUMEL"); 49 | cargoDetailsPage.expectDestinationOf("AUMEL"); 50 | cargoDetailsPage.expectArrivalDeadlineOf(arrivalDeadline); 51 | 52 | // Route cargo 53 | cargoDetailsPage.expectRoutedOf("Not routed"); 54 | CargoRoutingPage cargoRoutingPage = cargoDetailsPage.routeCargo(); 55 | cargoRoutingPage.expectAtLeastOneRoute(); 56 | CargoDetailsPage routedCargoDetailsPage = cargoRoutingPage.assignCargoToFirstRoute(); 57 | routedCargoDetailsPage.expectItinerary(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/CustomerAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.test.annotation.DirtiesContext; 6 | import se.citerus.dddsample.acceptance.pages.CustomerPage; 7 | 8 | public class CustomerAcceptanceTest extends AbstractAcceptanceTest { 9 | private CustomerPage customerPage; 10 | 11 | @BeforeEach 12 | public void goToCustomerPage() { 13 | customerPage = new CustomerPage(driver, port); 14 | } 15 | 16 | @DirtiesContext 17 | @Test 18 | public void customerSiteCanTrackValidCargo() { 19 | customerPage.trackCargoWithIdOf("ABC123"); 20 | customerPage.expectCargoLocation("New York"); 21 | } 22 | 23 | @DirtiesContext 24 | @Test 25 | public void customerSiteErrorsOnInvalidCargo() { 26 | customerPage.trackCargoWithIdOf("XXX999"); 27 | customerPage.expectErrorFor("Unknown tracking id"); 28 | } 29 | 30 | @DirtiesContext 31 | @Test 32 | public void customerSiteNotifiesOnMisdirectedCargo() { 33 | customerPage.trackCargoWithIdOf("JKL567"); 34 | customerPage.expectNotificationOf("Cargo is misdirected"); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/pages/AdminPage.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class AdminPage { 13 | private final WebDriver driver; 14 | private final int port; 15 | 16 | public AdminPage(WebDriver driver, int port) { 17 | this.driver = driver; 18 | this.port = port; 19 | driver.get(String.format("http://localhost:%d/dddsample/admin/list", port)); 20 | assertThat("Cargo Administration").isEqualTo(driver.getTitle()); 21 | } 22 | 23 | public void listAllCargo() { 24 | driver.findElement(By.linkText("List all cargos")).click(); 25 | assertThat("Cargo Administration").isEqualTo(driver.getTitle()); 26 | } 27 | 28 | public CargoBookingPage bookNewCargo() { 29 | driver.findElement(By.linkText("Book new cargo")).click(); 30 | 31 | return new CargoBookingPage(driver, port); 32 | } 33 | 34 | public boolean listedCargoContains(String expectedTrackingId) { 35 | List cargoList = driver.findElements(By.cssSelector("#body table tbody tr td a")); 36 | Optional matchingCargo = cargoList.stream().filter(cargo -> cargo.getText().equals(expectedTrackingId)).findFirst(); 37 | return matchingCargo.isPresent(); 38 | } 39 | 40 | public CargoDetailsPage showDetailsFor(String cargoTrackingId) { 41 | driver.findElement(By.linkText(cargoTrackingId)).click(); 42 | 43 | return new CargoDetailsPage(driver, port); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/pages/CargoBookingPage.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.support.ui.Select; 7 | 8 | import java.time.LocalDate; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | public class CargoBookingPage { 14 | 15 | private final WebDriver driver; 16 | private final int port; 17 | 18 | public CargoBookingPage(WebDriver driver, int port) { 19 | this.driver = driver; 20 | this.port = port; 21 | 22 | WebElement newCargoTableCaption = driver.findElement(By.cssSelector("table caption")); 23 | 24 | assertThat("Book new cargo").isEqualTo(newCargoTableCaption.getText()); 25 | } 26 | 27 | public void selectOrigin(String origin) { 28 | Select select = new Select(driver.findElement(By.name("originUnlocode"))); 29 | select.selectByVisibleText(origin); 30 | } 31 | 32 | public void selectDestination(String destination) { 33 | Select select = new Select(driver.findElement(By.name("destinationUnlocode"))); 34 | select.selectByVisibleText(destination); 35 | } 36 | 37 | public CargoDetailsPage book() { 38 | driver.findElement(By.name("originUnlocode")).submit(); 39 | 40 | return new CargoDetailsPage(driver, port); 41 | } 42 | 43 | public void selectArrivalDeadline(LocalDate arrivalDeadline) { 44 | WebElement datePicker = driver.findElement(By.id("arrivalDeadline")); 45 | datePicker.clear(); 46 | datePicker.sendKeys(arrivalDeadline.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/pages/CargoDestinationPage.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.support.ui.Select; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class CargoDestinationPage { 11 | private final WebDriver driver; 12 | private final int port; 13 | 14 | public CargoDestinationPage(WebDriver driver, int port) { 15 | this.driver = driver; 16 | this.port = port; 17 | WebElement cargoDestinationHeader = driver.findElement(By.cssSelector("table caption")); 18 | 19 | assertThat(cargoDestinationHeader.getText()).startsWith("Change destination for cargo "); 20 | } 21 | 22 | public CargoDetailsPage selectDestinationTo(String destination) { 23 | WebElement destinationPicker = driver.findElement(By.name("unlocode")); 24 | Select select = new Select(destinationPicker); 25 | select.selectByVisibleText(destination); 26 | 27 | destinationPicker.submit(); 28 | 29 | CargoDetailsPage cargoDetailsPage = new CargoDetailsPage(driver, port); 30 | return cargoDetailsPage; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/pages/CargoDetailsPage.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import java.time.LocalDate; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class CargoDetailsPage { 14 | public static final String TRACKING_ID_HEADER = "Details for cargo "; 15 | public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy"); 16 | private final WebDriver driver; 17 | private final int port; 18 | private String trackingId; 19 | 20 | public CargoDetailsPage(WebDriver driver, int port) { 21 | this.driver = driver; 22 | this.port = port; 23 | 24 | WebElement newCargoTableCaption = driver.findElement(By.cssSelector("table caption")); 25 | 26 | assertThat(newCargoTableCaption.getText()).startsWith(TRACKING_ID_HEADER); 27 | trackingId = newCargoTableCaption.getText().replaceFirst(TRACKING_ID_HEADER, ""); 28 | } 29 | 30 | public String getTrackingId() { 31 | return trackingId; 32 | } 33 | 34 | public AdminPage listAllCargo() { 35 | driver.findElement(By.linkText("List all cargos")).click(); 36 | 37 | return new AdminPage(driver, port); 38 | } 39 | 40 | public void expectOriginOf(String expectedOrigin) { 41 | String actualOrigin = driver.findElement(By.xpath("//div[@id='container']/table/tbody/tr[1]/td[2]")).getText(); 42 | 43 | assertThat(expectedOrigin).isEqualTo(actualOrigin); 44 | } 45 | 46 | public void expectDestinationOf(String expectedDestination) { 47 | String actualDestination = driver.findElement(By.xpath("//div[@id='container']/table/tbody/tr[2]/td[2]")).getText(); 48 | 49 | assertThat(expectedDestination).isEqualTo(actualDestination); 50 | } 51 | 52 | public CargoDestinationPage changeDestination() { 53 | driver.findElement(By.linkText("Change destination")).click(); 54 | 55 | return new CargoDestinationPage(driver, port); 56 | } 57 | 58 | public void expectArrivalDeadlineOf(LocalDate expectedArrivalDeadline) { 59 | String actualArrivalDeadline = driver.findElement(By.xpath("//div[@id='container']/table/tbody/tr[4]/td[2]")).getText(); 60 | 61 | assertThat(actualArrivalDeadline).isEqualTo(expectedArrivalDeadline.format(FORMATTER)); 62 | } 63 | 64 | public void expectRoutedOf(String routingStatus) { 65 | String actualRoutingStatus = driver.findElement(By.xpath("//div[@id='container']/p[2]/strong")).getText(); 66 | 67 | assertEquals(routingStatus, actualRoutingStatus); 68 | } 69 | 70 | public CargoRoutingPage routeCargo() { 71 | driver.findElement(By.linkText("Route this cargo")).click(); 72 | 73 | return new CargoRoutingPage(driver, port); 74 | } 75 | 76 | public void expectItinerary() { 77 | String itineraryText = driver.findElement(By.xpath("//div[@id='container']/table[2]/caption")).getText(); 78 | assertEquals("Itinerary", itineraryText); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/pages/CargoRoutingPage.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class CargoRoutingPage { 11 | private WebDriver driver; 12 | private int port; 13 | 14 | public CargoRoutingPage (WebDriver driver, int port) { 15 | this.driver = driver; 16 | this.port = port; 17 | 18 | WebElement routingHeader = driver.findElement(By.xpath("//h1")); 19 | assertThat(routingHeader.getText().equals("Cargo Booking and Routing")); 20 | } 21 | 22 | public void expectAtLeastOneRoute() { 23 | WebElement assignRouteCaption = driver.findElement(By.cssSelector("form table caption")); 24 | assertEquals("Route candidate 1", assignRouteCaption.getText()); 25 | } 26 | 27 | public CargoDetailsPage assignCargoToFirstRoute() { 28 | WebElement assignCargoForm = driver.findElement(By.xpath("//div[@id='container']/form[1]")); 29 | assignCargoForm.submit(); 30 | 31 | return new CargoDetailsPage(driver, port); 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/acceptance/pages/CustomerPage.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.acceptance.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class CustomerPage { 10 | private final WebDriver driver; 11 | 12 | public CustomerPage(WebDriver driver, int port) { 13 | this.driver = driver; 14 | driver.get(String.format("http://localhost:%d/dddsample/track", port)); 15 | assertThat("Tracking cargo").isEqualTo(driver.getTitle()); 16 | } 17 | 18 | public void trackCargoWithIdOf(String trackingId) { 19 | WebElement element = driver.findElement(By.id("idInput")); 20 | element.sendKeys(trackingId); 21 | element.submit(); 22 | 23 | } 24 | 25 | public void expectCargoLocation(String expectedLocation) { 26 | WebElement cargoSummary = driver.findElement(By.cssSelector("#result h2")); 27 | assertThat(cargoSummary.getText()).endsWith(expectedLocation); 28 | } 29 | 30 | public void expectErrorFor(String expectedErrorMessage) { 31 | WebElement error = driver.findElement(By.cssSelector(".error")); 32 | assertThat(error.getText()).endsWith(expectedErrorMessage); 33 | } 34 | 35 | public void expectNotificationOf(String expectedNotificationMessage) { 36 | WebElement error = driver.findElement(By.cssSelector(".notify")); 37 | assertThat(error.getText()).endsWith(expectedNotificationMessage); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/application/BookingServiceTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import se.citerus.dddsample.application.impl.BookingServiceImpl; 6 | import se.citerus.dddsample.domain.model.cargo.Cargo; 7 | import se.citerus.dddsample.domain.model.cargo.CargoFactory; 8 | import se.citerus.dddsample.domain.model.cargo.CargoRepository; 9 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 10 | import se.citerus.dddsample.domain.model.location.LocationRepository; 11 | import se.citerus.dddsample.domain.model.location.UnLocode; 12 | import se.citerus.dddsample.domain.service.RoutingService; 13 | 14 | import java.time.Instant; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.ArgumentMatchers.isA; 19 | import static org.mockito.Mockito.*; 20 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.CHICAGO; 21 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.STOCKHOLM; 22 | 23 | public class BookingServiceTest { 24 | 25 | BookingServiceImpl bookingService; 26 | CargoRepository cargoRepository; 27 | LocationRepository locationRepository; 28 | RoutingService routingService; 29 | CargoFactory cargoFactory; 30 | 31 | @BeforeEach 32 | public void setUp() { 33 | cargoRepository = mock(CargoRepository.class); 34 | locationRepository = mock(LocationRepository.class); 35 | routingService = mock(RoutingService.class); 36 | cargoFactory = new CargoFactory(locationRepository, cargoRepository); 37 | bookingService = new BookingServiceImpl(cargoRepository, locationRepository, routingService, cargoFactory); 38 | } 39 | 40 | @Test 41 | public void testRegisterNew() { 42 | TrackingId expectedTrackingId = new TrackingId("TRK1"); 43 | UnLocode fromUnlocode = new UnLocode("USCHI"); 44 | UnLocode toUnlocode = new UnLocode("SESTO"); 45 | 46 | when(cargoRepository.nextTrackingId()).thenReturn(expectedTrackingId); 47 | when(locationRepository.find(fromUnlocode)).thenReturn(CHICAGO); 48 | when(locationRepository.find(toUnlocode)).thenReturn(STOCKHOLM); 49 | 50 | TrackingId trackingId = bookingService.bookNewCargo(fromUnlocode, toUnlocode, Instant.now()); 51 | assertThat(trackingId).isEqualTo(expectedTrackingId); 52 | verify(cargoRepository, times(1)).store(isA(Cargo.class)); 53 | verify(locationRepository, times(2)).find(any(UnLocode.class)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/application/HandlingEventServiceTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.application; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import se.citerus.dddsample.application.impl.HandlingEventServiceImpl; 6 | import se.citerus.dddsample.domain.model.cargo.Cargo; 7 | import se.citerus.dddsample.domain.model.cargo.CargoRepository; 8 | import se.citerus.dddsample.domain.model.cargo.RouteSpecification; 9 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 10 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 11 | import se.citerus.dddsample.domain.model.handling.HandlingEventFactory; 12 | import se.citerus.dddsample.domain.model.handling.HandlingEventRepository; 13 | import se.citerus.dddsample.domain.model.location.LocationRepository; 14 | import se.citerus.dddsample.domain.model.voyage.VoyageRepository; 15 | 16 | import java.time.Instant; 17 | 18 | import static org.mockito.ArgumentMatchers.isA; 19 | import static org.mockito.Mockito.*; 20 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.*; 21 | import static se.citerus.dddsample.infrastructure.sampledata.SampleVoyages.CM001; 22 | 23 | public class HandlingEventServiceTest { 24 | private HandlingEventServiceImpl service; 25 | private ApplicationEvents applicationEvents; 26 | private CargoRepository cargoRepository; 27 | private VoyageRepository voyageRepository; 28 | private HandlingEventRepository handlingEventRepository; 29 | private LocationRepository locationRepository; 30 | 31 | private final Cargo cargo = new Cargo(new TrackingId("ABC"), new RouteSpecification(HAMBURG, TOKYO, Instant.now())); 32 | 33 | @BeforeEach 34 | public void setUp() { 35 | cargoRepository = mock(CargoRepository.class); 36 | voyageRepository = mock(VoyageRepository.class); 37 | handlingEventRepository = mock(HandlingEventRepository.class); 38 | locationRepository = mock(LocationRepository.class); 39 | applicationEvents = mock(ApplicationEvents.class); 40 | 41 | HandlingEventFactory handlingEventFactory = new HandlingEventFactory(cargoRepository, voyageRepository, locationRepository); 42 | service = new HandlingEventServiceImpl(handlingEventRepository, applicationEvents, handlingEventFactory); 43 | } 44 | 45 | @Test 46 | public void testRegisterEvent() throws Exception { 47 | when(cargoRepository.find(cargo.trackingId())).thenReturn(cargo); 48 | when(voyageRepository.find(CM001.voyageNumber())).thenReturn(CM001); 49 | when(locationRepository.find(STOCKHOLM.unLocode())).thenReturn(STOCKHOLM); 50 | 51 | service.registerHandlingEvent(Instant.now(), cargo.trackingId(), CM001.voyageNumber(), STOCKHOLM.unLocode(), HandlingEvent.Type.LOAD); 52 | verify(handlingEventRepository, times(1)).store(isA(HandlingEvent.class)); 53 | verify(applicationEvents, times(1)).cargoWasHandled(isA(HandlingEvent.class)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/cargo/LegTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.fail; 6 | 7 | public class LegTest { 8 | 9 | @Test 10 | public void testConstructor() { 11 | try { 12 | new Leg(null,null,null,null,null); 13 | fail("Should not accept null constructor arguments"); 14 | } catch (IllegalArgumentException expected) {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/cargo/RouteSpecificationTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import se.citerus.dddsample.domain.model.voyage.Voyage; 5 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 6 | 7 | import java.util.List; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static se.citerus.dddsample.application.util.DateUtils.toDate; 11 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.*; 12 | 13 | public class RouteSpecificationTest { 14 | 15 | final Voyage hongKongTokyoNewYork = new Voyage.Builder( 16 | new VoyageNumber("V001"), HONGKONG). 17 | addMovement(TOKYO, toDate("2009-02-01"), toDate("2009-02-05")). 18 | addMovement(NEWYORK, toDate("2009-02-06"), toDate("2009-02-10")). 19 | addMovement(HONGKONG, toDate("2009-02-11"), toDate("2009-02-14")). 20 | build(); 21 | 22 | final Voyage dallasNewYorkChicago = new Voyage.Builder( 23 | new VoyageNumber("V002"), DALLAS). 24 | addMovement(NEWYORK, toDate("2009-02-06"), toDate("2009-02-07")). 25 | addMovement(CHICAGO, toDate("2009-02-12"), toDate("2009-02-20")). 26 | build(); 27 | 28 | // TODO: 29 | // it shouldn't be possible to create Legs that have load/unload locations 30 | // and/or dates that don't match the voyage's carrier movements. 31 | final Itinerary itinerary = new Itinerary(List.of( 32 | new Leg(hongKongTokyoNewYork, HONGKONG, NEWYORK, 33 | toDate("2009-02-01"), toDate("2009-02-10")), 34 | new Leg(dallasNewYorkChicago, NEWYORK, CHICAGO, 35 | toDate("2009-02-12"), toDate("2009-02-20"))) 36 | ); 37 | @Test 38 | public void testIsSatisfiedBy_Success() { 39 | RouteSpecification routeSpecification = new RouteSpecification( 40 | HONGKONG, CHICAGO, toDate("2009-03-01") 41 | ); 42 | 43 | assertThat(routeSpecification.isSatisfiedBy(itinerary)).isTrue(); 44 | } 45 | 46 | @Test 47 | public void testIsSatisfiedBy_WrongOrigin() { 48 | RouteSpecification routeSpecification = new RouteSpecification( 49 | HANGZHOU, CHICAGO, toDate("2009-03-01") 50 | ); 51 | 52 | assertThat(routeSpecification.isSatisfiedBy(itinerary)).isFalse(); 53 | } 54 | @Test 55 | public void testIsSatisfiedBy_WrongDestination() { 56 | RouteSpecification routeSpecification = new RouteSpecification( 57 | HONGKONG, DALLAS, toDate("2009-03-01") 58 | ); 59 | 60 | assertThat(routeSpecification.isSatisfiedBy(itinerary)).isFalse(); 61 | } 62 | @Test 63 | public void testIsSatisfiedBy_MissedDeadline() { 64 | RouteSpecification routeSpecification = new RouteSpecification( 65 | HONGKONG, CHICAGO, toDate("2009-02-15") 66 | ); 67 | 68 | assertThat(routeSpecification.isSatisfiedBy(itinerary)).isFalse(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/cargo/TrackingIdTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.cargo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | 7 | public class TrackingIdTest { 8 | 9 | @Test 10 | public void testConstructor() { 11 | assertThatThrownBy(() -> new TrackingId(null)).isInstanceOf(NullPointerException.class); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/handling/HandlingHistoryTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.handling; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import se.citerus.dddsample.domain.model.cargo.Cargo; 6 | import se.citerus.dddsample.domain.model.cargo.RouteSpecification; 7 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 8 | import se.citerus.dddsample.domain.model.voyage.Voyage; 9 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 10 | 11 | import java.time.Instant; 12 | import java.util.List; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static se.citerus.dddsample.application.util.DateUtils.toDate; 16 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.*; 17 | 18 | public class HandlingHistoryTest { 19 | Cargo cargo; 20 | Voyage voyage; 21 | HandlingEvent event1; 22 | HandlingEvent event1duplicate; 23 | HandlingEvent event2; 24 | HandlingHistory handlingHistory; 25 | 26 | @BeforeEach 27 | public void setUp() { 28 | cargo = new Cargo(new TrackingId("ABC"), new RouteSpecification(SHANGHAI, DALLAS, toDate("2009-04-01"))); 29 | voyage = new Voyage.Builder(new VoyageNumber("X25"), HONGKONG). 30 | addMovement(SHANGHAI, Instant.now(), Instant.now()). 31 | addMovement(DALLAS, Instant.now(), Instant.now()). 32 | build(); 33 | event1 = new HandlingEvent(cargo, toDate("2009-03-05"), Instant.ofEpochMilli(100), HandlingEvent.Type.LOAD, SHANGHAI, voyage); 34 | event1duplicate = new HandlingEvent(cargo, toDate("2009-03-05"), Instant.ofEpochMilli(200), HandlingEvent.Type.LOAD, SHANGHAI, voyage); 35 | event2 = new HandlingEvent(cargo, toDate("2009-03-10"), Instant.ofEpochMilli(150), HandlingEvent.Type.UNLOAD, DALLAS, voyage); 36 | 37 | handlingHistory = new HandlingHistory(List.of(event2, event1, event1duplicate)); 38 | } 39 | 40 | @Test 41 | public void testDistinctEventsByCompletionTime() { 42 | assertThat(handlingHistory.distinctEventsByCompletionTime()).isEqualTo(List.of(event1, event2)); 43 | } 44 | 45 | @Test 46 | public void testMostRecentlyCompletedEvent() { 47 | assertThat(handlingHistory.mostRecentlyCompletedEvent()).isEqualTo(event2); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/location/LocationTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.location; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.fail; 7 | 8 | public class LocationTest { 9 | 10 | @Test 11 | public void testEquals() { 12 | // Same UN locode - equal 13 | assertThat(new Location(new UnLocode("ATEST"),"test-name"). 14 | equals(new Location(new UnLocode("ATEST"),"test-name"))).isTrue(); 15 | 16 | // Different UN locodes - not equal 17 | assertThat(new Location(new UnLocode("ATEST"),"test-name"). 18 | equals(new Location(new UnLocode("TESTB"), "test-name"))).isFalse(); 19 | 20 | // Always equal to itself 21 | Location location = new Location(new UnLocode("ATEST"),"test-name"); 22 | assertThat(location.equals(location)).isTrue(); 23 | 24 | // Never equal to null 25 | assertThat(location.equals(null)).isFalse(); 26 | 27 | // Special UNKNOWN location is equal to itself 28 | assertThat(Location.UNKNOWN.equals(Location.UNKNOWN)).isTrue(); 29 | 30 | try { 31 | new Location((UnLocode) null, null); 32 | fail("Should not allow any null constructor arguments"); 33 | } catch (NullPointerException expected) {} 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/location/UnLocodeTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.location; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.EmptySource; 6 | import org.junit.jupiter.params.provider.NullSource; 7 | import org.junit.jupiter.params.provider.ValueSource; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 11 | 12 | public class UnLocodeTest { 13 | 14 | @ValueSource(strings = {"AA234","AAA9B","AAAAA"}) 15 | @ParameterizedTest 16 | public void shouldAllowCreationOfValidUnLoCodes(String input) { 17 | assertThat(new UnLocode(input)).isNotNull(); 18 | } 19 | 20 | @ValueSource(strings = {"AAAA", "AAAAAA", "AAAA", "AAAAAA", "22AAA", "AA111"}) 21 | @NullSource 22 | @EmptySource 23 | @ParameterizedTest 24 | public void shouldPreventCreationOfInvalidUnLoCodes(String input) { 25 | assertThatThrownBy(() -> new UnLocode(input)).isInstanceOfAny(NullPointerException.class, IllegalArgumentException.class); 26 | } 27 | 28 | @Test 29 | public void testIdString() { 30 | assertThat(new UnLocode("AbcDe").idString()).isEqualTo("ABCDE"); 31 | } 32 | 33 | @Test 34 | public void testEquals() { 35 | UnLocode allCaps = new UnLocode("ABCDE"); 36 | UnLocode mixedCase = new UnLocode("aBcDe"); 37 | 38 | assertThat(allCaps.equals(mixedCase)).isTrue(); 39 | assertThat(mixedCase.equals(allCaps)).isTrue(); 40 | assertThat(allCaps.equals(allCaps)).isTrue(); 41 | 42 | assertThat(allCaps.equals(null)).isFalse(); 43 | assertThat(allCaps.equals(new UnLocode("FGHIJ"))).isFalse(); 44 | } 45 | 46 | @Test 47 | public void testHashCode() { 48 | UnLocode allCaps = new UnLocode("ABCDE"); 49 | UnLocode mixedCase = new UnLocode("aBcDe"); 50 | 51 | assertThat(mixedCase.hashCode()).isEqualTo(allCaps.hashCode()); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/model/voyage/CarrierMovementTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.model.voyage; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.time.Instant; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.assertj.core.api.Assertions.fail; 9 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.HAMBURG; 10 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.STOCKHOLM; 11 | 12 | public class CarrierMovementTest { 13 | 14 | @Test 15 | public void testConstructor() { 16 | try { 17 | new CarrierMovement(null, null, Instant.now(), Instant.now()); 18 | fail("Should not accept null constructor arguments"); 19 | } catch (IllegalArgumentException expected) {} 20 | 21 | try { 22 | new CarrierMovement(STOCKHOLM, null, Instant.now(), Instant.now()); 23 | fail("Should not accept null constructor arguments"); 24 | } catch (IllegalArgumentException expected) {} 25 | 26 | // Legal 27 | new CarrierMovement(STOCKHOLM, HAMBURG, Instant.now(), Instant.now()); 28 | } 29 | 30 | @Test 31 | public void testSameValueAsEqualsHashCode() { 32 | long referenceTime = System.currentTimeMillis(); 33 | 34 | // One could, in theory, use the same Date(referenceTime) for all of these movements 35 | // However, in practice, carrier movements will be initialized by different processes 36 | // so we might have different Date that reference the same time, and we want to be 37 | // certain that sameValueAs does the right thing in that case. 38 | CarrierMovement cm1 = new CarrierMovement(STOCKHOLM, HAMBURG, Instant.ofEpochMilli(referenceTime), Instant.ofEpochMilli(referenceTime)); 39 | CarrierMovement cm2 = new CarrierMovement(STOCKHOLM, HAMBURG, Instant.ofEpochMilli(referenceTime), Instant.ofEpochMilli(referenceTime)); 40 | CarrierMovement cm3 = new CarrierMovement(HAMBURG, STOCKHOLM, Instant.ofEpochMilli(referenceTime), Instant.ofEpochMilli(referenceTime)); 41 | CarrierMovement cm4 = new CarrierMovement(HAMBURG, STOCKHOLM, Instant.ofEpochMilli(referenceTime), Instant.ofEpochMilli(referenceTime)); 42 | 43 | assertThat(cm1.sameValueAs(cm2)).isTrue(); 44 | assertThat(cm2.sameValueAs(cm3)).isFalse(); 45 | assertThat(cm3.sameValueAs(cm4)).isTrue(); 46 | 47 | assertThat(cm1.equals(cm2)).isTrue(); 48 | assertThat(cm2.equals(cm3)).isFalse(); 49 | assertThat(cm3.equals(cm4)).isTrue(); 50 | 51 | assertThat(cm1.hashCode() == cm2.hashCode()).isTrue(); 52 | assertThat(cm2.hashCode() == cm3.hashCode()).isFalse(); 53 | assertThat(cm3.hashCode() == cm4.hashCode()).isTrue(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/shared/AlwaysFalseSpec.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | public class AlwaysFalseSpec extends AbstractSpecification { 4 | public boolean isSatisfiedBy(Object o) { 5 | return false; 6 | } 7 | } -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/shared/AlwaysTrueSpec.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | public class AlwaysTrueSpec extends AbstractSpecification { 4 | public boolean isSatisfiedBy(Object o) { 5 | return true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/shared/AndSpecificationTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class AndSpecificationTest { 8 | 9 | @Test 10 | public void testAndIsSatisifedBy() { 11 | AlwaysTrueSpec trueSpec = new AlwaysTrueSpec(); 12 | AlwaysFalseSpec falseSpec = new AlwaysFalseSpec(); 13 | 14 | AndSpecification andSpecification = new AndSpecification(trueSpec, trueSpec); 15 | assertThat(andSpecification.isSatisfiedBy(new Object())).isTrue(); 16 | 17 | andSpecification = new AndSpecification(falseSpec, trueSpec); 18 | assertThat(andSpecification.isSatisfiedBy(new Object())).isFalse(); 19 | 20 | andSpecification = new AndSpecification(trueSpec, falseSpec); 21 | assertThat(andSpecification.isSatisfiedBy(new Object())).isFalse(); 22 | 23 | andSpecification = new AndSpecification(falseSpec, falseSpec); 24 | assertThat(andSpecification.isSatisfiedBy(new Object())).isFalse(); 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/shared/NotSpecificationTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class NotSpecificationTest { 8 | 9 | @Test 10 | public void testAndIsSatisifedBy() { 11 | AlwaysTrueSpec trueSpec = new AlwaysTrueSpec(); 12 | AlwaysFalseSpec falseSpec = new AlwaysFalseSpec(); 13 | 14 | NotSpecification notSpecification = new NotSpecification(trueSpec); 15 | assertThat(notSpecification.isSatisfiedBy(new Object())).isFalse(); 16 | 17 | notSpecification = new NotSpecification(falseSpec); 18 | assertThat(notSpecification.isSatisfiedBy(new Object())).isTrue(); 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/domain/shared/OrSpecificationTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.domain.shared; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class OrSpecificationTest { 8 | 9 | @Test 10 | public void testAndIsSatisifedBy() { 11 | AlwaysTrueSpec trueSpec = new AlwaysTrueSpec(); 12 | AlwaysFalseSpec falseSpec = new AlwaysFalseSpec(); 13 | 14 | OrSpecification orSpecification = new OrSpecification(trueSpec, trueSpec); 15 | assertThat(orSpecification.isSatisfiedBy(new Object())).isTrue(); 16 | 17 | orSpecification = new OrSpecification(falseSpec, trueSpec); 18 | assertThat(orSpecification.isSatisfiedBy(new Object())).isTrue(); 19 | 20 | orSpecification = new OrSpecification(trueSpec, falseSpec); 21 | assertThat(orSpecification.isSatisfiedBy(new Object())).isTrue(); 22 | 23 | orSpecification = new OrSpecification(falseSpec, falseSpec); 24 | assertThat(orSpecification.isSatisfiedBy(new Object())).isFalse(); 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/messaging/stub/SynchronousApplicationEventsStub.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.messaging.stub; 2 | 3 | import se.citerus.dddsample.application.ApplicationEvents; 4 | import se.citerus.dddsample.application.CargoInspectionService; 5 | import se.citerus.dddsample.domain.model.cargo.Cargo; 6 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 7 | import se.citerus.dddsample.interfaces.handling.HandlingEventRegistrationAttempt; 8 | 9 | public class SynchronousApplicationEventsStub implements ApplicationEvents { 10 | 11 | CargoInspectionService cargoInspectionService; 12 | 13 | public void setCargoInspectionService(CargoInspectionService cargoInspectionService) { 14 | this.cargoInspectionService = cargoInspectionService; 15 | } 16 | 17 | @Override 18 | public void cargoWasHandled(HandlingEvent event) { 19 | System.out.println("EVENT: cargo was handled: " + event); 20 | cargoInspectionService.inspectCargo(event.cargo().trackingId()); 21 | } 22 | 23 | @Override 24 | public void cargoWasMisdirected(Cargo cargo) { 25 | System.out.println("EVENT: cargo was misdirected"); 26 | } 27 | 28 | @Override 29 | public void cargoHasArrived(Cargo cargo) { 30 | System.out.println("EVENT: cargo has arrived: " + cargo.trackingId().idString()); 31 | } 32 | 33 | @Override 34 | public void receivedHandlingEventRegistrationAttempt(HandlingEventRegistrationAttempt attempt) { 35 | System.out.println("EVENT: received handling event registration attempt"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/persistence/inmemory/HandlingEventRepositoryInMem.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.inmemory; 2 | 3 | import se.citerus.dddsample.domain.model.cargo.TrackingId; 4 | import se.citerus.dddsample.domain.model.handling.HandlingEvent; 5 | import se.citerus.dddsample.domain.model.handling.HandlingEventRepository; 6 | import se.citerus.dddsample.domain.model.handling.HandlingHistory; 7 | 8 | import java.util.*; 9 | 10 | public class HandlingEventRepositoryInMem implements HandlingEventRepository { 11 | 12 | private final Map> eventMap = new HashMap<>(); 13 | 14 | @Override 15 | public void store(HandlingEvent event) { 16 | final TrackingId trackingId = event.cargo().trackingId(); 17 | List list = eventMap.computeIfAbsent(trackingId, k -> new ArrayList<>()); 18 | list.add(event); 19 | } 20 | 21 | @Override 22 | public HandlingHistory lookupHandlingHistoryOfCargo(TrackingId trackingId) { 23 | List events = eventMap.get(trackingId); 24 | if (events == null) events = Collections.emptyList(); 25 | 26 | return new HandlingHistory(events); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/persistence/inmemory/LocationRepositoryInMem.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.inmemory; 2 | 3 | import se.citerus.dddsample.domain.model.location.Location; 4 | import se.citerus.dddsample.domain.model.location.LocationRepository; 5 | import se.citerus.dddsample.domain.model.location.UnLocode; 6 | import se.citerus.dddsample.infrastructure.sampledata.SampleLocations; 7 | 8 | import java.util.List; 9 | 10 | public class LocationRepositoryInMem implements LocationRepository { 11 | 12 | public Location find(UnLocode unLocode) { 13 | for (Location location : SampleLocations.getAll()) { 14 | if (location.unLocode().equals(unLocode)) { 15 | return location; 16 | } 17 | } 18 | return null; 19 | } 20 | 21 | public List getAll() { 22 | return SampleLocations.getAll(); 23 | } 24 | 25 | @Override 26 | public Location store(Location location) { 27 | return location; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/persistence/inmemory/VoyageRepositoryInMem.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.inmemory; 2 | 3 | import se.citerus.dddsample.domain.model.voyage.Voyage; 4 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 5 | import se.citerus.dddsample.domain.model.voyage.VoyageRepository; 6 | import se.citerus.dddsample.infrastructure.sampledata.SampleVoyages; 7 | 8 | public final class VoyageRepositoryInMem implements VoyageRepository { 9 | 10 | public Voyage find(VoyageNumber voyageNumber) { 11 | return SampleVoyages.lookup(voyageNumber); 12 | } 13 | 14 | @Override 15 | public void store(Voyage voyage) { 16 | // noop 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/persistence/jpa/CarrierMovementRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 7 | import org.springframework.test.annotation.DirtiesContext; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit.jupiter.SpringExtension; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import se.citerus.dddsample.domain.model.voyage.Voyage; 12 | import se.citerus.dddsample.domain.model.voyage.VoyageNumber; 13 | import se.citerus.dddsample.domain.model.voyage.VoyageRepository; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | @ExtendWith(SpringExtension.class) 18 | @DataJpaTest 19 | @ContextConfiguration(classes = TestRepositoryConfig.class) 20 | @Transactional 21 | @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) 22 | public class CarrierMovementRepositoryTest { 23 | 24 | @Autowired 25 | VoyageRepository voyageRepository; 26 | 27 | @Test 28 | public void testFind() { 29 | Voyage voyage = voyageRepository.find(new VoyageNumber("0100S")); 30 | assertThat(voyage).isNotNull(); 31 | assertThat(voyage.voyageNumber().idString()).isEqualTo("0100S"); 32 | /* TODO adapt 33 | assertThat(carrierMovement.departureLocation()).isEqualTo(STOCKHOLM); 34 | assertThat(carrierMovement.arrivalLocation()).isEqualTo(HELSINKI); 35 | assertThat(carrierMovement.departureTime()).isEqualTo(DateTestUtil.toDate("2007-09-23", "02:00")); 36 | assertThat(carrierMovement.arrivalTime()).isEqualTo(DateTestUtil.toDate("2007-09-23", "03:00")); 37 | */ 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/persistence/jpa/LocationRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 7 | import org.springframework.test.annotation.DirtiesContext; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit.jupiter.SpringExtension; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import se.citerus.dddsample.domain.model.location.Location; 12 | import se.citerus.dddsample.domain.model.location.LocationRepository; 13 | import se.citerus.dddsample.domain.model.location.UnLocode; 14 | 15 | import java.util.List; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | @ExtendWith(SpringExtension.class) 20 | @DataJpaTest 21 | @ContextConfiguration(classes = TestRepositoryConfig.class) 22 | @Transactional 23 | @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) 24 | public class LocationRepositoryTest { 25 | @Autowired 26 | private LocationRepository locationRepository; 27 | 28 | @Test 29 | public void testFind() { 30 | final UnLocode melbourne = new UnLocode("AUMEL"); 31 | Location location = locationRepository.find(melbourne); 32 | assertThat(location).isNotNull(); 33 | assertThat(location.unLocode()).isEqualTo(melbourne); 34 | 35 | assertThat(locationRepository.find(new UnLocode("NOLOC"))).isNull(); 36 | } 37 | 38 | @Test 39 | public void testFindAll() { 40 | List allLocations = locationRepository.getAll(); 41 | 42 | assertThat(allLocations).isNotNull().hasSize(13); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/infrastructure/persistence/jpa/TestRepositoryConfig.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.infrastructure.persistence.jpa; 2 | 3 | import org.springframework.boot.test.context.TestConfiguration; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Primary; 6 | import se.citerus.dddsample.interfaces.handling.file.UploadDirectoryScanner; 7 | 8 | /** 9 | * This config is required by the repository tests to avoid a strange behavior where the UploadDirectoryScanner 10 | * creates directories despite the file paths not having been initialized properly. 11 | */ 12 | @TestConfiguration 13 | public class TestRepositoryConfig { 14 | @Primary 15 | @Bean 16 | public UploadDirectoryScanner uploadDirectoryScanner() { 17 | return new UploadDirectoryScanner(null, null, null) { 18 | @Override 19 | public void afterPropertiesSet() { 20 | // noop 21 | } 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/interfaces/booking/facade/internal/assembler/CargoRoutingDTOAssemblerTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.internal.assembler; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import se.citerus.dddsample.domain.model.cargo.*; 5 | import se.citerus.dddsample.domain.model.location.Location; 6 | import se.citerus.dddsample.interfaces.booking.facade.dto.CargoRoutingDTO; 7 | import se.citerus.dddsample.interfaces.booking.facade.dto.LegDTO; 8 | 9 | import java.time.Instant; 10 | import java.util.List; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.*; 14 | import static se.citerus.dddsample.infrastructure.sampledata.SampleVoyages.CM001; 15 | 16 | public class CargoRoutingDTOAssemblerTest { 17 | 18 | @Test 19 | public void testToDTO() { 20 | final CargoRoutingDTOAssembler assembler = new CargoRoutingDTOAssembler(); 21 | 22 | final Location origin = STOCKHOLM; 23 | final Location destination = MELBOURNE; 24 | final Cargo cargo = new Cargo(new TrackingId("XYZ"), new RouteSpecification(origin, destination, Instant.now())); 25 | 26 | final Itinerary itinerary = new Itinerary( 27 | List.of( 28 | new Leg(CM001, origin, SHANGHAI, Instant.now(), Instant.now()), 29 | new Leg(CM001, ROTTERDAM, destination, Instant.now(), Instant.now()) 30 | ) 31 | ); 32 | 33 | cargo.assignToRoute(itinerary); 34 | 35 | final CargoRoutingDTO dto = assembler.toDTO(cargo); 36 | 37 | assertThat(dto.getLegs()).hasSize(2); 38 | 39 | LegDTO legDTO = dto.getLegs().get(0); 40 | assertThat(legDTO.getVoyageNumber()).isEqualTo("CM001"); 41 | assertThat(legDTO.getFrom()).isEqualTo("SESTO"); 42 | assertThat(legDTO.getTo()).isEqualTo("CNSHA"); 43 | 44 | legDTO = dto.getLegs().get(1); 45 | assertThat(legDTO.getVoyageNumber()).isEqualTo("CM001"); 46 | assertThat(legDTO.getFrom()).isEqualTo("NLRTM"); 47 | assertThat(legDTO.getTo()).isEqualTo("AUMEL"); 48 | } 49 | 50 | @Test 51 | public void testToDTO_NoItinerary() { 52 | final CargoRoutingDTOAssembler assembler = new CargoRoutingDTOAssembler(); 53 | 54 | final Cargo cargo = new Cargo(new TrackingId("XYZ"), new RouteSpecification(STOCKHOLM, MELBOURNE, Instant.now())); 55 | final CargoRoutingDTO dto = assembler.toDTO(cargo); 56 | 57 | assertThat(dto.getTrackingId()).isEqualTo("XYZ"); 58 | assertThat(dto.getOrigin()).isEqualTo("SESTO"); 59 | assertThat(dto.getFinalDestination()).isEqualTo("AUMEL"); 60 | assertThat(dto.getLegs().isEmpty()).isTrue(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/interfaces/booking/facade/internal/assembler/LocationDTOAssemblerTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.facade.internal.assembler; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import se.citerus.dddsample.domain.model.location.Location; 5 | import se.citerus.dddsample.interfaces.booking.facade.dto.LocationDTO; 6 | 7 | import java.util.List; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.HAMBURG; 11 | import static se.citerus.dddsample.infrastructure.sampledata.SampleLocations.STOCKHOLM; 12 | 13 | public class LocationDTOAssemblerTest { 14 | 15 | @Test 16 | public void testToDTOList() { 17 | final LocationDTOAssembler assembler = new LocationDTOAssembler(); 18 | final List locationList = List.of(STOCKHOLM, HAMBURG); 19 | 20 | final List dtos = assembler.toDTOList(locationList); 21 | 22 | assertThat(dtos).hasSize(2); 23 | 24 | LocationDTO dto = dtos.get(0); 25 | assertThat(dto.getUnLocode()).isEqualTo("SESTO"); 26 | assertThat(dto.getName()).isEqualTo("Stockholm"); 27 | 28 | dto = dtos.get(1); 29 | assertThat(dto.getUnLocode()).isEqualTo("DEHAM"); 30 | assertThat(dto.getName()).isEqualTo("Hamburg"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/interfaces/booking/web/ItinerarySelectionCommandTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.booking.web; 2 | 3 | import org.assertj.core.groups.Tuple; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.mock.web.MockHttpServletRequest; 6 | import org.springframework.web.bind.ServletRequestDataBinder; 7 | 8 | import java.util.List; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class ItinerarySelectionCommandTest { 13 | 14 | RouteAssignmentCommand command; 15 | MockHttpServletRequest request; 16 | 17 | @Test 18 | public void testBind() { 19 | command = new RouteAssignmentCommand(); 20 | request = new MockHttpServletRequest(); 21 | 22 | request.addParameter("legs[0].voyageNumber", "CM01"); 23 | request.addParameter("legs[0].fromUnLocode", "AAAAA"); 24 | request.addParameter("legs[0].toUnLocode", "BBBBB"); 25 | 26 | request.addParameter("legs[1].voyageNumber", "CM02"); 27 | request.addParameter("legs[1].fromUnLocode", "CCCCC"); 28 | request.addParameter("legs[1].toUnLocode", "DDDDD"); 29 | 30 | request.addParameter("trackingId", "XYZ"); 31 | 32 | ServletRequestDataBinder binder = new ServletRequestDataBinder(command); 33 | binder.bind(request); 34 | 35 | assertThat(command.getLegs()).hasSize(2).extracting("voyageNumber", "fromUnLocode", "toUnLocode") 36 | .containsAll(List.of(Tuple.tuple("CM01", "AAAAA", "BBBBB"), Tuple.tuple("CM02", "CCCCC", "DDDDD"))); 37 | 38 | assertThat(command.getTrackingId()).isEqualTo("XYZ"); 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/interfaces/tracking/TrackCommandValidatorTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.validation.BeanPropertyBindingResult; 6 | import org.springframework.validation.BindingResult; 7 | import org.springframework.validation.FieldError; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class TrackCommandValidatorTest { 12 | 13 | TrackCommandValidator validator; 14 | 15 | @BeforeEach 16 | public void setUp() { 17 | validator = new TrackCommandValidator(); 18 | } 19 | 20 | @Test 21 | public void testValidateIllegalId() { 22 | TrackCommand command = new TrackCommand(); 23 | BindingResult errors = new BeanPropertyBindingResult(command, "command"); 24 | validator.validate(command, errors); 25 | 26 | assertThat(errors.getErrorCount()).isEqualTo(1); 27 | FieldError error = errors.getFieldError("trackingId"); 28 | assertThat(error).isNotNull(); 29 | assertThat(error.getRejectedValue()).isNull(); 30 | assertThat(error.getCode()).isEqualTo("error.required"); 31 | } 32 | 33 | @Test 34 | public void testValidateSuccess() { 35 | TrackCommand command = new TrackCommand(); 36 | command.setTrackingId("non-empty"); 37 | BindingResult errors = new BeanPropertyBindingResult(command, "command"); 38 | validator.validate(command, errors); 39 | 40 | assertThat(errors.hasErrors()).isFalse(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/se/citerus/dddsample/interfaces/tracking/ws/CargoTrackingRestServiceIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package se.citerus.dddsample.interfaces.tracking.ws; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.web.server.LocalServerPort; 8 | import org.springframework.http.RequestEntity; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.test.context.junit.jupiter.SpringExtension; 11 | import org.springframework.transaction.annotation.Transactional; 12 | import org.springframework.util.StreamUtils; 13 | import org.springframework.web.client.HttpClientErrorException; 14 | import org.springframework.web.client.RestTemplate; 15 | import org.springframework.web.util.UriTemplate; 16 | import se.citerus.dddsample.Application; 17 | 18 | import java.net.URI; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.Locale; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | import static org.assertj.core.api.Assertions.fail; 24 | 25 | @ExtendWith(SpringExtension.class) 26 | @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 27 | public class CargoTrackingRestServiceIntegrationTest { 28 | 29 | @LocalServerPort 30 | private int port; 31 | 32 | private final RestTemplate restTemplate = new RestTemplate(); 33 | 34 | @BeforeAll 35 | static void beforeClass() { 36 | // The expected date time values in the resonse are formatted in US locale. 37 | // Set the locale in the tests so that they won't fail in non-US locales such as Europe. 38 | Locale.setDefault(Locale.US); 39 | } 40 | 41 | @Transactional 42 | @Test 43 | void shouldReturn200ResponseAndJsonWhenRequestingCargoWithIdABC123() throws Exception { 44 | URI uri = new UriTemplate("http://localhost:{port}/dddsample/api/track/ABC123").expand(port); 45 | RequestEntity request = RequestEntity.get(uri).build(); 46 | 47 | ResponseEntity response = restTemplate.exchange(request, String.class); 48 | 49 | assertThat(response.getStatusCode().value()).isEqualTo(200); 50 | String expected = StreamUtils.copyToString(getClass().getResourceAsStream("/sampleCargoTrackingResponse.json"), StandardCharsets.UTF_8); 51 | assertThat(response.getHeaders().get("Content-Type")).containsExactly("application/json"); 52 | assertThat(response.getBody()).isEqualTo(expected); 53 | } 54 | 55 | @Test 56 | void shouldReturnValidationErrorResponseWhenInvalidHandlingReportIsSubmitted() throws Exception { 57 | URI uri = new UriTemplate("http://localhost:{port}/dddsample/api/track/MISSING").expand(port); 58 | RequestEntity request = RequestEntity.get(uri).build(); 59 | 60 | try { 61 | restTemplate.exchange(request, String.class); 62 | fail("Did not throw HttpClientErrorException"); 63 | } catch (HttpClientErrorException e) { 64 | assertThat(e.getResponseHeaders().getLocation()).isEqualTo(new URI("/dddsample/api/track/MISSING")); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/resources/config/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | dataSource: 3 | url: jdbc:hsqldb:mem:dddsample_test 4 | main: 5 | allow-bean-definition-overriding: true 6 | server: 7 | error: 8 | include-message: always -------------------------------------------------------------------------------- /src/test/resources/handling_events.csv: -------------------------------------------------------------------------------- 1 | 2009-03-06 12:30 ABC123 0200T USNYC LOAD 2 | 2009-03-08 04:00 ABC123 0200T USDAL UNLOAD 3 | 2009-03-09 08:12 ABC123 0300A USDAL LOAD 4 | 2009-03-12 19:25 ABC123 0300A FIHEL UNLOAD 5 | -------------------------------------------------------------------------------- /src/test/resources/sampleCargoTrackingResponse.json: -------------------------------------------------------------------------------- 1 | {"trackingId":"ABC123","statusText":"In port New York","destination":"Helsinki","eta":"2009-03-12T00:00:00Z","nextExpectedActivity":"Next expected activity is to load cargo onto voyage 0200T in New York","isMisdirected":false,"handlingEvents":[{"location":"Hongkong","time":"2009-03-01T00:00:00Z","type":"RECEIVE","voyageNumber":"","isExpected":true,"description":"Received in Hongkong, at Mar 1, 2009, 12:00:00 AM."},{"location":"Hongkong","time":"2009-03-02T00:00:00Z","type":"LOAD","voyageNumber":"0100S","isExpected":true,"description":"Loaded onto voyage 0100S in Hongkong, at Mar 2, 2009, 12:00:00 AM."},{"location":"New York","time":"2009-03-05T00:00:00Z","type":"UNLOAD","voyageNumber":"0100S","isExpected":true,"description":"Unloaded off voyage 0100S in New York, at Mar 5, 2009, 12:00:00 AM."}]} -------------------------------------------------------------------------------- /src/test/resources/sampleHandlingReport.json: -------------------------------------------------------------------------------- 1 | { 2 | "completionTime": "2022-01-01T13:37:00", 3 | "trackingIds": ["ABC123"], 4 | "type": "LOAD", 5 | "unLocode": "SESTO", 6 | "voyageNumber": "0100S" 7 | } -------------------------------------------------------------------------------- /src/test/resources/sampleHandlingReportFile.csv: -------------------------------------------------------------------------------- 1 | 2022-10-29 13:37 ABC123 0101 SESTO CUSTOMS -------------------------------------------------------------------------------- /src/test/resources/sampleInvalidHandlingReportFile.csv: -------------------------------------------------------------------------------- 1 | 2022-10-29 13:37 ABC123 0101 XXX CUSTOMS --------------------------------------------------------------------------------