| /* |
| * Copyright 2011 Marc Grue. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| * implied. |
| * |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.zest.sample.dcicargo.sample_a.context.shipping.booking; |
| |
| import java.util.Date; |
| import java.util.Iterator; |
| import org.apache.zest.api.injection.scope.This; |
| import org.apache.zest.api.mixin.Mixins; |
| import org.apache.zest.api.value.ValueBuilder; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.cargo.Cargo; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.cargo.RouteSpecification; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.delivery.Delivery; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.delivery.ExpectedHandlingEvent; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.delivery.RoutingStatus; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.delivery.TransportStatus; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.handling.HandlingEvent; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.handling.HandlingEventType; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.itinerary.Itinerary; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.itinerary.Leg; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.location.Location; |
| import org.apache.zest.sample.dcicargo.sample_a.data.shipping.voyage.Voyage; |
| import org.apache.zest.sample.dcicargo.sample_a.infrastructure.dci.Context; |
| import org.apache.zest.sample.dcicargo.sample_a.infrastructure.dci.RoleMixin; |
| |
| /** |
| * Build Delivery Snapshot use case. |
| * |
| * This is a subfunction level use case (see Cockurn 2001) supporting user-goal level use cases |
| * like {@link BookNewCargo} by returning a Delivery snapshot Value object. |
| * |
| * A Delivery snapshot describes all we know about the current shipping status of a Cargo. It's the |
| * heart of the application domain knowledge. |
| * |
| * The Delivery snapshot is derived from one of the 3 overall stages a Cargo shipping can be in: |
| * 1) Cargo is being created (Route Specification is known) |
| * 2) Cargo has been routed (Route Specification + Itinerary is known) |
| * 3) Cargo is in transit (Route Specification + Itinerary + last Handling Event is known) |
| * |
| * Code is organized along the contours of the process flow of cargo shipping instead of the data |
| * structures. This makes it easier to reason about a certain step in the shipping process (use case) |
| * according to the intuitive understanding of the shipping domain. |
| * |
| * @see RouteSpecification plays a (methodful) Role of a factory that coordinates the Delivery build. |
| * @see HandlingEvent plays a (methodful) Role that coordinates deriving data from the last Handling Event. |
| * @see Itinerary plays a (methodful) Role supporting the other two roles with Itinerary calculations. |
| */ |
| public class BuildDeliverySnapshot extends Context |
| { |
| // ROLES --------------------------------------------------------------------- |
| |
| private FactoryRole factory; |
| private ItineraryRole itinerary; |
| private HandlingEventRole handlingEvent; |
| |
| private Delivery newDeliverySnapshot; |
| |
| // CONTEXT CONSTRUCTORS ------------------------------------------------------ |
| |
| public BuildDeliverySnapshot( RouteSpecification routeSpecification ) |
| { |
| // Deviation 2a |
| if( routeSpecification.origin().get() == routeSpecification.destination().get() ) |
| { |
| throw new RouteException( "Route specification is invalid. Origin equals destination." ); |
| } |
| |
| // Deviation 2b |
| if( routeSpecification.arrivalDeadline().get().before( new Date() ) ) |
| { |
| throw new RouteException( "Arrival deadline is in the past or Today." + |
| "\nDeadline " + routeSpecification.arrivalDeadline().get() + |
| "\nToday " + new Date() ); |
| } |
| |
| factory = rolePlayer( FactoryRole.class, routeSpecification ); |
| itinerary = null; |
| handlingEvent = null; |
| } |
| |
| public BuildDeliverySnapshot( Cargo cargo ) |
| { |
| factory = rolePlayer( FactoryRole.class, cargo.routeSpecification().get() ); |
| itinerary = rolePlayer( ItineraryRole.class, cargo.itinerary().get() ); |
| handlingEvent = rolePlayer( HandlingEventRole.class, cargo.delivery().get().lastHandlingEvent().get() ); |
| } |
| |
| public BuildDeliverySnapshot( Cargo cargo, HandlingEvent registeredHandlingEvent ) |
| { |
| factory = rolePlayer( FactoryRole.class, cargo.routeSpecification().get() ); |
| itinerary = rolePlayer( ItineraryRole.class, cargo.itinerary().get() ); |
| handlingEvent = rolePlayer( HandlingEventRole.class, registeredHandlingEvent ); |
| } |
| |
| // INTERACTIONS -------------------------------------------------------------- |
| |
| public Delivery get() |
| { |
| return factory.deriveDeliverySnapshot(); |
| } |
| |
| // METHODFUL ROLE IMPLEMENTATIONS -------------------------------------------- |
| |
| /** |
| * The FactoryRole coordinates the overall Delivery snapshot build. |
| * |
| * When the Cargo has a delivery history the FactoryRole delegates to the LastHandlingEventRole |
| * to derive data from the last HandlingEvent. |
| */ |
| @Mixins( FactoryRole.Mixin.class ) |
| public interface FactoryRole |
| { |
| void setContext( BuildDeliverySnapshot context ); |
| |
| Delivery deriveDeliverySnapshot(); |
| |
| class Mixin |
| extends RoleMixin<BuildDeliverySnapshot> |
| implements FactoryRole |
| { |
| @This |
| RouteSpecification routeSpecification; |
| |
| ValueBuilder<Delivery> deliveryBuilder; |
| |
| public Delivery deriveDeliverySnapshot() |
| { |
| /* |
| * Default values: |
| * |
| * isMisdirected = false |
| * nextExpectedHandlingEvent = null |
| * lastHandlingEvent = null |
| * lastKnownLocation = null |
| * currentVoyage = null |
| * eta = null |
| * isUnloadedAtDestination = false |
| * |
| * */ |
| |
| // Build delivery snapshot object |
| deliveryBuilder = vbf.newValueBuilder( Delivery.class ); |
| context.newDeliverySnapshot = deliveryBuilder.prototype(); |
| context.newDeliverySnapshot.timestamp().set( new Date() ); |
| |
| // Deviation 2c: Cargo is not routed yet |
| if( context.itinerary == null ) |
| { |
| return deriveWithRouteSpecification(); |
| } |
| |
| if( !routeSpecification.isSatisfiedBy( (Itinerary) context.itinerary ) ) |
| { |
| // Deviation 2d: Itinerary not satisfying |
| context.newDeliverySnapshot.routingStatus().set( RoutingStatus.MISROUTED ); |
| } |
| else |
| { |
| // Step 2 |
| context.newDeliverySnapshot.routingStatus().set( RoutingStatus.ROUTED ); |
| context.newDeliverySnapshot.eta().set( context.itinerary.eta() ); |
| } |
| |
| // Deviation 3a: Cargo has no handling history yet |
| if( context.handlingEvent == null ) |
| { |
| return deriveWithItinerary(); |
| } |
| |
| // Step 4: Cargo has a handling history |
| context.handlingEvent.deriveWithHandlingEvent(); |
| |
| // Step 5: Return Delivery object |
| return deliveryBuilder.newInstance(); |
| } |
| |
| private Delivery deriveWithRouteSpecification() |
| { |
| // Deviation 2c |
| context.newDeliverySnapshot.routingStatus().set( RoutingStatus.NOT_ROUTED ); |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.NOT_RECEIVED ); |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( buildExpectedReceiveEvent() ); |
| return deliveryBuilder.newInstance(); |
| } |
| |
| private Delivery deriveWithItinerary() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.NOT_RECEIVED ); |
| |
| // Deviation 3a.1.a |
| if( context.newDeliverySnapshot.routingStatus().get() == RoutingStatus.ROUTED ) |
| { |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( buildExpectedReceiveEvent() ); |
| } |
| |
| return deliveryBuilder.newInstance(); |
| } |
| |
| private ExpectedHandlingEvent buildExpectedReceiveEvent() |
| { |
| ValueBuilder<ExpectedHandlingEvent> builder = vbf.newValueBuilder( ExpectedHandlingEvent.class ); |
| builder.prototype().handlingEventType().set( HandlingEventType.RECEIVE ); |
| builder.prototype().location().set( routeSpecification.origin().get() ); |
| return builder.newInstance(); |
| } |
| } |
| } |
| |
| /** |
| * The HandlingEventRole derives data from the last Handling Event. |
| * |
| * This is where the core domain knowledge about the shipping flow is. Code is organized by the |
| * 6 different handling event types of a cargo: |
| * RECEIVE - LOAD - UNLOAD - CLAIM - CUSTOMS - (UNKNOWN) |
| * |
| * For each HandlingEventType we can reason about the status and what is expected next. This |
| * follows the real world shipping flow rather than data structures of the domain. |
| * |
| * The HandlingEventRole uses the ItineraryRole heavily to calculate values based on Itinerary data. |
| */ |
| @Mixins( HandlingEventRole.Mixin.class ) |
| public interface HandlingEventRole |
| { |
| void setContext( BuildDeliverySnapshot context ); |
| |
| void deriveWithHandlingEvent(); |
| |
| class Mixin |
| extends RoleMixin<BuildDeliverySnapshot> |
| implements HandlingEventRole |
| { |
| /* |
| * HandlingEvent can be from last Delivery snapshot (last expected HandlingEvent) or |
| * a new registered HandlingEvent. |
| * */ |
| @This |
| HandlingEvent handlingEvent; |
| |
| // Local convenience fields |
| Location lastKnownLocation; |
| Voyage currentVoyage; |
| Boolean cargoIsNotMisrouted; |
| |
| // Flag for determining if we should wasUnexpected a former misdirected Handling Event after a reroute |
| Boolean cargoIsRerouted; |
| |
| public void deriveWithHandlingEvent() |
| { |
| lastKnownLocation = handlingEvent.location().get(); |
| currentVoyage = handlingEvent.voyage().get(); |
| cargoIsNotMisrouted = context.newDeliverySnapshot.routingStatus().get() == RoutingStatus.ROUTED; |
| |
| // Cargo has been rerouted when the last handling event is declared unexpected. |
| cargoIsRerouted = handlingEvent.wasUnexpected().get(); |
| |
| // Step 3 |
| context.newDeliverySnapshot.lastHandlingEvent().set( handlingEvent ); |
| context.newDeliverySnapshot.lastKnownLocation().set( lastKnownLocation ); |
| |
| // Step 4 |
| switch( handlingEvent.handlingEventType().get() ) |
| { |
| case RECEIVE: |
| cargoReceived(); // Deviation 4a |
| return; |
| |
| case LOAD: |
| cargoLoaded(); // Deviation 4b |
| return; |
| |
| case UNLOAD: |
| cargoUnloaded(); // Deviation 4c |
| return; |
| |
| case CUSTOMS: |
| cargoInCustoms(); // Deviation 4d |
| return; |
| |
| case CLAIM: |
| cargoClaimed(); // Deviation 4e |
| return; |
| |
| default: |
| unknownHandlingEvent(); // Deviation 4f |
| } |
| } |
| |
| // Deviation 4a |
| private void cargoReceived() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.IN_PORT ); |
| |
| if( !cargoIsRerouted && !context.itinerary.expectsOrigin( lastKnownLocation ) ) |
| { |
| context.newDeliverySnapshot.eta().set( null ); |
| context.newDeliverySnapshot.isMisdirected().set( true ); |
| } |
| else if( cargoIsNotMisrouted ) |
| { |
| ExpectedHandlingEvent expectedEvent = context.itinerary.expectedEventAfterReceive(); |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent ); |
| } |
| } |
| |
| // Deviation 4b |
| private void cargoLoaded() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.ONBOARD_CARRIER ); |
| context.newDeliverySnapshot.currentVoyage().set( currentVoyage ); |
| |
| if( cargoIsRerouted ) |
| { |
| // After a reroute following a load in an unexpected location, we expected the cargo to be |
| // unloaded at the unload location of the first leg of the new itinerary. |
| ExpectedHandlingEvent expectedEvent = context.itinerary |
| .expectedEventAfterLoadAt( lastKnownLocation ); |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent ); |
| } |
| else if( !context.itinerary.expectsLoad( lastKnownLocation, currentVoyage ) ) |
| { |
| context.newDeliverySnapshot.eta().set( null ); |
| context.newDeliverySnapshot.isMisdirected().set( true ); |
| } |
| else if( cargoIsNotMisrouted ) |
| { |
| ExpectedHandlingEvent expectedEvent = context.itinerary |
| .expectedEventAfterLoadAt( lastKnownLocation ); |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent ); |
| } |
| } |
| |
| // Deviation 4c |
| private void cargoUnloaded() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.IN_PORT ); |
| |
| if( cargoIsRerouted ) |
| { |
| // After a reroute following an unload in an unexpected location, we expected the cargo |
| // to be loaded onto a carrier at the first location of the new route specification. |
| ExpectedHandlingEvent expectedEvent = context.itinerary.expectedEventAfterReceive(); |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent ); |
| } |
| else if( !context.itinerary.expectsUnload( lastKnownLocation, currentVoyage ) ) |
| { |
| context.newDeliverySnapshot.eta().set( null ); |
| context.newDeliverySnapshot.isMisdirected().set( true ); |
| } |
| else if( cargoIsNotMisrouted ) |
| { |
| ExpectedHandlingEvent expectedEvent = context.itinerary |
| .expectedEventAfterUnloadAt( lastKnownLocation ); |
| context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent ); |
| |
| Location expectedDestination = ( (RouteSpecification) context.factory ).destination().get(); |
| context.newDeliverySnapshot |
| .isUnloadedAtDestination() |
| .set( lastKnownLocation.equals( expectedDestination ) ); |
| } |
| } |
| |
| // Deviation 4d |
| private void cargoInCustoms() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.IN_PORT ); |
| |
| Location expectedDestination = ( (RouteSpecification) context.factory ).destination().get(); |
| context.newDeliverySnapshot |
| .isUnloadedAtDestination() |
| .set( lastKnownLocation.equals( expectedDestination ) ); |
| } |
| |
| // Deviation 4e |
| private void cargoClaimed() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.CLAIMED ); |
| |
| Location expectedDestination = ( (RouteSpecification) context.factory ).destination().get(); |
| context.newDeliverySnapshot |
| .isUnloadedAtDestination() |
| .set( lastKnownLocation.equals( expectedDestination ) ); |
| |
| if( !context.itinerary.expectsDestination( lastKnownLocation ) ) |
| { |
| context.newDeliverySnapshot.eta().set( null ); |
| context.newDeliverySnapshot.isMisdirected().set( true ); |
| } |
| } |
| |
| // Deviation 4f |
| private void unknownHandlingEvent() |
| { |
| context.newDeliverySnapshot.transportStatus().set( TransportStatus.UNKNOWN ); |
| } |
| } |
| } |
| |
| /** |
| * The ItineraryRole supports the HandlingEventRole with calculated results derived from Itinerary Legs. |
| */ |
| @Mixins( ItineraryRole.Mixin.class ) |
| public interface ItineraryRole |
| { |
| void setContext( BuildDeliverySnapshot context ); |
| |
| Date eta(); |
| |
| boolean expectsOrigin( Location location ); |
| |
| boolean expectsLoad( Location location, Voyage voyage ); |
| |
| boolean expectsUnload( Location location, Voyage voyage ); |
| |
| boolean expectsDestination( Location location ); |
| |
| ExpectedHandlingEvent expectedEventAfterReceive(); |
| |
| ExpectedHandlingEvent expectedEventAfterLoadAt( Location lastLoadLocation ); |
| |
| ExpectedHandlingEvent expectedEventAfterUnloadAt( Location lastUnloadLocation ); |
| |
| class Mixin |
| extends RoleMixin<BuildDeliverySnapshot> |
| implements ItineraryRole |
| { |
| @This |
| Itinerary itinerary; |
| |
| public Date eta() |
| { |
| return itinerary.lastLeg().unloadTime().get(); |
| } |
| |
| // Route expectations ---------------------------------------------------- |
| |
| public boolean expectsOrigin( Location location ) |
| { |
| return itinerary.firstLeg().loadLocation().get().equals( location ); |
| } |
| |
| public boolean expectsLoad( Location location, Voyage voyage ) |
| { |
| // One leg with same load location and voyage |
| for( Leg leg : itinerary.legs().get() ) |
| { |
| if( leg.loadLocation().get().equals( location ) && leg.voyage().get().equals( voyage ) ) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean expectsUnload( Location location, Voyage voyage ) |
| { |
| // One leg with same unload location and voyage |
| for( Leg leg : itinerary.legs().get() ) |
| { |
| if( leg.unloadLocation().get().equals( location ) && leg.voyage().get().equals( voyage ) ) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean expectsDestination( Location location ) |
| { |
| // Last leg destination matches |
| return ( itinerary.lastLeg().unloadLocation().get().equals( location ) ); |
| } |
| |
| // Next expected handling event ---------------------------------------------- |
| |
| public ExpectedHandlingEvent expectedEventAfterReceive() |
| { |
| // After RECEIVE, expect LOAD location and voyage of first itinerary leg |
| final Leg firstLeg = itinerary.legs().get().iterator().next(); |
| return buildEvent( HandlingEventType.LOAD, firstLeg.loadLocation().get(), firstLeg.loadTime() |
| .get(), firstLeg.voyage().get() ); |
| } |
| |
| public ExpectedHandlingEvent expectedEventAfterLoadAt( Location lastLoadLocation ) |
| { |
| // After LOAD, expect UNLOAD location and voyage of same itinerary leg as LOAD |
| for( Leg leg : itinerary.legs().get() ) |
| { |
| if( leg.loadLocation().get().equals( lastLoadLocation ) ) |
| { |
| return buildEvent( HandlingEventType.UNLOAD, leg.unloadLocation().get(), leg.unloadTime() |
| .get(), leg.voyage().get() ); |
| } |
| } |
| return null; |
| } |
| |
| public ExpectedHandlingEvent expectedEventAfterUnloadAt( Location lastUnloadLocation ) |
| { |
| // After UNLOAD, expect LOAD location and voyage of following itinerary leg, or CLAIM if no more legs |
| for( Iterator<Leg> it = itinerary.legs().get().iterator(); it.hasNext(); ) |
| { |
| final Leg leg = it.next(); |
| if( leg.unloadLocation().get().equals( lastUnloadLocation ) ) |
| { |
| // Cargo has a matching unload location in itinerary |
| |
| if( it.hasNext() ) |
| { |
| // Cargo has not arrived yet (uncompleted legs in itinerary) |
| // We expect it to be loaded onto some Carrier |
| final Leg nextLeg = it.next(); |
| return buildEvent( HandlingEventType.LOAD, nextLeg.loadLocation().get(), nextLeg.loadTime() |
| .get(), nextLeg.voyage().get() ); |
| } |
| else |
| { |
| // Cargo has arrived (no more legs in itinerary) |
| // We expect it to be claimed by the customer |
| return buildEvent( HandlingEventType.CLAIM, leg.unloadLocation().get(), leg.unloadTime() |
| .get(), null ); |
| } |
| } |
| } |
| |
| // Itinerary doesn't recognize last unload location |
| return null; |
| } |
| |
| private ExpectedHandlingEvent buildEvent( HandlingEventType eventType, |
| Location location, |
| Date time, |
| Voyage voyage |
| ) |
| { |
| ValueBuilder<ExpectedHandlingEvent> builder = vbf.newValueBuilder( ExpectedHandlingEvent.class ); |
| builder.prototype().handlingEventType().set( eventType ); |
| builder.prototype().location().set( location ); |
| builder.prototype().time().set( time ); |
| builder.prototype().voyage().set( voyage ); |
| return builder.newInstance(); |
| } |
| } |
| } |
| } |