/*
* The CroudTrip! application aims at revolutionizing the car-ride-sharing market with its easy,
* user-friendly and highly automated way of organizing shared Trips. Copyright (C) 2015 Nazeeh Ammari,
* Philipp Eichhorn, Ricarda Hohn, Vanessa Lange, Alexander Popp, Frederik Simon, Michael Weber
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU
* Affero General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package org.croudtrip.rest;
import com.google.common.base.Optional;
import org.croudtrip.account.VehicleManager;
import org.croudtrip.api.account.User;
import org.croudtrip.api.account.Vehicle;
import org.croudtrip.api.directions.NavigationResult;
import org.croudtrip.api.trips.JoinTripRequest;
import org.croudtrip.api.trips.JoinTripRequestUpdate;
import org.croudtrip.api.trips.JoinTripRequestUpdateType;
import org.croudtrip.api.trips.JoinTripStatus;
import org.croudtrip.api.trips.RunningTripQuery;
import org.croudtrip.api.trips.SuperTrip;
import org.croudtrip.api.trips.SuperTripReservation;
import org.croudtrip.api.trips.SuperTripSubQuery;
import org.croudtrip.api.trips.TripOffer;
import org.croudtrip.api.trips.TripOfferDescription;
import org.croudtrip.api.trips.TripOfferStatus;
import org.croudtrip.api.trips.TripOfferUpdate;
import org.croudtrip.api.trips.TripQuery;
import org.croudtrip.api.trips.TripQueryDescription;
import org.croudtrip.api.trips.TripQueryResult;
import org.croudtrip.api.trips.TripReservation;
import org.croudtrip.directions.RouteNotFoundException;
import org.croudtrip.logs.LogManager;
import org.croudtrip.trips.RunningTripQueriesManager;
import org.croudtrip.trips.TripsManager;
import org.croudtrip.trips.TripsNavigationManager;
import org.croudtrip.trips.TripsUtils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import io.dropwizard.auth.Auth;
import io.dropwizard.hibernate.UnitOfWork;
/**
* Resource for managing trips.
*/
@Path("/trips")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class TripsResource {
private static final String
PATH_OFFERS = "/offers",
PATH_JOINS = "/joins",
PATH_SUPER_TRIP = "/superTrips",
PATH_ACCEPTED_JOINS = "/accepted",
PATH_QUERIES = "/queries",
PATH_RESERVATIONS = "/reservations";
private final TripsManager tripsManager;
private final TripsNavigationManager tripsNavigationManager;
private final RunningTripQueriesManager runningTripQueriesManager;
private final TripsUtils tripsUtils;
private final VehicleManager vehicleManager;
private final LogManager logManager;
@Inject
TripsResource(
TripsManager tripsManager,
TripsNavigationManager tripsNavigationManager,
RunningTripQueriesManager runningTripQueriesManager,
TripsUtils tripsUtils,
VehicleManager vehicleManager,
LogManager logManager) {
this.tripsManager = tripsManager;
this.tripsNavigationManager = tripsNavigationManager;
this.runningTripQueriesManager = runningTripQueriesManager;
this.tripsUtils = tripsUtils;
this.vehicleManager = vehicleManager;
this.logManager = logManager;
}
/**
* Adds a new {@link TripOffer} which passengers can query for.
* @return the newly created offer (which also includes the offer id!).
*/
@POST
@UnitOfWork
@Path(PATH_OFFERS)
public TripOffer addOffer(@Auth User driver, @Valid TripOfferDescription offerDescription) throws RouteNotFoundException {
logManager.d("Add offer");
Optional<Vehicle> vehicle = vehicleManager.findVehicleById(offerDescription.getVehicleId());
if (!vehicle.isPresent() || vehicle.get().getOwner().getId() != driver.getId()) {
throw RestUtils.createNotFoundException("not vehicle with id " + offerDescription.getVehicleId() + " found");
}
return tripsManager.addOffer(driver, offerDescription);
}
/**
* Find an offer by id.
*/
@GET
@Path(PATH_OFFERS + "/{offerId}")
@UnitOfWork
public TripOffer getOffer(@PathParam("offerId") long offerId) {
return assertIsValidOfferId(offerId);
}
/**
* Get the {@link org.croudtrip.api.directions.NavigationResult} for an offer
* The result will contain a complete route visiting all the passengers pick-up and destination
* locations as well as a list of all the waypoints in the correct order of the current trip.
*/
@GET
@Path(PATH_OFFERS + "/{offerId}/navigation")
@UnitOfWork
public NavigationResult computeNavigationResultForOffer(@PathParam("offerId") long offerId) throws RouteNotFoundException {
TripOffer offer = assertIsValidOfferId(offerId);
return tripsNavigationManager.getNavigationResultForOffer(offer);
}
/**
* Get the {@link org.croudtrip.api.directions.NavigationResult} for an offer containing a not
* yet accepted additionally provided {@link JoinTripRequest}. The result will contain a complete
* route visiting all the passengers pick-up and destination locations as well as a list of all
* the waypoints in the correct order of the current trip.
* @param offerId The offer the navigation result should be computed for
* @param joinRequestId the not yet accepted join trip request ({@link JoinTripStatus#PASSENGER_ACCEPTED})
* which should be included into the navigation result.
* @return A navigation result that contains the route and waypoints for all the passengers
* especially of the additionally provided request.
*/
@GET
@Path(PATH_OFFERS + "/{offerId}/navigation/{joinRequestId}")
@UnitOfWork
public NavigationResult computePendingNavigationResultForOffer(@PathParam("offerId") long offerId, @PathParam("joinRequestId") long joinRequestId) throws RouteNotFoundException {
TripOffer offer = assertIsValidOfferId( offerId );
Optional<JoinTripRequest> request = tripsManager.findJoinRequest(joinRequestId);
if (!request.isPresent()) throw RestUtils.createNotFoundException();
if( request.get().getStatus() != JoinTripStatus.PASSENGER_ACCEPTED )
throw RestUtils.createJsonFormattedException("Request must have status PASSENGER_ACCEPTED.", 409);
SuperTripSubQuery subQuery = request.get().getSubQuery();
TripQuery origQuery = request.get().getSuperTrip().getQuery();
TripQuery query = new TripQuery( null, subQuery.getStartLocation(), subQuery.getDestinationLocation(), origQuery.getMaxWaitingTimeInSeconds(), origQuery.getCreationTimestamp(), origQuery.getPassenger() );
return tripsNavigationManager.getNavigationResultForOffer( offer, query );
}
/**
* Find all offers that belong to a particular driver.
* @param showActiveAndNotFullOnly if false this method will return all offers that belong to a driver,
* otherwise only those which are active (not disable due to timeouts etc.)
* and are not full (passengers can still join). Default is true.
*/
@GET
@Path(PATH_OFFERS)
@UnitOfWork
public List<TripOffer> getOffers(@Auth User driver, @DefaultValue("true") @QueryParam("active") boolean showActiveAndNotFullOnly) {
List<TripOffer> offers = new ArrayList<>(tripsManager.findOffersByDriver(driver));
if (!showActiveAndNotFullOnly) return offers;
// filter by active status
Iterator<TripOffer> iterator = offers.iterator();
while (iterator.hasNext()) {
TripOffer offer = iterator.next();
if (!offer.getStatus().equals(TripOfferStatus.ACTIVE)) iterator.remove();
else if (tripsUtils.getActivePassengerCountForOffer(offer) >= offer.getVehicle().getCapacity()) iterator.remove();
}
return offers;
}
/**
* Find all active offers that belong to a particular driver.
* Even offers with a full car will be shown. Call this method if you want to get active running
* offers of a driver. Also {@link org.croudtrip.api.trips.TripOfferStatus#DISABLED} offers will
* be shown, since the offer is basically active (from the point of view of the driver), but
* needs to be enabled again.
*/
@GET
@Path("/active_offers")
@UnitOfWork
public List<TripOffer> getActiveOffers(@Auth User driver ) {
List<TripOffer> offers = new ArrayList<>(tripsManager.findOffersByDriver(driver));
// filter by active status
Iterator<TripOffer> iterator = offers.iterator();
while (iterator.hasNext()) {
TripOffer offer = iterator.next();
if (!offer.getStatus().equals(TripOfferStatus.ACTIVE) && !offer.getStatus().equals(TripOfferStatus.DISABLED)) {
iterator.remove();
}
}
return offers;
}
/**
* Allows drivers to update their offers, e.g. update the drivers current location (which changes
* what passengers can join a trip).
*/
@PUT
@Path(PATH_OFFERS + "/{offerId}")
@UnitOfWork
public TripOffer updateOffer(@Auth User driver, @PathParam("offerId") long offerId, @Valid TripOfferUpdate offerUpdate) throws RouteNotFoundException {
// find offer
TripOffer offer = assertIsValidOfferId(offerId);
if (offer.getDriver().getId() != driver.getId()) throw RestUtils.createUnauthorizedException();
// check passenger status
if (offerUpdate.getFinishOffer()) {
if (tripsUtils.getActivePassengerCountForOffer(offer) > 0) {
throw RestUtils.createJsonFormattedException("there are still passengers that need to be taken care of first!", 400);
}
} else if (offerUpdate.getCancelOffer()) {
for (JoinTripRequest request : tripsManager.findAllJoinRequests(offerId)) {
JoinTripStatus status = request.getStatus();
if (status.equals(JoinTripStatus.PASSENGER_IN_CAR)) {
throw RestUtils.createJsonFormattedException("are you looking to the eject passenger button?", 400);
}
}
}
return tripsManager.updateOffer(offer, offerUpdate);
}
/**
* Removes an offer. You should probably NOT use this method, please consider finishing or canceling
* the trip instead.
*/
@DELETE
@UnitOfWork
@Path(PATH_OFFERS + "/{offerId}")
public void deleteOffer(@PathParam("offerId") long offerId) {
tripsManager.deleteOffer(assertIsValidOfferId(offerId));
}
/**
* Allows passengers to query for offers.
* @return the result will EITHER contain a list of {@link TripReservation} which indicate that
* trips have been found, OR a {@link RunningTripQuery} which means that a background search has
* been started. In the latter case passengers will receive a GCM notification once an offer
* has been found.
*/
@POST
@UnitOfWork
@Path(PATH_QUERIES)
public TripQueryResult queryOffers(@Auth User passenger, @Valid TripQueryDescription requestDescription) throws RouteNotFoundException {
return tripsManager.queryOffers(passenger, requestDescription);
}
/**
* Returns all running background queries for a passenger.
* @param showOnlyRunning if true this method will return only running background queries but not
* those which have been cancelled, finished, etc. Default is false.
*/
@GET
@UnitOfWork
@Path(PATH_QUERIES)
public List<RunningTripQuery> getQueries(@Auth User passenger, @DefaultValue("false") @QueryParam("running") boolean showOnlyRunning) {
return runningTripQueriesManager.getRunningQueries(passenger, showOnlyRunning);
}
/**
* Returns a single running background query for a passenger.
*/
@GET
@UnitOfWork
@Path(PATH_QUERIES + "/{queryId}")
public RunningTripQuery getQuery(@Auth User passenger, @PathParam("queryId") long queryId) {
Optional<RunningTripQuery> query = runningTripQueriesManager.getRunningQuery(queryId);
if (!query.isPresent()) throw RestUtils.createNotFoundException();
if (query.get().getQuery().getPassenger().getId() != passenger.getId()) throw RestUtils.createUnauthorizedException();
return query.get();
}
/**
* Stops one particular background query for a passenger.
*/
@DELETE
@UnitOfWork
@Path(PATH_QUERIES + "/{queryId}")
public void deleteQuery(@Auth User passenger, @PathParam("queryId") long queryId) {
runningTripQueriesManager.deleteRunningQuery(getQuery(passenger, queryId));
}
/**
* Returns all trip reservations ever made.
*/
@GET
@UnitOfWork
@Path(PATH_RESERVATIONS)
public List<SuperTripReservation> getReservations() {
return tripsManager.findAllReservations();
}
/**
* Returns one trip price reservation.
*/
@GET
@UnitOfWork
@Path(PATH_RESERVATIONS + "/{reservationId}")
public SuperTripReservation getReservation(@PathParam("reservationId") long reservationId) {
Optional<SuperTripReservation> reservation = tripsManager.findReservation(reservationId);
if (!reservation.isPresent()) throw RestUtils.createNotFoundException();
return reservation.get();
}
/**
* Allows passengers to join a trip by "accepting" a previously created {@link TripReservation}.
* @return the resulting join requests which indicates all further stages of a trip.
*/
@PUT
@UnitOfWork
@Path(PATH_RESERVATIONS + "/{reservationId}")
public SuperTrip joinTrip(@Auth User passenger, @PathParam("reservationId") long reservationId) {
Optional<SuperTripReservation> reservation = tripsManager.findReservation(reservationId);
if (!reservation.isPresent()) throw RestUtils.createNotFoundException("reservation does not exist");
Optional<SuperTrip> joinRequest = tripsManager.joinTrip(reservation.get());
if (!joinRequest.isPresent()) throw RestUtils.createNotFoundException("offer does not exist");
return joinRequest.get();
}
/**
* Returns a single trip, assuming this trip belongs to the authenticated passenger.
* @param tripId the id of the trip to fetch
*/
@GET
@UnitOfWork
@Path(PATH_SUPER_TRIP + "/{tripId}")
public SuperTrip getTrip(@Auth User passenger, @PathParam("tripId") long tripId) {
return assertIsValidTripId(tripId, passenger);
}
/**
* Returns all trips for a single passenger.
*/
@GET
@UnitOfWork
@Path(PATH_SUPER_TRIP)
public List<SuperTrip> getAllTrips(@Auth User passenger) {
return tripsManager.findAllTrips(passenger);
}
/**
* Returns all active (not cancelled by the passenger, passenger has not reached his destination)
* {@link SuperTrip}s that belong to a passenger.
* Ideally the count of active SuperTrips should be not greater than 1.
*/
@GET
@UnitOfWork
@Path(PATH_SUPER_TRIP + "/active")
public List<SuperTrip> getAllActiveTrips( @Auth User passenger ) {
return tripsManager.findAllActiveTrips(passenger);
}
/**
* Returns all {@link JoinTripRequest} that belong to one {@link SuperTrip}.
*/
@GET
@UnitOfWork
@Path(PATH_SUPER_TRIP + "/{tripId}/joins")
public List<JoinTripRequest> getJoinTripRequestsForSuperTrip(@Auth User user, @PathParam("tripId") long tripId) {
return assertIsValidTripId(tripId, user).getJoinRequests();
}
/**
* Returns a list of join requests for either passengers or drivers.
* @param showOnlyPassengerAccepted if true this method will only return those join requests with a
* status of {@link JoinTripStatus#PASSENGER_ACCEPTED}.
* Default is false.
*/
@GET
@UnitOfWork
@Path(PATH_JOINS)
public List<JoinTripRequest> getJoinRequests(@Auth User passengerOrDriver, @DefaultValue("false") @QueryParam("open") boolean showOnlyPassengerAccepted) {
return tripsManager.findAllJoinRequests(passengerOrDriver, showOnlyPassengerAccepted);
}
/**
* Returns a list of join requests for either passengers or drivers where the status of the join
* request if {@link JoinTripStatus#DRIVER_ACCEPTED}.
*/
@GET
@UnitOfWork
@Path(PATH_ACCEPTED_JOINS)
public List<JoinTripRequest> getDriverAcceptedJoinRequests(@Auth User passengerOrDriver ) {
return tripsManager.findDriverAcceptedJoinRequests(passengerOrDriver);
}
/**
* Gets one particular join request.
*/
@GET
@UnitOfWork
@Path(PATH_JOINS + "/{joinRequestId}")
public JoinTripRequest getJoinRequest(@Auth User driverOrPassenger, @PathParam("joinRequestId") long joinRequestId) {
Optional<JoinTripRequest> request = tripsManager.findJoinRequest(joinRequestId);
if (!request.isPresent()) throw RestUtils.createNotFoundException();
else return request.get();
}
/**
* Computes the diversion in seconds for a driver if he accepts the query of the {@link org.croudtrip.api.trips.JoinTripRequest}.
* @param driverOrPassenger a valid user
* @param joinRequestId the id of the JoinTripRequest.
* @return the diversion in seconds the driver has to take to pick up the passenger of this request.
* @throws RouteNotFoundException if there is no route for the driver or the passenger.
*/
@GET
@UnitOfWork
@Path(PATH_JOINS + "/{joinRequestId}/diversionInSeconds")
public Long getDiversionInSecondsForJoinRequest( @Auth User driverOrPassenger, @PathParam("joinRequestId") long joinRequestId ) throws RouteNotFoundException {
// TODO: maybe only allow actual participants of the request to see this information, but for testing this is good enough
Optional<JoinTripRequest> joinRequest = tripsManager.findJoinRequest(joinRequestId);
if (!joinRequest.isPresent()) throw RestUtils.createNotFoundException();
NavigationResult actualOfferNavigationResult = tripsNavigationManager.getNavigationResultForOffer( joinRequest.get().getOffer() );
NavigationResult diversionOfferNavigationResult = tripsNavigationManager.getNavigationResultForOffer(joinRequest.get().getOffer(), joinRequest.get().getSuperTrip().getQuery());
return diversionOfferNavigationResult.getRoute().getDurationInSeconds() - actualOfferNavigationResult.getRoute().getDurationInSeconds();
}
/**
* Computes the diversion in meters for a driver if he accepts the query of the {@link org.croudtrip.api.trips.JoinTripRequest}.
* @param driverOrPassenger a valid user
* @param joinRequestId the id of the JoinTripRequest.
* @return the diversion in meters the driver has to take to pick up the passenger of this request.
* @throws RouteNotFoundException if there is no route for the driver or the passenger.
*/
@GET
@UnitOfWork
@Path(PATH_JOINS + "/{joinRequestId}/diversionInMeters")
public Long getDiversionInMetersForJoinRequest( @Auth User driverOrPassenger, @PathParam("joinRequestId") long joinRequestId ) throws RouteNotFoundException {
// TODO: maybe only allow actual participants of the request to see this information, but for testing this is good enough
Optional<JoinTripRequest> joinRequest = tripsManager.findJoinRequest(joinRequestId);
if (!joinRequest.isPresent()) throw RestUtils.createNotFoundException();
NavigationResult actualOfferNavigationResult = tripsNavigationManager.getNavigationResultForOffer( joinRequest.get().getOffer() );
NavigationResult diversionOfferNavigationResult = tripsNavigationManager.getNavigationResultForOffer( joinRequest.get().getOffer(), joinRequest.get().getSuperTrip().getQuery() );
return diversionOfferNavigationResult.getRoute().getDistanceInMeters() - actualOfferNavigationResult.getRoute().getDistanceInMeters();
}
/**
* Updates a join request. Use this method if you want to advance the state of a join request,
* e.g. passenger enters / leaves car.
*/
@PUT
@UnitOfWork
@Path(PATH_JOINS + "/{joinRequestId}")
public JoinTripRequest updateJoinRequest(
@Auth User driverOrPassenger,
@PathParam("joinRequestId") long joinRequestId,
JoinTripRequestUpdate update) {
Optional<JoinTripRequest> joinRequest = tripsManager.findJoinRequest(joinRequestId);
if (!joinRequest.isPresent()) throw RestUtils.createNotFoundException();
JoinTripStatus status = joinRequest.get().getStatus();
switch(update.getType()) {
case ACCEPT_PASSENGER:
case DECLINE_PASSENGER:
if (!status.equals(JoinTripStatus.PASSENGER_ACCEPTED))
throw RestUtils.createJsonFormattedException("status must be " + JoinTripStatus.PASSENGER_ACCEPTED, 409);
if (driverOrPassenger.getId() != joinRequest.get().getOffer().getDriver().getId()) {
throw RestUtils.createJsonFormattedException("only driver can take this action", 400);
}
return tripsManager.updateJoinRequestAcceptance(joinRequest.get(), update.getType().equals(JoinTripRequestUpdateType.ACCEPT_PASSENGER));
case ENTER_CAR:
if (!status.equals(JoinTripStatus.DRIVER_ACCEPTED))
throw RestUtils.createJsonFormattedException("status must be " + JoinTripStatus.DRIVER_ACCEPTED, 409);
assertUserIsPassenger(joinRequest.get(), driverOrPassenger);
return tripsManager.updateJoinRequestPassengerEnterCar(joinRequest.get());
case LEAVE_CAR:
if (!status.equals(JoinTripStatus.PASSENGER_IN_CAR))
throw RestUtils.createJsonFormattedException("status must be " + JoinTripStatus.PASSENGER_IN_CAR, 409);
assertUserIsPassenger(joinRequest.get(), driverOrPassenger);
return tripsManager.updateJoinRequestPassengerExitCar(joinRequest.get());
case CANCEL:
if (status.equals(JoinTripStatus.PASSENGER_IN_CAR) || status.equals(JoinTripStatus.PASSENGER_AT_DESTINATION))
throw RestUtils.createJsonFormattedException("cannot cancel when in car or at destination", 409);
assertUserIsPassenger(joinRequest.get(), driverOrPassenger);
return tripsManager.updateJoinRequestPassengerCancel(joinRequest.get());
}
throw RestUtils.createJsonFormattedException("unknown update type " + update.getType(), 400);
}
@PUT
@UnitOfWork
@Path(PATH_SUPER_TRIP + "/{superTripId}")
public SuperTrip cancelSuperTrip( @Auth User passenger, @PathParam("superTripId") long superTripId ) {
SuperTrip superTrip = assertIsValidTripId(superTripId, passenger);
for( JoinTripRequest request : superTrip.getJoinRequests() ) {
JoinTripStatus status = request.getStatus();
if (status.equals(JoinTripStatus.PASSENGER_IN_CAR) || status.equals(JoinTripStatus.PASSENGER_AT_DESTINATION))
throw RestUtils.createJsonFormattedException("cannot cancel when in car or at destination", 409);
assertUserIsPassenger(request, passenger);
}
return tripsManager.updateSuperTripPassengerCancel(superTrip);
}
@PUT
@UnitOfWork
@Path(PATH_SUPER_TRIP + "/cancel")
public void cancelActiveSuperTrips( @Auth User passenger ){
List<SuperTrip> superTrips = tripsManager.findAllActiveTrips(passenger);
for( SuperTrip superTrip : superTrips ) {
for( JoinTripRequest request : superTrip.getJoinRequests() ) {
JoinTripStatus status = request.getStatus();
if (status.equals(JoinTripStatus.PASSENGER_IN_CAR) || status.equals(JoinTripStatus.PASSENGER_AT_DESTINATION))
throw RestUtils.createJsonFormattedException("cannot cancel when in car or at destination", 409);
assertUserIsPassenger(request, passenger);
}
tripsManager.updateSuperTripPassengerCancel(superTrip);
}
}
private void assertUserIsPassenger(JoinTripRequest request, User user) {
if (user.getId() != request.getSuperTrip().getQuery().getPassenger().getId()) {
throw RestUtils.createJsonFormattedException("only passenger can take this action", 400);
}
}
private TripOffer assertIsValidOfferId(long offerId) {
Optional<TripOffer> offer = tripsManager.findOffer(offerId);
if (offer.isPresent()) return offer.get();
else throw RestUtils.createNotFoundException();
}
private SuperTrip assertIsValidTripId(long tripId, User passenger) {
Optional<SuperTrip> trip = tripsManager.findTrip(tripId);
if (!trip.isPresent()) throw RestUtils.createNotFoundException();
if (passenger.getId() != trip.get().getQuery().getPassenger().getId()) throw RestUtils.createUnauthorizedException();
return trip.get();
}
}