Java tutorial
/* * This file is part of Transitime.org * * Transitime.org is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License (GPL) as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Transitime.org 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Transitime.org . If not, see <http://www.gnu.org/licenses/>. */ package org.transitime.db.structs; import java.io.Serializable; import java.util.Date; import java.util.Iterator; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; import org.hibernate.CallbackException; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.annotations.DynamicUpdate; import org.hibernate.classic.Lifecycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.transitime.applications.Core; import org.transitime.configData.DbSetupConfig; import org.transitime.core.TemporalDifference; import org.transitime.db.hibernate.HibernateUtils; import org.transitime.utils.Geo; import org.transitime.utils.IntervalTimer; import org.transitime.utils.Time; /** * For persisting an Arrival or a Departure time. Should use Arrival or * Departure subclasses. * <p> * Implements Lifecycle so that can have the onLoad() callback be called when * reading in data so that can intern() member strings. In order to do this the * String members could not be declared as final since they are updated after * the constructor is called. By interning the member strings less than half * (about 40%) of the RAM to be used. This is very important when reading in * large batches of ArrivalDeparture objects! * * @author SkiBu Smith */ @Entity @DynamicUpdate @Table(name = "ArrivalsDepartures", indexes = { @Index(name = "ArrivalsDeparturesTimeIndex", columnList = "time") }) public class ArrivalDeparture implements Lifecycle, Serializable { @Id @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String vehicleId; // Originally did not use msec precision (datetime(3)) specification // because arrival/departure times are only estimates and having such // precision is not generally appropriate. But found that then some // arrival and departures for a stop would have the same time and when // one would query for the arrivals/departures and order by time one // could get a departure before an arrival. To avoid this kind of // incorrect ordering using the additional precision. And this way // don't have to add an entire second to a departure time to make // sure that it is after the arrival. Adding a second is an // exaggeration because it implies the vehicle was stopped for a second // when most likely it zoomed by the stop. It looks better to add // only a msec to make the departure after the arrival. @Id @Column @Temporal(TemporalType.TIMESTAMP) private final Date time; @Id @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String stopId; // From the GTFS stop_times.txt file for the trip. The gtfsStopSeq can // be different from stopPathIndex. The stopIndex is included here so that // it is easy to find the corresponding stop in the stop_times.txt file. // It needs to be part of the @Id because can have loops for a route // such that a stop is served twice on a trip. Otherwise would get a // constraint violation. @Id @Column private final int gtfsStopSeq; @Id @Column private final boolean isArrival; @Id @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String tripId; // The revision of the configuration data that was being used @Column final int configRev; // So can match the ArrivalDeparture time to the AvlReport that // generated it by using vehicleId and avlTime. @Column @Temporal(TemporalType.TIMESTAMP) private final Date avlTime; // The schedule time will only be set if the schedule info was available // from the GTFS data and it is the proper type of arrival or departure // stop (there is an arrival schedule time and this is the last stop for // a trip and and this is an arrival time OR there is a departure schedule // time and this is not the last stop for a trip and this is a departure // time. Otherwise will be null. @Column @Temporal(TemporalType.TIMESTAMP) private final Date scheduledTime; @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String blockId; @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String routeId; // routeShortName is included because for some agencies the // route_id changes when there are schedule updates. But the // routeShortName is more likely to stay consistent. Therefore // it is better for when querying for arrival/departure data // over a timespan. @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String routeShortName; @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String serviceId; @Column(length = HibernateUtils.DEFAULT_ID_SIZE) private String directionId; // The index of which trip this is within the block. @Column private final int tripIndex; // The index of which stop path this is within the trip. // Different from the GTFS gtfsStopSeq. The stopPathIndex starts // at 0 and increments by one for every stop. The GTFS gtfsStopSeq // on the other hand doesn't need to be sequential. @Column private final int stopPathIndex; // Sometimes want to look at travel times using arrival/departure times. // This would be complicated if had to get the path length by using // tripIndex to determine trip to determine trip pattern to determine // StopPath to determine length. So simply storing the stop path // length along with arrivals/departures so that it is easy to obtain // for post-processing. @Column private final float stopPathLength; // So can easily create copy constructor withUpdatedTime() @Transient private final Block block; // Needed because some methods need to know if dealing with arrivals or // departures. public enum ArrivalsOrDepartures { ARRIVALS, DEPARTURES }; private static final Logger logger = LoggerFactory.getLogger(ArrivalDeparture.class); // Needed because Hibernate objects must be serializable private static final long serialVersionUID = 6511713704337986699L; /********************** Member Functions **************************/ /** * Constructor called when creating an ArrivalDeparture object to be * stored in db. * * @param vehicleId * @param time * @param avlTime * @param block * @param tripIndex * @param stopPathIndex * @param isArrival */ protected ArrivalDeparture(String vehicleId, Date time, Date avlTime, Block block, int tripIndex, int stopPathIndex, boolean isArrival) { this.vehicleId = vehicleId; this.time = time; this.avlTime = avlTime; this.block = block; this.tripIndex = tripIndex; this.stopPathIndex = stopPathIndex; this.isArrival = isArrival; this.configRev = Core.getInstance().getDbConfig().getConfigRev(); // Some useful convenience variables Trip trip = block.getTrip(tripIndex); StopPath stopPath = trip.getStopPath(stopPathIndex); String stopId = stopPath.getStopId(); // Determine the schedule time, which is a bit complicated. // Of course, only do this for schedule based assignments. // The schedule time will only be set if the schedule info was available // from the GTFS data and it is the proper type of arrival or departure // stop (there is an arrival schedule time and this is the last stop for // a trip and and this is an arrival time OR there is a departure schedule // time and this is not the last stop for a trip and this is a departure // time. Date scheduledEpochTime = null; if (!trip.isNoSchedule()) { ScheduleTime scheduleTime = trip.getScheduleTime(stopPathIndex); if (stopPath.isLastStopInTrip() && scheduleTime.getArrivalTime() != null && isArrival) { long epochTime = Core.getInstance().getTime().getEpochTime(scheduleTime.getArrivalTime(), time); scheduledEpochTime = new Date(epochTime); } else if (!stopPath.isLastStopInTrip() && scheduleTime.getDepartureTime() != null && !isArrival) { long epochTime = Core.getInstance().getTime().getEpochTime(scheduleTime.getDepartureTime(), time); scheduledEpochTime = new Date(epochTime); } } this.scheduledTime = scheduledEpochTime; this.blockId = block.getId(); this.tripId = trip.getId(); this.directionId = trip.getDirectionId(); this.stopId = stopId; this.gtfsStopSeq = stopPath.getGtfsStopSeq(); this.stopPathLength = (float) stopPath.getLength(); this.routeId = trip.getRouteId(); this.routeShortName = trip.getRouteShortName(); this.serviceId = block.getServiceId(); } /** * Hibernate requires a no-arg constructor for reading objects * from database. */ protected ArrivalDeparture() { this.vehicleId = null; this.time = null; this.avlTime = null; this.block = null; this.directionId = null; this.tripIndex = -1; this.stopPathIndex = -1; this.isArrival = false; this.configRev = -1; this.scheduledTime = null; this.blockId = null; this.tripId = null; this.stopId = null; this.gtfsStopSeq = -1; this.stopPathLength = Float.NaN; this.routeId = null; this.routeShortName = null; this.serviceId = null; } /** * Callback due to implementing Lifecycle interface. Used to compact * string members by interning them. */ @Override public void onLoad(Session s, Serializable id) throws CallbackException { if (vehicleId != null) vehicleId = vehicleId.intern(); if (stopId != null) stopId = stopId.intern(); if (tripId != null) tripId = tripId.intern(); if (blockId != null) blockId = blockId.intern(); if (routeId != null) routeId = routeId.intern(); if (routeShortName != null) routeShortName = routeShortName.intern(); if (serviceId != null) serviceId = serviceId.intern(); if (directionId != null) directionId = directionId.intern(); } /** * Implemented due to Lifecycle interface being implemented. Not actually * used. */ @Override public boolean onSave(Session s) throws CallbackException { return Lifecycle.NO_VETO; } /** * Implemented due to Lifecycle interface being implemented. Not actually * used. */ @Override public boolean onUpdate(Session s) throws CallbackException { return Lifecycle.NO_VETO; } /** * Implemented due to Lifecycle interface being implemented. Not actually * used. */ @Override public boolean onDelete(Session s) throws CallbackException { return Lifecycle.NO_VETO; } /** * For logging each creation of an ArrivalDeparture to the separate * ArrivalsDepartures.log file. */ public void logCreation() { logger.info(this.toString()); } /** * Because using a composite Id Hibernate wants this member. */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((blockId == null) ? 0 : blockId.hashCode()); result = prime * result + configRev; result = prime * result + (isArrival ? 1231 : 1237); result = prime * result + stopPathIndex; result = prime * result + gtfsStopSeq; result = prime * result + ((routeId == null) ? 0 : routeId.hashCode()); result = prime * result + ((routeShortName == null) ? 0 : routeShortName.hashCode()); result = prime * result + ((serviceId == null) ? 0 : serviceId.hashCode()); result = prime * result + ((stopId == null) ? 0 : stopId.hashCode()); result = prime * result + ((time == null) ? 0 : time.hashCode()); result = prime * result + ((tripId == null) ? 0 : tripId.hashCode()); result = prime * result + tripIndex; result = prime * result + ((vehicleId == null) ? 0 : vehicleId.hashCode()); return result; } /** * Because using a composite Id Hibernate wants this member. */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ArrivalDeparture other = (ArrivalDeparture) obj; if (blockId == null) { if (other.blockId != null) return false; } else if (!blockId.equals(other.blockId)) return false; if (configRev != other.configRev) return false; if (isArrival != other.isArrival) return false; if (stopPathIndex != other.stopPathIndex) return false; if (gtfsStopSeq != other.gtfsStopSeq) return false; if (routeId == null) { if (other.routeId != null) return false; } else if (!routeId.equals(other.routeId)) return false; if (routeShortName == null) { if (other.routeShortName != null) return false; } else if (!routeShortName.equals(other.routeShortName)) return false; if (serviceId == null) { if (other.serviceId != null) return false; } else if (!serviceId.equals(other.serviceId)) return false; if (stopId == null) { if (other.stopId != null) return false; } else if (!stopId.equals(other.stopId)) return false; if (time == null) { if (other.time != null) return false; } else if (!time.equals(other.time)) return false; if (tripId == null) { if (other.tripId != null) return false; } else if (!tripId.equals(other.tripId)) return false; if (tripIndex != other.tripIndex) return false; if (vehicleId == null) { if (other.vehicleId != null) return false; } else if (!vehicleId.equals(other.vehicleId)) return false; return true; } @Override public String toString() { return (isArrival ? "Arrival " : "Departure") + " [" + "vehicleId=" + vehicleId // + ", isArrival=" + isArrival + ", time=" + Time.dateTimeStrMsec(time) + ", route=" + routeId + ", rteName=" + routeShortName + ", directionId=" + directionId + ", stop=" + stopId + ", gtfsStopSeq=" + gtfsStopSeq + ", stopIdx=" + stopPathIndex + ", avlTime=" + Time.timeStrMsec(avlTime) + ", trip=" + tripId + ", tripIdx=" + tripIndex + ", block=" + blockId + ", srv=" + serviceId + ", cfg=" + configRev + ", pathLnth=" + Geo.distanceFormat(stopPathLength) + (scheduledTime != null ? ", schedTime=" + Time.timeStr(scheduledTime) : "") + (scheduledTime != null ? ", schedAdh=" + new TemporalDifference(scheduledTime.getTime() - time.getTime()) : "") + "]"; } /** * For querying large amount of data. With a Hibernate Iterator not * all the data is read in at once. This means that can iterate over * a large dataset without running out of memory. But this can be slow * because when using iterate() an initial query is done to get all of * Id column data and then a separate query is done when iterating * over each row. Doing an individual query per row is of course * quite time consuming. Better to use getArrivalsDeparturesFromDb() * with a fairly large batch size of ~50000. * <p> * Note that the session needs to be closed externally once done with * the Iterator. * * @param session * @param beginTime * @param endTime * @return * @throws HibernateException */ public static Iterator<ArrivalDeparture> getArrivalsDeparturesDbIterator(Session session, Date beginTime, Date endTime) throws HibernateException { // Create the query. Table name is case sensitive and needs to be the // class name instead of the name of the db table. String hql = "FROM ArrivalDeparture " + " WHERE time >= :beginDate " + " AND time < :endDate"; Query query = session.createQuery(hql); // Set the parameters query.setTimestamp("beginDate", beginTime); query.setTimestamp("endDate", endTime); @SuppressWarnings("unchecked") Iterator<ArrivalDeparture> iterator = query.iterate(); return iterator; } /** * Read in arrivals and departures for a vehicle, over a time range. * * @param projectId * @param beginTime * @param endTime * @param vehicleId * @return */ public static List<ArrivalDeparture> getArrivalsDeparturesFromDb(Date beginTime, Date endTime, String vehicleId) { // Call in standard getArrivalsDeparturesFromDb() but pass in // sql clause return getArrivalsDeparturesFromDb(null, // Use db specified by transitime.db.dbName beginTime, endTime, "AND vehicleId='" + vehicleId + "'", 0, 0, // Don't use batching null); // Read both arrivals and departures } /** * Reads the arrivals/departures for the timespan specified. All of the * data is read in at once so could present memory issue if reading * in a very large amount of data. For that case probably best to instead * use getArrivalsDeparturesDb() where one specifies the firstResult and * maxResult parameters. * * @param projectId * @param beginTime * @param endTime * @return */ public static List<ArrivalDeparture> getArrivalsDeparturesFromDb(String projectId, Date beginTime, Date endTime) { IntervalTimer timer = new IntervalTimer(); // Get the database session. This is supposed to be pretty light weight Session session = HibernateUtils.getSession(projectId); // Create the query. Table name is case sensitive and needs to be the // class name instead of the name of the db table. String hql = "FROM ArrivalDeparture " + " WHERE time >= :beginDate " + " AND time < :endDate"; Query query = session.createQuery(hql); // Set the parameters query.setTimestamp("beginDate", beginTime); query.setTimestamp("endDate", endTime); try { @SuppressWarnings("unchecked") List<ArrivalDeparture> arrivalsDeparatures = query.list(); logger.debug("Getting arrival/departures from database took {} msec", timer.elapsedMsec()); return arrivalsDeparatures; } catch (HibernateException e) { // Log error to the Core logger Core.getLogger().error(e.getMessage(), e); return null; } finally { // Clean things up. Not sure if this absolutely needed nor if // it might actually be detrimental and slow things down. session.close(); } } /** * Allows batch retrieval of data. This is likely the best way to read in * large amounts of data. Using getArrivalsDeparturesDbIterator() reads in * only data as needed so good with respect to memory usage but it does a * separate query for each row. Reading in list of all data is quick but can * cause memory problems if reading in a very large amount of data. This * method is a good compromise because it only reads in a batch of data at a * time so is not as memory intensive yet it is quite fast. With a batch * size of 50k found it to run in under 1/4 the time as with the iterator * method. * * @param dbName * Name of the database to retrieve data from. If set to null * then will use db name configured by Java property * transitime.db.dbName * @param beginTime * @param endTime * @param sqlClause * The clause is added to the SQL for retrieving the * arrival/departures. Useful for ordering the results. Can be * null. * @param firstResult * For when reading in batch of data at a time. * @param maxResults * For when reading in batch of data at a time. If set to 0 then * will read in all data at once. * @param arrivalOrDeparture * Enumeration specifying whether to read in just arrivals or * just departures. Set to null to read in both. * @return */ public static List<ArrivalDeparture> getArrivalsDeparturesFromDb(String dbName, Date beginTime, Date endTime, String sqlClause, final int firstResult, final int maxResults, ArrivalsOrDepartures arrivalOrDeparture) { IntervalTimer timer = new IntervalTimer(); // Get the database session. This is supposed to be pretty light weight Session session = dbName != null ? HibernateUtils.getSession(dbName) : HibernateUtils.getSession(); // Create the query. Table name is case sensitive and needs to be the // class name instead of the name of the db table. String hql = "FROM ArrivalDeparture " + " WHERE time >= :beginDate " + " AND time < :endDate"; if (arrivalOrDeparture != null) { if (arrivalOrDeparture == ArrivalsOrDepartures.ARRIVALS) hql += " AND isArrival = true"; else hql += " AND isArrival = false"; } if (sqlClause != null) hql += " " + sqlClause; Query query = session.createQuery(hql); // Set the parameters for the query query.setTimestamp("beginDate", beginTime); query.setTimestamp("endDate", endTime); // Only get a batch of data at a time if maxResults specified query.setFirstResult(firstResult); if (maxResults > 0) query.setMaxResults(maxResults); try { @SuppressWarnings("unchecked") List<ArrivalDeparture> arrivalsDeparatures = query.list(); logger.debug("Getting arrival/departures from database took {} msec", timer.elapsedMsec()); return arrivalsDeparatures; } catch (HibernateException e) { // Log error to the Core logger Core.getLogger().error(e.getMessage(), e); return null; } finally { // Clean things up. Not sure if this absolutely needed nor if // it might actually be detrimental and slow things down. session.close(); } } /** * Same as other getArrivalsDeparturesFromDb() but uses * -Dtransitime.db.dbName Java property to specify the name of the database. * * @param beginTime * @param endTime * @param sqlClause * @param firstResult * @param maxResults * @param arrivalOrDeparture * @return */ public static List<ArrivalDeparture> getArrivalsDeparturesFromDb(Date beginTime, Date endTime, String sqlClause, final int firstResult, final int maxResults, ArrivalsOrDepartures arrivalOrDeparture) { return getArrivalsDeparturesFromDb(DbSetupConfig.getDbName(), beginTime, endTime, sqlClause, firstResult, maxResults, arrivalOrDeparture); } public String getVehicleId() { return vehicleId; } public Date getDate() { return time; } public Date getAvlTime() { return avlTime; } public long getTime() { return time.getTime(); } public String getStopId() { return stopId; } public boolean isArrival() { return isArrival; } /** * Can be more clear than using !isArrival() * @return */ public boolean isDeparture() { return !isArrival; } public String getTripId() { return tripId; } public String getBlockId() { return blockId; } public String getRouteId() { return routeId; } public String getServiceId() { return serviceId; } public String getDirectionId() { return directionId; } public int getConfigRev() { return configRev; } public int getTripIndex() { return tripIndex; } public int getStopPathIndex() { return stopPathIndex; } public float getStopPathLength() { return stopPathLength; } /** * Note that the block is a transient element so will not be available if * this object was read from the database. In that case it will be null. * * @return */ public Block getBlock() { return block; } /** * The schedule time will only be set if the schedule info was available * from the GTFS data and it is the proper type of arrival or departure * stop (there is an arrival schedule time and this is the last stop for * a trip and and this is an arrival time OR there is a departure schedule * time and this is not the last stop for a trip and this is a departure * time. Otherwise will be null. * * @return */ public Date getScheduledDate() { return scheduledTime; } /** * Same as getScheduledDate() but returns long epoch time. * @return */ public long getScheduledTime() { return scheduledTime.getTime(); } /** * Returns the schedule adherence for the stop if there was a schedule * time. Otherwise returns null. * * @return */ public TemporalDifference getScheduleAdherence() { // If there is no schedule time for this stop then there // is no schedule adherence information. if (scheduledTime == null) return null; // Return the schedule adherence return new TemporalDifference(scheduledTime.getTime() - time.getTime()); } /** * Returns the Stop object associated with the arrival/departure. Will only * be valid for the Core system where the configuration has been read in. * * @return The Stop associated with the arrival/departure */ public Stop getStop() { return Core.getInstance().getDbConfig().getStop(stopId); } }