diff --git a/app/src/main/java/org/transitclock/api/reports/PredictionAccuracyQuery.java b/app/src/main/java/org/transitclock/api/reports/PredictionAccuracyQuery.java index 285c3a8f..19dc7c5f 100644 --- a/app/src/main/java/org/transitclock/api/reports/PredictionAccuracyQuery.java +++ b/app/src/main/java/org/transitclock/api/reports/PredictionAccuracyQuery.java @@ -1,20 +1,28 @@ /* (C)2023 */ package org.transitclock.api.reports; -import lombok.extern.slf4j.Slf4j; -import org.transitclock.domain.GenericQuery; -import org.transitclock.utils.Time; - import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Time; import java.sql.Timestamp; +import java.text.DateFormat; import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.transitclock.domain.hibernate.HibernateUtils; + +import com.google.common.base.Strings; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; + +import static org.transitclock.utils.Time.parseDate; + /** * For doing SQL query and generating JSON data for a prediction accuracy chart. This abstract class * does the SQL query and puts data into a map. Then a subclass must be used to convert the data to @@ -25,10 +33,9 @@ * @author SkiBu Smith */ @Slf4j -public abstract class PredictionAccuracyQuery extends GenericQuery { +public abstract class PredictionAccuracyQuery { protected static final int MAX_PRED_LENGTH = 900; protected static final int PREDICTION_LENGTH_BUCKET_SIZE = 30; - // Keyed on source (so can show data for multiple sources at // once in order to compare prediction accuracy. Contains a array, // with an element for each prediction bucket, containing an array @@ -37,51 +44,10 @@ public abstract class PredictionAccuracyQuery extends GenericQuery { // a certain prediction range, specified by predictionLengthBucketSize. protected final Map>> map = new HashMap<>(); - // Defines the output type for the intervals, whether should show - // standard deviation, percentage, or both. - // Can iterate over the enumerated type using: - // for (IntervalsType type : IntervalsType.values()) {} - public enum IntervalsType { - PERCENTAGE("PERCENTAGE"), - STD_DEV("STD_DEV"), - BOTH("BOTH"); - - private final String text; - - IntervalsType(final String text) { - this.text = text; - } - - /** - * For converting from a string to an IntervalsType - * - * @param text String to be converted - * @return The corresponding IntervalsType, or IntervalsType.PERCENTAGE as the default if - * text doesn't match a type. - */ - public static IntervalsType createIntervalsType(String text) { - for (IntervalsType type : IntervalsType.values()) { - if (type.toString().equals(text)) { - return type; - } - } - - // If a bad non-null value was specified then log the error - if (text != null) logger.error("\"{}\" is not a valid IntervalsType", text); - - // Couldn't match so use default value - return IntervalsType.PERCENTAGE; - } - - @Override - public String toString() { - return text; - } - } - + private final String agencyId; public PredictionAccuracyQuery(String agencyId) throws SQLException { - super(agencyId); + this.agencyId = agencyId; } /** @@ -91,6 +57,7 @@ public PredictionAccuracyQuery(String agencyId) throws SQLException { * is in the middle of the range. * * @param predLength + * * @return */ private static int index(int predLength) { @@ -111,7 +78,9 @@ private void addDataToMap(int predLength, int predAccuracy, String source) { // Determine the index of the appropriate prediction bucket int predictionBucketIndex = index(predLength); - while (predictionBuckets.size() < predictionBucketIndex + 1) predictionBuckets.add(new ArrayList<>()); + while (predictionBuckets.size() < predictionBucketIndex + 1) { + predictionBuckets.add(new ArrayList<>()); + } if (predictionBucketIndex < predictionBuckets.size() && predictionBucketIndex >= 0) { List predictionAccuracies = predictionBuckets.get(predictionBucketIndex); // Add the prediction accuracy to the bucket. @@ -131,23 +100,25 @@ private void addDataToMap(int predLength, int predAccuracy, String source) { * Performs the SQL query and puts the resulting data into the map. * * @param beginDateStr Begin date for date range of data to use. - * @param numDaysStr How many days to do the query for + * @param numDaysStr How many days to do the query for * @param beginTimeStr For specifying time of day between the begin and end date to use data - * for. Can thereby specify a date range of a week but then just look at data for particular - * time of day, such as 7am to 9am, for those days. Set to null or empty string to use data - * for entire day. - * @param endTimeStr For specifying time of day between the begin and end date to use data for. - * Can thereby specify a date range of a week but then just look at data for particular time - * of day, such as 7am to 9am, for those days. Set to null or empty string to use data for - * entire day. - * @param routeIds Array of IDs of routes to get data for - * @param predSource The source of the predictions. Can be null or "" (for all), "Transitime", - * or "Other" - * @param predType Whether predictions are affected by wait stop. Can be "" (for all), - * "AffectedByWaitStop", or "NotAffectedByWaitStop". + * for. Can thereby specify a date range of a week but then just look at data for particular + * time of day, such as 7am to 9am, for those days. Set to null or empty string to use data + * for entire day. + * @param endTimeStr For specifying time of day between the begin and end date to use data for. + * Can thereby specify a date range of a week but then just look at data for particular time + * of day, such as 7am to 9am, for those days. Set to null or empty string to use data for + * entire day. + * @param routeIds Array of IDs of routes to get data for + * @param predSource The source of the predictions. Can be null or "" (for all), "Transitime", + * or "Other" + * @param predType Whether predictions are affected by wait stop. Can be "" (for all), + * "AffectedByWaitStop", or "NotAffectedByWaitStop". + * * @throws SQLException * @throws ParseException */ + protected void doQuery( String beginDateStr, String numDaysStr, @@ -164,27 +135,49 @@ protected void doQuery( throw new ParseException( "Begin date to end date spans more than a month for endDate=" + " startDate=" - + Time.parseDate(beginDateStr) + + parseDate(beginDateStr) + " Number of days of " + numDays + " spans more than a month", 0); } - String timeSql = ""; - String mySqlTimeSql = ""; - if ((beginTimeStr != null && !beginTimeStr.isEmpty()) || (endTimeStr != null && !endTimeStr.isEmpty())) { - // If only begin or only end time set then use default value - if (beginTimeStr == null || beginTimeStr.isEmpty()) beginTimeStr = "00:00:00"; - else { - // beginTimeStr set so make sure it is valid, and prevent - // possible SQL injection - if (!beginTimeStr.matches("\\d+:\\d+")) - throw new ParseException("begin time \"" + beginTimeStr + "\" is not valid.", 0); + Date beginDate; + try { + DateFormat defaultDateFormat = new SimpleDateFormat("MM-dd-yyyy"); + DateFormat altDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + beginDate = (beginDateStr.charAt(4) != '-') ? defaultDateFormat.parse(beginDateStr) : altDateFormat.parse(beginDateStr); + } catch (ParseException ex) { + logger.warn("Invalid date format. Use MM-dd-yyyy or yyyy-MM-dd. " + ex.getMessage()); + throw ex; + } + + // Parse beginTime and endTime + Time beginTime; + Time endTime; + SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss"); + SimpleDateFormat shortTimeFormat = new SimpleDateFormat("HH:mm"); + + if (Strings.isNullOrEmpty(beginTimeStr) || Strings.isNullOrEmpty(endTimeStr)) { + if (Strings.isNullOrEmpty(beginTimeStr)) { + beginTimeStr = "00:00:00"; } - if (endTimeStr == null || endTimeStr.isEmpty()) endTimeStr = "23:59:59"; - // time param is jdbc param -- no need to check for injection attacks - timeSql = " AND arrival_departure_time::time BETWEEN ? AND ? "; + if (Strings.isNullOrEmpty(endTimeStr)) { + endTimeStr = "23:59:59"; + } + } + + try { + beginTime = (beginTimeStr.length() < 6) + ? new Time(shortTimeFormat.parse(beginTimeStr).getTime()) + : new Time(timeFormat.parse(beginTimeStr).getTime()); + endTime = (endTimeStr.length() < 6) + ? new Time(shortTimeFormat.parse(endTimeStr).getTime()) + : new Time(timeFormat.parse(endTimeStr).getTime()); + } catch (ParseException e) { + logger.warn("Invalid time format: " + e.getMessage()); + throw e; } + String timeSql = " AND arrival_departure_time::time BETWEEN ? AND ? "; // Determine route portion of SQL // Need to examine each route ID twice since doing a @@ -200,78 +193,47 @@ protected void doQuery( routeSql += ")"; } - // Determine the source portion of the SQL. Default is to provide - // predictions for all sources - String sourceSql = ""; - if (predSource != null && !predSource.isEmpty()) { - if (predSource.equals("Transitime")) { - // Only "Transitime" predictions - sourceSql = " AND prediction_source='Transitime'"; + // TODO generate database independent SQL if possible! + + // Put the entire SQL query together + StringBuilder sql = new StringBuilder("SELECT to_char(predicted_time-prediction_read_time, 'SSSS')::integer as predLength, ") + .append("prediction_accuracy_msecs/1000 as predAccuracy, ") + .append(" prediction_source as source FROM prediction_accuracy WHERE") + .append(" arrival_departure_time BETWEEN ? AND TIMESTAMP '") + .append(beginDate).append("' + INTERVAL '") + .append(numDays).append(" day' ").append(timeSql) + .append(" AND predicted_time - prediction_read_time < '00:15:00' ") + .append(routeSql); + + + // Add prediction type condition if provided + if (!Strings.isNullOrEmpty(predType)) { + if (predType.equals("AffectedByWaitStop")) { + sql.append("AND affected_by_wait_stop = true "); } else { - // Anything but "Transitime" - sourceSql = " AND prediction_source<>'Transitime'"; + sql.append("AND affected_by_wait_stop = false "); } } - // Determine SQL for prediction type. Can be "" (for - // all), "AffectedByWaitStop", or "NotAffectedByWaitStop". - String predTypeSql = ""; - if (predType != null && !predType.isEmpty()) { - if (predSource.equals("AffectedByWaitStop")) { - // Only "AffectedByLayover" predictions - predTypeSql = " AND affected_by_wait_stop = true "; + // Add prediction source condition if provided + if (!Strings.isNullOrEmpty(predSource)) { + if (predSource.equals("Transitime")) { + sql.append("AND prediction_source = 'Transitime' "); } else { - // Only "NotAffectedByLayover" predictions - predTypeSql = " AND affected_by_wait_stop = false "; + sql.append("AND prediction_source <> 'Transitime' "); } } - // TODO generate database independent SQL if possible! - // Put the entire SQL query together - String sql = "SELECT to_char(predicted_time-prediction_read_time, 'SSSS')::integer as predLength, " - + "prediction_accuracy_msecs/1000 as predAccuracy, " - + " prediction_source as source FROM prediction_accuracy WHERE" - + " arrival_departure_time BETWEEN ? AND TIMESTAMP '" + beginDateStr - + "' + INTERVAL '" - + numDays - + " day' " - + timeSql - + " AND predicted_time - prediction_read_time < '00:15:00' " - + routeSql - + sourceSql - + predTypeSql; - - PreparedStatement statement = null; - try { + try (var connection = HibernateUtils + .getSessionFactory(this.agencyId) + .getSessionFactoryOptions() + .getServiceRegistry() + .getService(ConnectionProvider.class) + .getConnection()) { + connection.setReadOnly(true); logger.debug("SQL: {}", sql); - statement = getConnection().prepareStatement(sql); - - // Determine the date parameters for the query - Timestamp beginDate = null; - java.util.Date date = Time.parse(beginDateStr); - beginDate = new Timestamp(date.getTime()); - - // Determine the time parameters for the query - // If begin time not set but end time is then use midnight as begin - // time - if ((beginTimeStr == null || beginTimeStr.isEmpty()) && endTimeStr != null && !endTimeStr.isEmpty()) { - beginTimeStr = "00:00:00"; - } - // If end time not set but begin time is then use midnight as end - // time - if ((endTimeStr == null || endTimeStr.isEmpty()) && beginTimeStr != null && !beginTimeStr.isEmpty()) { - endTimeStr = "23:59:59"; - } - - java.sql.Time beginTime = null; - java.sql.Time endTime = null; - if (beginTimeStr != null && !beginTimeStr.isEmpty()) { - beginTime = new java.sql.Time(Time.parseTimeOfDay(beginTimeStr) * Time.MS_PER_SEC); - } - if (endTimeStr != null && !endTimeStr.isEmpty()) { - endTime = new java.sql.Time(Time.parseTimeOfDay(endTimeStr) * Time.MS_PER_SEC); - } + statement = connection.prepareStatement(sql.toString()); logger.debug( "beginDate {} beginDateStr {} endDateStr {} beginTime {} beginTimeStr {}" @@ -286,16 +248,12 @@ protected void doQuery( // Set the parameters for the query int i = 1; - statement.setTimestamp(i++, beginDate); + statement.setTimestamp(i++, new Timestamp(beginDate.getTime())); + statement.setTime(i++, beginTime); + statement.setTime(i++, endTime); - if (beginTime != null) { - statement.setTime(i++, beginTime); - } - if (endTime != null) { - statement.setTime(i++, endTime); - } if (routeIds != null) { - for (String routeId : routeIds) + for (String routeId : routeIds) { if (!routeId.trim().isEmpty()) { // Need to add the route ID twice since doing a // routeId='stableId' OR routeShortName='stableId' in @@ -304,6 +262,7 @@ protected void doQuery( statement.setString(i++, routeId); statement.setString(i++, routeId); } + } } // Actually execute the query @@ -320,14 +279,57 @@ protected void doQuery( } rs.close(); - } catch (SQLException e) { - throw e; + } catch (SQLException ex) { + throw ex; } finally { - if (statement != null) + if (statement != null) { statement.close(); - if (!getConnection().isClosed()) { - getConnection().close(); } } } + + // Defines the output type for the intervals, whether should show + // standard deviation, percentage, or both. + // Can iterate over the enumerated type using: + // for (IntervalsType type : IntervalsType.values()) {} + public enum IntervalsType { + PERCENTAGE("PERCENTAGE"), + STD_DEV("STD_DEV"), + BOTH("BOTH"); + + private final String text; + + IntervalsType(final String text) { + this.text = text; + } + + /** + * For converting from a string to an IntervalsType + * + * @param text String to be converted + * + * @return The corresponding IntervalsType, or IntervalsType.PERCENTAGE as the default if + * text doesn't match a type. + */ + public static IntervalsType createIntervalsType(String text) { + for (IntervalsType type : IntervalsType.values()) { + if (type.toString().equals(text)) { + return type; + } + } + + // If a bad non-null value was specified then log the error + if (text != null) { + logger.error("\"{}\" is not a valid IntervalsType", text); + } + + // Couldn't match so use default value + return IntervalsType.PERCENTAGE; + } + + @Override + public String toString() { + return text; + } + } } diff --git a/app/src/main/java/org/transitclock/api/reports/ScheduleAdherenceController.java b/app/src/main/java/org/transitclock/api/reports/ScheduleAdherenceController.java index 1591ae95..3e8e5fd0 100644 --- a/app/src/main/java/org/transitclock/api/reports/ScheduleAdherenceController.java +++ b/app/src/main/java/org/transitclock/api/reports/ScheduleAdherenceController.java @@ -1,19 +1,27 @@ /* (C)2023 */ package org.transitclock.api.reports; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - +import jakarta.persistence.criteria.*; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.Session; +import org.hibernate.query.Query; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.transitclock.domain.hibernate.HibernateUtils; +import org.transitclock.domain.structs.ArrivalDeparture; +import org.transitclock.utils.Time; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; -@Component @Slf4j +@Component +@RequiredArgsConstructor public class ScheduleAdherenceController { // TODO: Combine routeScheduleAdherence and stopScheduleAdherence // - Make this a REST endpoint @@ -29,11 +37,10 @@ public class ScheduleAdherenceController { @Value("${transitclock.web.userPredictionLimits:false}") private Boolean usePredictionLimits; -// private static final String ADHERENCE_SQL = "(time - scheduledTime) AS scheduleAdherence"; -// private static final Projection ADHERENCE_PROJECTION = Projections.sqlProjection( -// ADHERENCE_SQL, new String[] {"scheduleAdherence"}, new Type[] {DoubleType.INSTANCE}); -// private static final Projection AVG_ADHERENCE_PROJECTION = Projections.sqlProjection( -// "avg" + ADHERENCE_SQL, new String[] {"scheduleAdherence"}, new Type[] {DoubleType.INSTANCE}); + // TODO: Combine routeScheduleAdherence and stopScheduleAdherence + // - Make this a REST endpoint + // problem - negative schedule adherence means we're late + public List stopScheduleAdherence( Date startDate, @@ -59,128 +66,155 @@ public List routeScheduleAdherence( return groupScheduleAdherence(startDate, numDays, startTime, endTime, "routeId", routeIds, byRoute, datatype); } - public List routeScheduleAdherenceSummary( - Date startDate, - int numDays, - String startTime, - String endTime, - Double earlyLimitParam, - Double lateLimitParam, - List routeIds) { + public Map routeScheduleAdherenceSummary(Date startDate, + int numDays, + String startTime, + String endTime, + Double earlyLimitParam, + Double lateLimitParam, + List routeIds) { int count = 0; int early = 0; int late = 0; int ontime = 0; + Double earlyLimit = (usePredictionLimits ? earlyLimitParam : (double) scheduleEarlySeconds); Double lateLimit = (usePredictionLimits ? lateLimitParam : (double) scheduleLateSeconds); + List results = routeScheduleAdherence(startDate, numDays, startTime, endTime, routeIds, false, null); + Map result = new HashMap<>(); for (Object o : results) { count++; - HashMap hm = (HashMap) o; - Double d = (Double) hm.get("scheduleAdherence"); - if (d > lateLimit) { + + var hm = (HashMap) o; + Duration d = (Duration) hm.get("scheduleAdherence"); + double totalSeconds = d.toMillis() / 1000.0; + + if (totalSeconds > lateLimit) { late++; - } else if (d < earlyLimit) { + } else if (totalSeconds < earlyLimit) { early++; } else { ontime++; } } - logger.info( - "query complete -- earlyLimit={}, lateLimit={}, early={}, ontime={}, late={}," + " count={}", + logger.info("query complete -- earlyLimit={}, lateLimit={}, early={}, onTime={}, late={}," + " count={}", earlyLimit, lateLimit, early, ontime, late, count); + double earlyPercent = (1.0 - (double) (count - early) / count) * 100; double onTimePercent = (1.0 - (double) (count - ontime) / count) * 100; double latePercent = (1.0 - (double) (count - late) / count) * 100; - logger.info( - "count={} earlyPercent={} onTimePercent={} latePercent={}", + logger.info("count=static{} earlyPercent={} onTimePercent={} latePercent={}", count, earlyPercent, onTimePercent, latePercent); - Integer[] summary = new Integer[] {count, (int) earlyPercent, (int) onTimePercent, (int) latePercent}; - return Arrays.asList(summary); + DecimalFormat df = new DecimalFormat("#.##"); + df.setRoundingMode(RoundingMode.DOWN); + + result.put("count", String.valueOf(count)); + result.put("early", df.format(earlyPercent)); + result.put("late", df.format(latePercent)); + result.put("onTime", df.format(onTimePercent)); + return result; } - private List groupScheduleAdherence( - Date startDate, - int numDays, - String startTime, - String endTime, - String groupName, - List idsOrEmpty, - boolean byGroup, - String datatype) { -/* + private List groupScheduleAdherence(Date startDate, + int numDays, + String startTime, + String endTime, + String groupName, + List idsOrEmpty, + boolean byGroup, + String datatype) { - var qentity = QArrivalDeparture.arrivalDeparture; - Session session = HibernateUtils.getSession(); - JPAQuery query = new JPAQuery<>(session); - - // filter ids which may be empty. List ids = new ArrayList<>(); - if (idsOrEmpty != null) - for (String id : idsOrEmpty) + if (idsOrEmpty != null) { + for (String id : idsOrEmpty) { if (!StringUtils.isBlank(id)) { ids.add(id); } + } + } + // Calculate end date based on start date and numDays Date endDate = new Date(startDate.getTime() + (numDays * Time.MS_PER_DAY)); - ProjectionList proj = Projections.projectionList(); - - if (byGroup) - proj.add(Projections.groupProperty(groupName), groupName) - .add(Projections.rowCount(), "count"); - else - proj.add(Projections.property("routeId"), "routeId") - .add(Projections.property("stopId"), "stopId") - .add(Projections.property("tripId"), "tripId"); - - proj.add(byGroup ? AVG_ADHERENCE_PROJECTION : ADHERENCE_PROJECTION, "scheduleAdherence"); - - DetachedCriteria criteria = DetachedCriteria.forClass(ArrivalDeparture.class) - .add(Restrictions.between("time", startDate, endDate)) - .add(Restrictions.isNotNull("scheduledTime")); + try (Session session = HibernateUtils.getSession()) { + // Create Query + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(Object[].class); + Root root = query.from(ArrivalDeparture.class); + + // Building predicates + List predicates = new ArrayList<>(); + predicates.add(cb.between(root.get("time"), startDate, endDate)); + predicates.add(cb.isNotNull(root.get("scheduledTime"))); + + // Check if we're dealing with 'arrival' or 'departure' + if ("arrival".equals(datatype)) { + predicates.add(cb.isTrue(root.get("isArrival"))); + } else if ("departure".equals(datatype)) { + predicates.add(cb.isFalse(root.get("isArrival"))); + } - if ("arrival".equals(datatype)) criteria.add(Restrictions.eq("isArrival", true)); - else if ("departure".equals(datatype)) criteria.add(Restrictions.eq("isArrival", false)); + Expression timePartExpr = cb.function("TO_CHAR", String.class, root.get("time"), cb.literal("HH24:MI:SS")); + predicates.add(cb.greaterThanOrEqualTo(timePartExpr, startTime)); + predicates.add(cb.lessThanOrEqualTo(timePartExpr, endTime)); - String sql = "time({alias}.time) between ? and ?"; - String[] values = {startTime, endTime}; - Type[] types = {StringType.INSTANCE, StringType.INSTANCE}; - criteria.add(Restrictions.sqlRestriction(sql, values, types)); + if (!ids.isEmpty()) { + predicates.add(root.get(groupName).in(ids)); + } - criteria.setProjection(proj).setResultTransformer(DetachedCriteria.ALIAS_TO_ENTITY_MAP); + query.where(predicates.toArray(new Predicate[0])); - if (ids != null && ids.size() > 0) criteria.add(Restrictions.in(groupName, ids)); -*/ - return Collections.emptyList(); - } + // Grouping logic based on byGroup flag + if (byGroup) { + query.multiselect( + root.get(groupName), + cb.count(root), + cb.avg(cb.diff(root.get("time"), root.get("scheduledTime"))) + ).groupBy(root.get(groupName)); + } else { + query.multiselect( + root.get("routeId"), + root.get("stopId"), + root.get("tripId"), + cb.diff(root.get("time"), root.get("scheduledTime")) + ); + } - private Date endOfDay(Date endDate) { - Calendar c = Calendar.getInstance(); - c.setTime(endDate); - c.set(Calendar.HOUR, 23); - c.set(Calendar.MINUTE, 59); - c.set(Calendar.SECOND, 59); - return c.getTime(); + Query hibernateQuery = session.createQuery(query); + List results = hibernateQuery.getResultList(); + // Get result + return results.stream() + .map(result -> { + HashMap map = new HashMap<>(); + if (byGroup) { + map.put(groupName, result[0]); + map.put("count", result[1]); + map.put("scheduleAdherence", result[2]); + } else { + map.put("routeId", result[0]); + map.put("stopId", result[1]); + map.put("tripId", result[2]); + map.put("scheduleAdherence", result[3]); + } + return map; + }) + .collect(Collectors.toList()); + + } catch (Exception esqEx) { + esqEx.printStackTrace(); + } + return List.of(); } -// -// private static List dbify(DetachedCriteria criteria) { -// Session session = HibernateUtils.getSession(); -// try { -// return criteria.getExecutableCriteria(session).list(); -// } finally { -// session.close(); -// } -// } } diff --git a/app/src/main/java/org/transitclock/api/resources/CommandsApi.java b/app/src/main/java/org/transitclock/api/resources/CommandsApi.java index 0733909c..2421f4dc 100644 --- a/app/src/main/java/org/transitclock/api/resources/CommandsApi.java +++ b/app/src/main/java/org/transitclock/api/resources/CommandsApi.java @@ -171,7 +171,7 @@ ResponseEntity pushAvlData( @Operation( summary = "Cancel a trip in order to be shown in GTFS realtime.", description = "Experimental. It will work olny with the correct" - + " version. It cancel a trip that has no vechilce assigned.", + + " version. It cancel a trip that has no vehicle assigned.", tags = {"command", "trip"}) ResponseEntity cancelTrip( StandardParameters stdParameters, @@ -190,7 +190,7 @@ ResponseEntity cancelTrip( tags = {"command", "trip"}) ResponseEntity reenableTrip( StandardParameters stdParameters, - @Parameter(description = "tripId to remove calceled satate.", required = true) @PathVariable("tripId") + @Parameter(description = "tripId to remove canceled satate.", required = true) @PathVariable("tripId") String tripId, @Parameter(description = "start trip time", required = false) @RequestParam(value = "at", required = false) DateTimeParam at); diff --git a/app/src/main/java/org/transitclock/api/resources/ReportsApi.java b/app/src/main/java/org/transitclock/api/resources/ReportsApi.java index 63d24dd7..4978b8fc 100644 --- a/app/src/main/java/org/transitclock/api/resources/ReportsApi.java +++ b/app/src/main/java/org/transitclock/api/resources/ReportsApi.java @@ -4,6 +4,7 @@ import java.sql.SQLException; import java.text.ParseException; import java.util.List; +import java.util.Map; import org.transitclock.api.utils.StandardParameters; @@ -42,11 +43,12 @@ ResponseEntity getTripsWithTravelTimes( summary = "Returns avl report.", description = "Returns avl report.", tags = {"report", "vehicle"}) + @GetMapping(value = "/reports/avlReport", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) ResponseEntity getAvlReport( StandardParameters stdParameters, - @Parameter(description = "Vehicle id") @RequestParam(value = "v") String vehicleId, + @Parameter(description = "Vehicle id") @RequestParam(value = "v", required = false) String vehicleId, @Parameter(description = "Begin date(MM-DD-YYYY or YYYY-MM-DD") @RequestParam(value = "beginDate") String beginDate, @Parameter(description = "Num days.", required = false) @RequestParam(value = "numDays", defaultValue = "1", required = false) int numDays, @Parameter(description = "Begin time(HH:MM)", required = false) @RequestParam(value = "beginTime", required = false) String beginTime, @@ -121,7 +123,7 @@ ResponseEntity scheduleAdhReport( produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) ResponseEntity reportForStopById( StandardParameters stdParameters, - @Parameter(description = "Stop id") @RequestParam(value = "stopId") String stopId, + @Parameter(description = "Stop id") @RequestParam(value = "id") String stopId, @Parameter(description = "Begin date(MM-DD-YYYY or YYYY-MM-DD") @RequestParam(value = "beginDate") String beginDate, @Parameter(description = "Num days.") @RequestParam(value = "numDays", defaultValue = "1", required = false) int numDays, @Parameter(description = "Begin time(HH:MM)") @RequestParam(value = "beginTime", required = false) String beginTime, @@ -146,7 +148,7 @@ ResponseEntity reportForStopById( ResponseEntity predAccuracyRangeData(HttpServletRequest request) throws SQLException, ParseException; @GetMapping(value = "/reports/data/summaryScheduleAdherence.jsp") - ResponseEntity> summaryScheduleAdherence(HttpServletRequest request) throws ParseException; + ResponseEntity> summaryScheduleAdherence(HttpServletRequest request) throws ParseException; @GetMapping(value = "/reports/predAccuracyScatterData.jsp") ResponseEntity predAccuracyScatterData(HttpServletRequest request) throws ParseException, SQLException; diff --git a/app/src/main/java/org/transitclock/api/resources/ReportsResource.java b/app/src/main/java/org/transitclock/api/resources/ReportsResource.java index bb7f0afc..773616f6 100644 --- a/app/src/main/java/org/transitclock/api/resources/ReportsResource.java +++ b/app/src/main/java/org/transitclock/api/resources/ReportsResource.java @@ -1,12 +1,16 @@ package org.transitclock.api.resources; +import com.google.common.base.Strings; + import jakarta.servlet.http.HttpServletRequest; import java.sql.SQLException; +import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +33,7 @@ @RestController public class ReportsResource extends BaseApiResource implements ReportsApi { + @Autowired ScheduleAdherenceController scheduleAdherenceController; @@ -197,9 +202,8 @@ public ResponseEntity predAccuracyIntervalsData(HttpServletRequest reque } // Respond with the JSON string - ResponseEntity response = ResponseEntity.status(HttpStatus.OK) + return ResponseEntity.status(HttpStatus.OK) .body(jsonString); - return response; } @Override @@ -277,7 +281,7 @@ public ResponseEntity predAccuracyRangeData(HttpServletRequest request) } @Override - public ResponseEntity> summaryScheduleAdherence(HttpServletRequest request) throws ParseException { + public ResponseEntity> summaryScheduleAdherence(HttpServletRequest request) throws ParseException { String startDateStr = request.getParameter("beginDate"); String numDaysStr = request.getParameter("numDays"); String startTime = request.getParameter("beginTime"); @@ -287,43 +291,45 @@ public ResponseEntity> summaryScheduleAdherence(HttpServletRequest double earlyLimit = -60.0; double lateLimit = 60.0; - if (StringUtils.hasText(startTime)) { + if (!StringUtils.hasText(startTime)) { startTime = "00:00:00"; - } else { + } else if (startTime.length() < 6) { startTime += ":00"; } - if (StringUtils.hasText(endTime)) { + if (!StringUtils.hasText(endTime)) { endTime = "23:59:59"; - } else { + } else if (endTime.length() < 6) { endTime += ":00"; } - if (!StringUtils.hasText(earlyLimitStr)) { - earlyLimit = Double.parseDouble(earlyLimitStr) * 60; + if (StringUtils.hasText(earlyLimitStr)) { + earlyLimit = Double.parseDouble(earlyLimitStr) * -60; } - if (!StringUtils.hasText(lateLimitStr)) { + if (StringUtils.hasText(lateLimitStr)) { lateLimit = Double.parseDouble(lateLimitStr) * 60; } - String routeIdList = request.getParameter("r"); List routeIds = routeIdList == null ? null : Arrays.asList(routeIdList.split(",")); + Date beginDate; + try { + DateFormat defaultDateFormat = new SimpleDateFormat("MM-dd-yyyy"); + DateFormat altDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + beginDate = (startDateStr.charAt(4) != '-') ? defaultDateFormat.parse(startDateStr) : altDateFormat.parse(startDateStr); + } catch (ParseException e) { + throw e; + } - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - Date startDate = dateFormat.parse(startDateStr); - - List results = scheduleAdherenceController.routeScheduleAdherenceSummary(startDate, - Integer.parseInt(numDaysStr), - startTime, endTime, - earlyLimit, lateLimit, - routeIds); - + Map results = scheduleAdherenceController.routeScheduleAdherenceSummary(beginDate, + Integer.parseInt(numDaysStr), + startTime, endTime, + earlyLimit, lateLimit, + routeIds); return ResponseEntity.ok(results); } - @Override public ResponseEntity predAccuracyScatterData(HttpServletRequest request) throws ParseException, SQLException { String agencyId = request.getParameter("a"); @@ -351,20 +357,6 @@ public ResponseEntity predAccuracyScatterData(HttpServletRequest request throw new ParseException("Number of days of " + numDays + " spans more than a month", 0); } - // Determine the time portion of the SQL - String timeSql = ""; - if ((beginTime != null && !beginTime.isEmpty()) || (endTime != null && !endTime.isEmpty())) { - // If only begin or only end time set then use default value - if (beginTime == null || beginTime.isEmpty()) { - beginTime = "00:00:00"; - } - if (endTime == null || endTime.isEmpty()) { - endTime = "23:59:59"; - } - - timeSql = SqlUtils.timeRangeClause(request, "arrival_depature_time", Integer.parseInt(numDays)); - } - // Determine route portion of SQL. Default is to provide info for all routes. String routeSql = ""; if (routeId != null && !routeId.trim().isEmpty()) { @@ -373,19 +365,19 @@ public ResponseEntity predAccuracyScatterData(HttpServletRequest request // Determine the source portion of the SQL. Default is to provide predictions for all sources String sourceSql = ""; - if (source != null && !source.isEmpty()) { + if (!Strings.isNullOrEmpty(source)) { if (source.equals("Transitime")) { // Only "Transitime" predictions - sourceSql = " AND prediction_source='Transitime'"; + sourceSql = " AND prediction_source = 'Transitime'"; } else { // Anything but "Transitime" - sourceSql = " AND prediction_source<>'Transitime'"; + sourceSql = " AND prediction_source <> 'Transitime'"; } } // Determine SQL for prediction type String predTypeSql = ""; - if (predictionType != null && !predictionType.isEmpty()) { + if (!Strings.isNullOrEmpty(predictionType)) { if (Objects.equals(source, "AffectedByWaitStop")) { // Only "AffectedByLayover" predictions predTypeSql = " AND affected_by_wait_stop = true "; @@ -421,23 +413,19 @@ public ResponseEntity predAccuracyScatterData(HttpServletRequest request String predLengthSql = " to_char(predicted_time-prediction_read_time, 'SSSS')::integer "; String predAccuracySql = " prediction_accuracy_msecs/1000 as predAccuracy "; - String sql = "SELECT " - + predLengthSql + " as predLength," - + predAccuracySql - + tooltipsSql - + " FROM prediction_accuracy " - + "WHERE " - + "1=1 " - + SqlUtils.timeRangeClause(request, "arrival_departure_time", 30) - + " AND " + predLengthSql + " < 900 " - + routeSql - + sourceSql - + predTypeSql - // Filter out MBTA_seconds source since it is isn't significantly different from MBTA_epoch. - // TODO should clean this up by not having MBTA_seconds source at all - // in the prediction accuracy module for MBTA. - + " AND prediction_source <> 'MBTA_seconds' "; + // Filter out MBTA_seconds source since it is isn't significantly different from MBTA_epoch. + // TODO should clean this up by not having MBTA_seconds source at all + // in the prediction accuracy module for MBTA. + String sql = "SELECT %s as predLength,%s%s FROM prediction_accuracy WHERE 1=1 %s AND %s < 900 %s%s%s AND prediction_source <> 'MBTA_seconds'".formatted( + predLengthSql, + predAccuracySql, + tooltipsSql, + SqlUtils.timeRangeClause(request, "arrival_departure_time", 30), + predLengthSql, + routeSql, + sourceSql, + predTypeSql); // Determine the json data by running the query String jsonString = ChartGenericJsonQuery.getJsonString(agencyId, sql); @@ -458,5 +446,4 @@ public ResponseEntity predAccuracyScatterData(HttpServletRequest request // Return the JSON data return ResponseEntity.ok(jsonString); } - } diff --git a/app/src/main/java/org/transitclock/core/reports/Reports.java b/app/src/main/java/org/transitclock/core/reports/Reports.java index 2379a28f..2f210c74 100644 --- a/app/src/main/java/org/transitclock/core/reports/Reports.java +++ b/app/src/main/java/org/transitclock/core/reports/Reports.java @@ -1,16 +1,20 @@ /* (C)2023 */ package org.transitclock.core.reports; +import java.text.DateFormat; import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.transitclock.domain.webstructs.WebAgency; + +import com.google.common.base.Strings; import org.json.JSONArray; import org.json.JSONObject; -import org.transitclock.domain.webstructs.WebAgency; -import org.transitclock.utils.Time; public class Reports { private static final int MAX_ROWS = 50000; - private static final int MAX_NUM_DAYS = 7; /** @@ -19,11 +23,12 @@ public class Reports { * * @param agencyId * @param vehicleId Which vehicle to get data for. Set to null or empty string to get data for - * all vehicles + * all vehicles * @param beginDate date to start query - * @param numdays of days to collect data for + * @param numdays of days to collect data for * @param beginTime optional time of day during the date range - * @param endTime optional time of day during the date range + * @param endTime optional time of day during the date range + * * @return AVL reports in JSON format. Can be empty JSON array if no data meets criteria. */ public static String getAvlJson( @@ -32,12 +37,16 @@ public static String getAvlJson( String timeSql = ""; WebAgency agency = WebAgency.getCachedWebAgency(agencyId); // If beginTime or endTime set but not both then use default values - if ((beginTime != null && !beginTime.isEmpty()) || (endTime != null && !endTime.isEmpty())) { - if (beginTime == null || beginTime.isEmpty()) beginTime = "00:00"; - if (endTime == null || endTime.isEmpty()) endTime = "24:00"; + if (Strings.isNullOrEmpty(beginTime) || Strings.isNullOrEmpty(endTime)) { + if (Strings.isNullOrEmpty(beginTime)) { + beginTime = "00:00"; + } + if (Strings.isNullOrEmpty(endTime)) { + endTime = "24:00"; + } } // cast('2000-01-01 01:12:00'::timestamp as time); - if (beginTime != null && !beginTime.isEmpty() && endTime != null && !endTime.isEmpty()) { + if (!Strings.isNullOrEmpty(beginTime) && !Strings.isNullOrEmpty(endTime)) { if ("mysql".equals(agency.getDbType())) { timeSql = " AND time(time) BETWEEN '" + beginTime + "' AND '" + endTime + "' "; } else { @@ -66,7 +75,9 @@ public static String getAvlJson( } // If only want data for single vehicle then specify so in SQL - if (vehicleId != null && !vehicleId.isEmpty()) sql += " AND vehicle_id='" + vehicleId + "' "; + if (vehicleId != null && !vehicleId.isEmpty()) { + sql += " AND vehicle_id='" + vehicleId + "' "; + } // Make sure data is ordered by vehicleId so that can draw lines // connecting the AVL reports per vehicle properly. Also then need @@ -76,14 +87,19 @@ public static String getAvlJson( sql += "ORDER BY vehicle_id, time LIMIT " + MAX_ROWS; - String json = null; + String json; + Date startdate; try { - java.util.Date startdate = Time.parseDate(beginDate); - + if (beginDate.charAt(4) != '-') { + DateFormat defaultDateFormat = new SimpleDateFormat("MM-dd-yyyy"); + startdate = defaultDateFormat.parse(beginDate); + } else { + DateFormat altDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + startdate = altDateFormat.parse(beginDate); + } json = GenericJsonQuery.getJsonString(agencyId, sql, startdate, startdate); - } catch (ParseException e) { - json = e.getMessage(); + return e.getMessage(); } return json; @@ -238,10 +254,14 @@ public static String getScheduleAdhByStops( String beginTime, String endTime, int numDays) { - if (allowableEarly == null || allowableEarly.isEmpty()) allowableEarly = "1.0"; + if (allowableEarly == null || allowableEarly.isEmpty()) { + allowableEarly = "1.0"; + } String allowableEarlyMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableEarly) + " seconds'"; - if (allowableLate == null || allowableLate.isEmpty()) allowableLate = "4.0"; + if (allowableLate == null || allowableLate.isEmpty()) { + allowableLate = "4.0"; + } String allowableLateMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableLate) + " seconds'"; String sql = "WITH trips_early_query_with_time AS ( SELECT trip_id AS trips_early, " @@ -461,10 +481,14 @@ public static String getScheduleAdhByStops_v2( String beginTime, String endTime, int numDays) { - if (allowableEarly == null || allowableEarly.isEmpty()) allowableEarly = "1.0"; + if (allowableEarly == null || allowableEarly.isEmpty()) { + allowableEarly = "1.0"; + } String allowableEarlyMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableEarly) + " seconds'"; - if (allowableLate == null || allowableLate.isEmpty()) allowableLate = "4.0"; + if (allowableLate == null || allowableLate.isEmpty()) { + allowableLate = "4.0"; + } String allowableLateMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableLate) + " seconds'"; String sql = "WITH trips_early_query_with_time AS ( SELECT trip_id AS trips_early, " @@ -648,18 +672,22 @@ public static String getScheduleAdhByStops_v2( * * @return Stop reports in JSON format. Can be empty JSON array if no data meets criteria. */ - public static String getReportForStopById (String agencyId, - String stop, - String beginDate, - String allowableEarly, - String allowableLate, - String beginTime, - String endTime, - int numDays) { - if (allowableEarly == null || allowableEarly.isEmpty()) allowableEarly = "1.0"; + public static String getReportForStopById(String agencyId, + String stop, + String beginDate, + String allowableEarly, + String allowableLate, + String beginTime, + String endTime, + int numDays) { + if (allowableEarly == null || allowableEarly.isEmpty()) { + allowableEarly = "1.0"; + } String allowableEarlyMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableEarly) + " seconds'"; - if (allowableLate == null || allowableLate.isEmpty()) allowableLate = "4.0"; + if (allowableLate == null || allowableLate.isEmpty()) { + allowableLate = "4.0"; + } String allowableLateMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableLate) + " seconds'"; String sql = " WITH early AS (SELECT time AS early,\n" + @@ -754,7 +782,7 @@ public static String getReportForStopById (String agencyId, /** * Queries agency for AVL data and returns result as a JSON string. Limited to returning - + *

* MAX_ROWS (50,000) data points. * * @return Last AVL reports in JSON format. Can be empty JSON array if no data meets criteria. diff --git a/app/src/main/java/org/transitclock/core/reports/SqlUtils.java b/app/src/main/java/org/transitclock/core/reports/SqlUtils.java index 57a6746c..9bb1cf2b 100644 --- a/app/src/main/java/org/transitclock/core/reports/SqlUtils.java +++ b/app/src/main/java/org/transitclock/core/reports/SqlUtils.java @@ -2,13 +2,13 @@ package org.transitclock.core.reports; import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import java.text.ParseException; +import java.text.SimpleDateFormat; import org.transitclock.utils.Time; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; /** * SQL utilities for creating SQL statements using parameters passed in to a page. Intended to make @@ -23,19 +23,20 @@ public class SqlUtils { * To be called on request parameters to make sure they don't contain any SQL injection * trickery. * - * @param parameter + * @param parameters * @throws RuntimeException if problem characters detected */ - public static void throwOnSqlInjection(String parameter) { - // If null then it is not a problem - if (parameter == null) { + public static void throwOnSqlInjection(String... parameters) { + if (parameters == null) { return; } - // If parameter contains a ' or a ; then throw error to - // prevent possible SQL injection attack - if (parameter.contains("'") || parameter.contains(";")) { - throw new IllegalArgumentException("Parameter \"" + parameter + "\" not valid."); + for (String parameter : parameters) { + // If parameter contains an ' or an ; then throw error to + // prevent possible SQL injection attack + if (parameter.contains("'") || parameter.contains(";")) { + throw new IllegalArgumentException("Parameter \"" + parameter + "\" not valid."); + } } } @@ -81,7 +82,7 @@ public static String routeIdentifiersList(String r) { * NULL AND (ad.routeshortname IN ('21','5') OR ad.routeid IN ('21','5') )" */ public static String routeClause(String r, String tableAliasName) { - if (StringUtils.isEmpty(r)) + if (StringUtils.hasText(r)) return ""; String routeIdentifiers = routeIdentifiersList(r); @@ -95,7 +96,7 @@ public static String routeClause(String r, String tableAliasName) { } public static String stopClause(String id, String tableAliasName) { - if (StringUtils.isEmpty(id)) + if (StringUtils.hasText(id)) return ""; String tableAlias = ""; @@ -117,10 +118,9 @@ public static String stopClause(String id, String tableAliasName) { */ public static String timeRangeClause(HttpServletRequest request, String timeColumnName, int maxNumDays) { String beginTime = request.getParameter("beginTime"); - throwOnSqlInjection(beginTime); - String endTime = request.getParameter("endTime"); - throwOnSqlInjection(endTime); + + throwOnSqlInjection(beginTime, endTime); // Determine the time portion of the SQL // If beginTime or endTime set but not both then use default values @@ -160,10 +160,9 @@ public static String timeRangeClause(HttpServletRequest request, String timeColu .formatted(timeColumnName, beginDateStr, endDateStr, timeSql); } else { // Not using dateRange so must be using beginDate and numDays params String beginDate = request.getParameter("beginDate"); - throwOnSqlInjection(beginDate); - String numDaysStr = request.getParameter("numDays"); - throwOnSqlInjection(numDaysStr); + + throwOnSqlInjection(beginDate, numDaysStr); if (numDaysStr == null) { numDaysStr = "1"; @@ -176,14 +175,8 @@ public static String timeRangeClause(HttpServletRequest request, String timeColu if (numDays > maxNumDays) { numDays = maxNumDays; } + beginDate = validateDate(beginDate); - SimpleDateFormat currentFormat = new SimpleDateFormat("MM-dd-yyyy"); - SimpleDateFormat requiredFormat = new SimpleDateFormat("yyyy-MM-dd"); - try { - beginDate = requiredFormat.format(currentFormat.parse(beginDate)); - } catch (ParseException e) { - logger.error("Exception occurred while processing time-range clause.", e); - } return " AND %s BETWEEN '%s' AND TIMESTAMP '%s' + INTERVAL '%d day' %s " .formatted(timeColumnName, beginDate, beginDate, numDays, timeSql); } @@ -193,7 +186,6 @@ public static String timeRangeClause(HttpServletRequest request, String timeColu * Creates a SQL clause for specifying a time range. Looks at the request parameters * "beginDate", "numDays", "beginTime", and "endTime" * - * @param request Http request containing parameters for the query * @param timeColumnName name of time column for that for query * @param maxNumDays maximum number of days for query. Request parameter numDays is limited to * this value in order to make sure that query doesn't try to process too much data. @@ -207,8 +199,7 @@ public static String timeRangeClause( String beginTime, String endTime, String beginDate) { - throwOnSqlInjection(beginTime); - throwOnSqlInjection(endTime); + throwOnSqlInjection(beginTime, endTime, beginDate); // If beginTime or endTime set but not both then use default values if (beginTime == null || beginTime.isEmpty()) { @@ -220,24 +211,13 @@ public static String timeRangeClause( String timeSql = " AND " + timeColumnName + "::time BETWEEN '" + beginTime + "' AND '" + endTime + "' "; - throwOnSqlInjection(beginDate); + beginDate = validateDate(beginDate); if (numDays > maxNumDays) { numDays = maxNumDays; } - SimpleDateFormat currentFormat = new SimpleDateFormat("MM-dd-yyyy"); - SimpleDateFormat requiredFormat = new SimpleDateFormat("yyyy-MM-dd"); - try { - if (beginDate.charAt(4) != '-') { // for two patterns MM-dd-yyyy & yyyy-MM-dd - beginDate = requiredFormat.format(currentFormat.parse(beginDate)); - } else { - requiredFormat.parse(beginDate); - } - } catch (ParseException e) { - logger.error("Exception happened while processing time-range clause", e); - } return " AND %s BETWEEN '%s' AND TIMESTAMP '%s' + INTERVAL '%d day' %s " .formatted(timeColumnName, beginDate, beginDate, numDays, timeSql); @@ -252,4 +232,19 @@ public static String timeRangeClause( public static int convertMinutesToSecs(String minutes) { return (int) Double.parseDouble(minutes) * Time.SEC_PER_MIN; } + + private static String validateDate(String date) { + SimpleDateFormat currentFormat = new SimpleDateFormat("MM-dd-yyyy"); + SimpleDateFormat requiredFormat = new SimpleDateFormat("yyyy-MM-dd"); + try { + if (date.charAt(4) != '-') { + date = requiredFormat.format(currentFormat.parse(date)); + } else { + requiredFormat.parse(date); + } + } catch (ParseException e) { + logger.error("Exception happened while processing time-range clause", e); + } + return date; + } } diff --git a/libs/core/src/main/java/org/transitclock/domain/GenericQuery.java b/libs/core/src/main/java/org/transitclock/domain/GenericQuery.java index 42d4d120..5e0cb4f1 100755 --- a/libs/core/src/main/java/org/transitclock/domain/GenericQuery.java +++ b/libs/core/src/main/java/org/transitclock/domain/GenericQuery.java @@ -1,7 +1,6 @@ /* (C)2023 */ package org.transitclock.domain; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.transitclock.domain.hibernate.HibernateUtils; @@ -26,7 +25,6 @@ @Slf4j public class GenericQuery { - @Getter private final Connection connection; // Number of rows read in private int rows; diff --git a/libs/core/src/main/java/org/transitclock/properties/WebProperties.java b/libs/core/src/main/java/org/transitclock/properties/WebProperties.java new file mode 100644 index 00000000..e69de29b