/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package net.java.cargotracker.domain.model.cargo; import java.io.Serializable; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import net.java.cargotracker.domain.model.handling.HandlingEvent; import net.java.cargotracker.domain.model.handling.HandlingHistory; import net.java.cargotracker.domain.model.location.Location; import net.java.cargotracker.domain.shared.DomainObjectUtils; import org.apache.commons.lang3.Validate; /** * A Cargo. This is the central class in the domain model, and it is the root of * the Cargo-Itinerary-Leg-Delivery-RouteSpecification aggregate. * * A cargo is identified by a unique tracking id, and it always has an origin * and a route specification. The life cycle of a cargo begins with the booking * procedure, when the tracking id is assigned. During a (short) period of time, * between booking and initial routing, the cargo has no itinerary. * * The booking clerk requests a list of possible routes, matching the route * specification, and assigns the cargo to one route. The route to which a cargo * is assigned is described by an itinerary. * * A cargo can be re-routed during transport, on demand of the customer, in * which case a new route is specified for the cargo and a new route is * requested. The old itinerary, being a value object, is discarded and a new * one is attached. * * It may also happen that a cargo is accidentally misrouted, which should * notify the proper personnel and also trigger a re-routing procedure. * * When a cargo is handled, the status of the delivery changes. Everything about * the delivery of the cargo is contained in the Delivery value object, which is * replaced whenever a cargo is handled by an asynchronous event triggered by * the registration of the handling event. * * The delivery can also be affected by routing changes, i.e. when a the route * specification changes, or the cargo is assigned to a new route. In that case, * the delivery update is performed synchronously within the cargo aggregate. * * The life cycle of a cargo ends when the cargo is claimed by the customer. * * The cargo aggregate, and the entire domain model, is built to solve the * problem of booking and tracking cargo. All important business rules for * determining whether or not a cargo is misdirected, what the current status of * the cargo is (on board carrier, in port etc), are captured in this aggregate. */ @Entity @NamedQueries({ @NamedQuery(name = "Cargo.findAll", query = "Select c from Cargo c"), @NamedQuery(name = "Cargo.findByTrackingId", query = "Select c from Cargo c where c.trackingId = :trackingId")}) public class Cargo implements Serializable { private static final long serialVersionUID = 1L; // Auto-generated surrogate key @Id @GeneratedValue private Long id; @Embedded private TrackingId trackingId; @ManyToOne @JoinColumn(name = "origin_id", updatable = false) private Location origin; @Embedded private RouteSpecification routeSpecification; @Embedded // This should be nullable: https://java.net/jira/browse/JPA_SPEC-42 private Itinerary itinerary; @Embedded private Delivery delivery; public Cargo() { // Nothing to initialize. } public Cargo(TrackingId trackingId, RouteSpecification routeSpecification) { Validate.notNull(trackingId, "Tracking ID is required"); Validate.notNull(routeSpecification, "Route specification is required"); this.trackingId = trackingId; // Cargo origin never changes, even if the route specification changes. // However, at creation, cargo orgin can be derived from the initial // route specification. this.origin = routeSpecification.getOrigin(); this.routeSpecification = routeSpecification; this.delivery = Delivery.derivedFrom(this.routeSpecification, this.itinerary, HandlingHistory.EMPTY); this.itinerary = Itinerary.EMPTY_ITINERARY; } public TrackingId getTrackingId() { return trackingId; } public void setOrigin(Location origin) { this.origin = origin; } public Location getOrigin() { return origin; } public RouteSpecification getRouteSpecification() { return routeSpecification; } /** * @return The delivery. Never null. */ public Delivery getDelivery() { return delivery; } /** * @return The itinerary. Never null. */ public Itinerary getItinerary() { return DomainObjectUtils.nullSafe(this.itinerary, Itinerary.EMPTY_ITINERARY); } /** * Specifies a new route for this cargo. */ public void specifyNewRoute(RouteSpecification routeSpecification) { Validate.notNull(routeSpecification, "Route specification is required"); this.routeSpecification = routeSpecification; // Handling consistency within the Cargo aggregate synchronously this.delivery = delivery.updateOnRouting(this.routeSpecification, this.itinerary); } public void assignToRoute(Itinerary itinerary) { Validate.notNull(itinerary, "Itinerary is required for assignment"); this.itinerary = itinerary; // Handling consistency within the Cargo aggregate synchronously this.delivery = delivery.updateOnRouting(this.routeSpecification, this.itinerary); } /** * Updates all aspects of the cargo aggregate status based on the current * route specification, itinerary and handling of the cargo. * <p/> * When either of those three changes, i.e. when a new route is specified * for the cargo, the cargo is assigned to a route or when the cargo is * handled, the status must be re-calculated. * <p/> * {@link RouteSpecification} and {@link Itinerary} are both inside the * Cargo aggregate, so changes to them cause the status to be updated * <b>synchronously</b>, but changes to the delivery history (when a cargo * is handled) cause the status update to happen <b>asynchronously</b> since * {@link HandlingEvent} is in a different aggregate. * * @param handlingHistory handling history */ public void deriveDeliveryProgress(HandlingHistory handlingHistory) { this.delivery = Delivery.derivedFrom(getRouteSpecification(), getItinerary(), handlingHistory); } /** * @param object to compare * @return True if they have the same identity * @see #sameIdentityAs(Cargo) */ @Override public boolean equals(Object object) { if (this == object) { return true; } if (object == null || getClass() != object.getClass()) { return false; } Cargo other = (Cargo) object; return sameIdentityAs(other); } private boolean sameIdentityAs(Cargo other) { return other != null && trackingId.sameValueAs(other.trackingId); } /** * @return Hash code of tracking id. */ @Override public int hashCode() { return trackingId.hashCode(); } @Override public String toString() { return trackingId.toString(); } }