Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Creating an Uber Clone - Part XXVI - Transcript.pdf
1. Creating an Uber Clone - Part XXVI
Before we proceed into the actual driver app work & push notification we need to implement all the infrastructure in the server side. I also need to implement some
changes we need in the protocol.
2. private String hailingFrom;
private String hailingTo;
private String pushToken;
public RideDAO getRideDao() {
return new RideDAO(id, givenName, hailingFrom, hailingTo);
}
public UserDAO getDao() {
return new UserDAO(id, givenName, surname, phone, email,
facebookId, googleId, driver, car, currentRating,
latitude, longitude, direction, pushToken);
}
public UserDAO getPartialDao() {
return new UserDAO(id, givenName, surname, null, null,
null, null, driver, car, currentRating, latitude,
longitude, direction, pushToken);
}
User (Server)
Lets start by looking at the User class. I had to add 3 new fields and modify/add some methods.
hailingFrom & hailingTo allow us to communicate our trip details with the driver community
3. private String hailingFrom;
private String hailingTo;
private String pushToken;
public RideDAO getRideDao() {
return new RideDAO(id, givenName, hailingFrom, hailingTo);
}
public UserDAO getDao() {
return new UserDAO(id, givenName, surname, phone, email,
facebookId, googleId, driver, car, currentRating,
latitude, longitude, direction, pushToken);
}
public UserDAO getPartialDao() {
return new UserDAO(id, givenName, surname, null, null,
null, null, driver, car, currentRating, latitude,
longitude, direction, pushToken);
}
User (Server)
I need the pushToken of drivers so we can hail them directly from the app
4. private String hailingFrom;
private String hailingTo;
private String pushToken;
public RideDAO getRideDao() {
return new RideDAO(id, givenName, hailingFrom, hailingTo);
}
public UserDAO getDao() {
return new UserDAO(id, givenName, surname, phone, email,
facebookId, googleId, driver, car, currentRating,
latitude, longitude, direction, pushToken);
}
public UserDAO getPartialDao() {
return new UserDAO(id, givenName, surname, null, null,
null, null, driver, car, currentRating, latitude,
longitude, direction, pushToken);
}
User (Server)
I'll discuss the `RideDAO` class soon, it allows us to send details about the trip to drivers
5. private String hailingFrom;
private String hailingTo;
private String pushToken;
public RideDAO getRideDao() {
return new RideDAO(id, givenName, hailingFrom, hailingTo);
}
public UserDAO getDao() {
return new UserDAO(id, givenName, surname, phone, email,
facebookId, googleId, driver, car, currentRating,
latitude, longitude, direction, pushToken);
}
public UserDAO getPartialDao() {
return new UserDAO(id, givenName, surname, null, null,
null, null, driver, car, currentRating, latitude,
longitude, direction, pushToken);
}
User (Server)
Not much of a change but I added the pushToken to the UserDAO factory methods
6. public class RideDAO implements Serializable {
private long userId;
private String name;
private String from;
private String destination;
public RideDAO() {
}
public RideDAO(long userId, String name, String from, String destination) {
this.userId = userId;
this.name = name;
if(this.name == null) {
this.name = "[Unnamed User]";
}
this.from = from;
this.destination = destination;
}
// getters and setters trimmed out
}
RideDAO
The RideDAO class is a pretty trivial object. There's no point in enumerating the details of this class as it's pretty simple. The main usage for this is in the new RideService
which I will get to soon but first we need to discuss the Ride class.
7. @Entity
public class Ride {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@ManyToOne
private User passenger;
@ManyToOne
private User driver;
@OneToMany
@OrderBy("time ASC")
private Set<Waypoint> route;
private BigDecimal cost;
private String currency;
private boolean finished;
private boolean started;
// trimmed out constructors, getters and setters
}
Ride
Ride isn't as simple as RideDAO despite their common name. It contains far more information. Currently we don't use all of that but the fact that it's logged will let you
provide all of that information within the app or a management app easily.
The Ride class is a JPA entity similar to the User class.
I used an auto-increment value for the id instead of a random string. I wanted to keep things simple but notice this can expose a security vulnerability of scanning for
rides…
8. @Entity
public class Ride {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@ManyToOne
private User passenger;
@ManyToOne
private User driver;
@OneToMany
@OrderBy("time ASC")
private Set<Waypoint> route;
private BigDecimal cost;
private String currency;
private boolean finished;
private boolean started;
// trimmed out constructors, getters and setters
}
Ride
The passenger & driver are relational database references to the respective database objects representing each one of them
9. @Entity
public class Ride {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@ManyToOne
private User passenger;
@ManyToOne
private User driver;
@OneToMany
@OrderBy("time ASC")
private Set<Waypoint> route;
private BigDecimal cost;
private String currency;
private boolean finished;
private boolean started;
// trimmed out constructors, getters and setters
}
Ride
The route itself is a set of waypoints sorted by the time associated with the given waypoint. We'll discuss waypoints soon enough but technically it's just a set of
coordinates
10. @Entity
public class Ride {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@ManyToOne
private User passenger;
@ManyToOne
private User driver;
@OneToMany
@OrderBy("time ASC")
private Set<Waypoint> route;
private BigDecimal cost;
private String currency;
private boolean finished;
private boolean started;
// trimmed out constructors, getters and setters
}
Ride
I really oversimplified the cost field. It should work for sum and currency but it's usually not as simple as that. It's important to use something like BigDecimal and not
double when dealing with financial numbers as double is built for scientific usage and has rounding errors
11. @Entity
public class Ride {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@ManyToOne
private User passenger;
@ManyToOne
private User driver;
@OneToMany
@OrderBy("time ASC")
private Set<Waypoint> route;
private BigDecimal cost;
private String currency;
private boolean finished;
private boolean started;
// trimmed out constructors, getters and setters
}
Ride
We have two boolean flags, a ride is started once a passenger is picked up. It’s finished once he is dropped off or if the ride was canceled
12. public interface RideRepository extends CrudRepository<Ride, Long> {
@Query("select b from Ride b where b.finished = false and b.driver.id = ?1")
public List<Ride> findByNotFinishedUser(long id);
}
RideRepository
The companion CRUD RideRepository is pretty standard with one big exception. I added a special case finder that lets us locate the User that is currently hailing a car.
Notice the syntax b.driver.id = ?1 which points through the relation to the driver object.
13. @Entity
public class Waypoint {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private long time;
private double latitude;
private double longitude;
private float direction;
// trimmed out constructors, getters and setters
}
Waypoint
The Waypoint entity referenced from the Ride entity is pretty trivial. Notice we still need a unique id for a waypoint even if we don't actually use it in code...
The interesting part here is the time value which is the value of System.currentTimeMillis(). This allows us to build a path based on the time sequence. It will also allow us
to reconstruct a trip and generate additional details such as speed/cost if we wish to do that in the future.
Notice that there is also a WaypointRepository interface. I’m skipping it as it contains no actual code
14. @Service
public class RideService {
@Autowired
private UserRepository users;
@Autowired
private RideRepository rides;
@Transactional
public UserDAO hailCar(String token, boolean h, String from, String to) {
User u = users.findByAuthToken(token).get(0);
if(h) {
if(u.getAssignedUser() != null) {
long driverId = u.getAssignedUser();
u.setAssignedUser(null);
users.save(u);
User driver = users.findOne(driverId);
return driver.getPartialDao();
}
} else {
u.setAssignedUser(null);
}
u.setHailing(h);
u.setHailingFrom(from);
u.setHailingTo(to);
users.save(u);
return null;
}
RideService
The RideService class serves the same purpose as the UserService class focusing on rides and driver related features. I could have just stuck all of this logic into one
huge class but separating functionality to different service classes based on logic makes sense.
We manipulate both the rides and users CRUD objects from this class
15. @Service
public class RideService {
@Autowired
private UserRepository users;
@Autowired
private RideRepository rides;
@Transactional
public UserDAO hailCar(String token, boolean h, String from, String to) {
User u = users.findByAuthToken(token).get(0);
if(h) {
if(u.getAssignedUser() != null) {
long driverId = u.getAssignedUser();
u.setAssignedUser(null);
users.save(u);
User driver = users.findOne(driverId);
return driver.getPartialDao();
}
} else {
u.setAssignedUser(null);
}
u.setHailing(h);
u.setHailingFrom(from);
u.setHailingTo(to);
users.save(u);
return null;
}
RideService
Hailing is a transactional method, this means that all operations within the method will either succeed or fail depending on the outcome. This is important to prevent an
inconsistent state in the database
16. @Service
public class RideService {
@Autowired
private UserRepository users;
@Autowired
private RideRepository rides;
@Transactional
public UserDAO hailCar(String token, boolean h, String from, String to) {
User u = users.findByAuthToken(token).get(0);
if(h) {
if(u.getAssignedUser() != null) {
long driverId = u.getAssignedUser();
u.setAssignedUser(null);
users.save(u);
User driver = users.findOne(driverId);
return driver.getPartialDao();
}
} else {
u.setAssignedUser(null);
}
u.setHailing(h);
u.setHailingFrom(from);
u.setHailingTo(to);
users.save(u);
return null;
}
RideService
This method can be invoked to start and stop hailing. In this case we use the assigned user property to detect if a driver accepted the ride. If so we return the driver data
to the client
17. users.save(u);
return null;
}
public RideDAO getRideData(long userId) {
User u = users.findOne(userId);
if(u == null) {
return null;
}
return u.getRideDao();
}
@Transactional
public long acceptRide(String token, long userId) {
User driver = users.findByAuthToken(token).get(0);
User passenger = users.findOne(userId);
if(!passenger.isHailing()) {
throw new RuntimeException("Not hailing");
}
passenger.setHailing(false);
passenger.setAssignedUser(driver.getId());
driver.setAssignedUser(userId);
users.save(driver);
users.save(passenger);
Ride r = new Ride();
r.setDriver(driver);
r.setPassenger(passenger);
rides.save(r);
RideService
When a driver gets a notification of a ride he invokes this method to get back the data about the ride
18. users.save(u);
return null;
}
public RideDAO getRideData(long userId) {
User u = users.findOne(userId);
if(u == null) {
return null;
}
return u.getRideDao();
}
@Transactional
public long acceptRide(String token, long userId) {
User driver = users.findByAuthToken(token).get(0);
User passenger = users.findOne(userId);
if(!passenger.isHailing()) {
throw new RuntimeException("Not hailing");
}
passenger.setHailing(false);
passenger.setAssignedUser(driver.getId());
driver.setAssignedUser(userId);
users.save(driver);
users.save(passenger);
Ride r = new Ride();
r.setDriver(driver);
r.setPassenger(passenger);
rides.save(r);
RideService
If the driver wishes to accept the ride he invokes this transactional method. The method accepts the token from the driver and the id of the user hailing the ride. It creates
a new Ride entity and returns its ID, from this point on we need to refer to the Ride id and not the user id or token
19. User driver = users.findByAuthToken(token).get(0);
User passenger = users.findOne(userId);
if(!passenger.isHailing()) {
throw new RuntimeException("Not hailing");
}
passenger.setHailing(false);
passenger.setAssignedUser(driver.getId());
driver.setAssignedUser(userId);
users.save(driver);
users.save(passenger);
Ride r = new Ride();
r.setDriver(driver);
r.setPassenger(passenger);
rides.save(r);
return r.getId();
}
public void startRide(long rideId) {
Ride current = rides.findOne(rideId);
current.setStarted(true);
rides.save(current);
}
public void finishRide(long rideId) {
Ride current = rides.findOne(rideId);
current.setFinished(true);
rides.save(current);
}
}
RideService
Start ride and finish ride are invoked by the driver when he picks up the passenger and when he drops him off. Normally, finish ride should also handle elements like
billing etc. but I won't go into that now
20. @Controller
@RequestMapping("/ride")
public class RideWebservice {
@Autowired
private RideService rides;
@RequestMapping(method=RequestMethod.GET,value = "/get")
public @ResponseBody RideDAO getRideData(long id) {
return rides.getRideData(id);
}
@RequestMapping(method=RequestMethod.GET,value="/accept")
public @ResponseBody String acceptRide(@RequestParam(name="token", required = true) String token,
@RequestParam(name="userId", required = true) long userId) {
long val = rides.acceptRide(token, userId);
return "" + val;
}
@RequestMapping(method=RequestMethod.POST,value="/start")
public @ResponseBody String startRide(@RequestParam(name="id", required = true) long rideId) {
rides.startRide(rideId);
return "OK";
}
@RequestMapping(method=RequestMethod.POST,value="/finish")
public @ResponseBody String finishRide(@RequestParam(name="id", required = true) long rideId) {
rides.finishRide(rideId);
return "OK";
}
}
RideWebservice
The next step is bridging this to the user through a webservice...
The RideWebservice class exposes the RideService calls almost verbatim to the client.
The get call fetches the RideDAO for the given user id
21. @Controller
@RequestMapping("/ride")
public class RideWebservice {
@Autowired
private RideService rides;
@RequestMapping(method=RequestMethod.GET,value = "/get")
public @ResponseBody RideDAO getRideData(long id) {
return rides.getRideData(id);
}
@RequestMapping(method=RequestMethod.GET,value="/accept")
public @ResponseBody String acceptRide(@RequestParam(name="token", required = true) String token,
@RequestParam(name="userId", required = true) long userId) {
long val = rides.acceptRide(token, userId);
return "" + val;
}
@RequestMapping(method=RequestMethod.POST,value="/start")
public @ResponseBody String startRide(@RequestParam(name="id", required = true) long rideId) {
rides.startRide(rideId);
return "OK";
}
@RequestMapping(method=RequestMethod.POST,value="/finish")
public @ResponseBody String finishRide(@RequestParam(name="id", required = true) long rideId) {
rides.finishRide(rideId);
return "OK";
}
}
RideWebservice
Start and finish rides are again very simple with only one argument which is the ride id
22. public void updatePushToken(String token, String pushToken) {
User u = users.findByAuthToken(token).get(0);
u.setPushToken(pushToken);
users.save(u);
}
UserService
We also have to add some minor changes to the UserService and LocationService classes. Lets start with the UserService class. Drivers need a push token so we can
hail them. This is always set outside of the user creation code for two reasons.
The first time around the user is created but the push key isn't there yet (it arrives asynchronously)
Push is re-registered in every launch and refreshed, there is no reason to update the entire object for that
23. @RequestMapping(method = RequestMethod.GET,value = "/setPushToken")
public @ResponseBody String updatePushToken(
@RequestParam(name="token", required = true) String token,
@RequestParam(name="pushToken", required = true) String
pushToken) {
users.updatePushToken(token, pushToken);
return "OK";
}
UserWebservice
The UserWebservice class needs to mirror these changes obviously...
There isn't much here we just added a new setPushToken URL and we accept this update.
24. public class LocationService {
@Autowired
private UserRepository users;
@Autowired
private RideRepository rides;
@Autowired
private WaypointRepository waypoints;
public void updateUserLocation(String token,double lat,double lon,float dir) {
List<User> us = users.findByAuthToken(token);
User u = us.get(0);
u.setLatitude(lat);
u.setLongitude(lat);
u.setDirection(dir);
users.save(u);
if(u.isDriver() && u.getAssignedUser() != null) {
List<Ride> r = rides.findByNotFinishedUser(u.getId());
if(r != null && !r.isEmpty()) {
Ride ride = r.get(0);
if(ride.isStarted() && !ride.isFinished()) {
Set<Waypoint> route = ride.getRoute();
Waypoint newPosition = new Waypoint(
System.currentTimeMillis(), lat, lon, dir);
waypoints.save(newPosition);
route.add(newPosition);
ride.setRoute(route);
rides.save(ride);
LocationService
The LocationService needs a bit more work.
Every time we update a users location we check if he's a driver on a ride
25. public class LocationService {
@Autowired
private UserRepository users;
@Autowired
private RideRepository rides;
@Autowired
private WaypointRepository waypoints;
public void updateUserLocation(String token,double lat,double lon,float dir) {
List<User> us = users.findByAuthToken(token);
User u = us.get(0);
u.setLatitude(lat);
u.setLongitude(lat);
u.setDirection(dir);
users.save(u);
if(u.isDriver() && u.getAssignedUser() != null) {
List<Ride> r = rides.findByNotFinishedUser(u.getId());
if(r != null && !r.isEmpty()) {
Ride ride = r.get(0);
if(ride.isStarted() && !ride.isFinished()) {
Set<Waypoint> route = ride.getRoute();
Waypoint newPosition = new Waypoint(
System.currentTimeMillis(), lat, lon, dir);
waypoints.save(newPosition);
route.add(newPosition);
ride.setRoute(route);
rides.save(ride);
LocationService
Assuming we have a Ride object we check if this is currently an ongoing ride that wasn't finished
26. public class LocationService {
@Autowired
private UserRepository users;
@Autowired
private RideRepository rides;
@Autowired
private WaypointRepository waypoints;
public void updateUserLocation(String token,double lat,double lon,float dir) {
List<User> us = users.findByAuthToken(token);
User u = us.get(0);
u.setLatitude(lat);
u.setLongitude(lat);
u.setDirection(dir);
users.save(u);
if(u.isDriver() && u.getAssignedUser() != null) {
List<Ride> r = rides.findByNotFinishedUser(u.getId());
if(r != null && !r.isEmpty()) {
Ride ride = r.get(0);
if(ride.isStarted() && !ride.isFinished()) {
Set<Waypoint> route = ride.getRoute();
Waypoint newPosition = new Waypoint(
System.currentTimeMillis(), lat, lon, dir);
waypoints.save(newPosition);
route.add(newPosition);
ride.setRoute(route);
rides.save(ride);
LocationService
If so we add a waypoint to the ride and update it so we can later on inspect the path of the Ride.
This pretty much tracks rides seamlessly. If we wanted to be really smart we could detect the driver and user position to detect them traveling together and automatically
handle the ride. There are obviously problems with this as it means a user can't order a cab for someone else but it might be an interesting feature since we have two
close data points…