// Copyright © 2015 HSL <https://www.hsl.fi>
// This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses.
package fi.hsl.parkandride.back;
import com.querydsl.core.Tuple;
import com.querydsl.core.dml.StoreClause;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.MappingProjection;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.sql.SQLExpressions;
import com.querydsl.sql.dml.SQLInsertClause;
import com.querydsl.sql.postgresql.PostgreSQLQuery;
import com.querydsl.sql.postgresql.PostgreSQLQueryFactory;
import fi.hsl.parkandride.back.sql.QFacilityCapacityHistory;
import fi.hsl.parkandride.back.sql.QFacilityStatusHistory;
import fi.hsl.parkandride.back.sql.QUnavailableCapacityHistory;
import fi.hsl.parkandride.core.back.FacilityHistoryRepository;
import fi.hsl.parkandride.core.domain.*;
import fi.hsl.parkandride.core.service.TransactionalRead;
import fi.hsl.parkandride.core.service.TransactionalWrite;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import static com.querydsl.core.group.GroupBy.groupBy;
import static com.querydsl.core.group.GroupBy.list;
import static com.querydsl.core.types.Projections.constructor;
import static com.querydsl.sql.SQLExpressions.select;
import static fi.hsl.parkandride.core.domain.CapacityType.*;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
public class FacilityHistoryDao implements FacilityHistoryRepository {
public static final String STATUS_HISTORY_ID_SEQ = "facility_status_history_seq";
public static final String CAPACITY_HISTORY_ID_SEQ = "facility_capacity_history_seq";
private static final QFacilityStatusHistory qFacilityStatusHistory = QFacilityStatusHistory.facilityStatusHistory;
private static final QFacilityCapacityHistory qFacilityCapacityHistory = QFacilityCapacityHistory.facilityCapacityHistory;
private static final QUnavailableCapacityHistory qUnavailableCapacityHistory = QUnavailableCapacityHistory.unavailableCapacityHistory;
private static final MappingProjection<UnavailableCapacity> unavailableCapacityHistoryMapping = new MappingProjection<UnavailableCapacity>(UnavailableCapacity.class, qUnavailableCapacityHistory.all()) {
@Override
protected UnavailableCapacity map(Tuple row) {
final UnavailableCapacity uc = new UnavailableCapacity();
uc.capacityType = row.get(qUnavailableCapacityHistory.capacityType);
uc.usage = row.get(qUnavailableCapacityHistory.usage);
uc.capacity = row.get(qUnavailableCapacityHistory.capacity);
return uc;
}
};
private static final Expression<Long> nextStatusHistoryId = SQLExpressions.nextval(STATUS_HISTORY_ID_SEQ);
private static final Expression<Long> nextCapacityHistoryId = SQLExpressions.nextval(CAPACITY_HISTORY_ID_SEQ);
private static final MultilingualStringMapping statusHistoryDescriptionMapping = new MultilingualStringMapping(
qFacilityStatusHistory.statusDescriptionFi,
qFacilityStatusHistory.statusDescriptionSv,
qFacilityStatusHistory.statusDescriptionEn
);
private final PostgreSQLQueryFactory queryFactory;
public FacilityHistoryDao(PostgreSQLQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
private static void populateCapacity(NumberPath<Integer> path, Integer value, StoreClause store) {
if (value == null || value < 1) {
store.setNull(path);
} else {
store.set(path, value);
}
}
private static void mapCapacity(Map<CapacityType, Integer> capacities, CapacityType type, Integer capacity) {
if (capacity != null && capacity > 0) {
capacities.put(type, capacity);
}
}
@Override
@TransactionalWrite
public void updateCapacityHistory(DateTime currentDate, long facilityId, Map<CapacityType, Integer> builtCapacity, List<UnavailableCapacity> unavailableCapacities) {
setEndDateForPreviousCapacityHistoryEntry(facilityId, currentDate);
final long historyEntryId = insertNewCapacityHistoryEntry(facilityId, currentDate, builtCapacity);
insertUnavailableCapacitiesHistory(historyEntryId, unavailableCapacities);
}
private void insertUnavailableCapacitiesHistory(long historyEntryId, List<UnavailableCapacity> unavailableCapacities) {
if (unavailableCapacities.isEmpty()) {
return;
}
final SQLInsertClause insert = queryFactory.insert(qUnavailableCapacityHistory);
unavailableCapacities.forEach(uc -> {
insert.set(qUnavailableCapacityHistory.capacityHistoryId, historyEntryId)
.set(qUnavailableCapacityHistory.capacityType, uc.capacityType)
.set(qUnavailableCapacityHistory.usage, uc.usage)
.set(qUnavailableCapacityHistory.capacity, uc.capacity);
insert.addBatch();
});
insert.execute();
}
@Override
@TransactionalWrite
public void updateStatusHistory(DateTime currentDate, long facilityId, FacilityStatus newStatus, MultilingualString statusDescription) {
setEndDateForPreviousStateHistoryEntry(facilityId, currentDate);
insertNewStatusHistoryEntry(facilityId, currentDate, newStatus, statusDescription);
}
private long insertNewStatusHistoryEntry(long facilityId, DateTime currentDate, FacilityStatus newStatus, MultilingualString statusDescription) {
final SQLInsertClause insert = queryFactory.insert(qFacilityStatusHistory);
statusHistoryDescriptionMapping.populate(statusDescription, insert);
return insert
.set(qFacilityStatusHistory.id, select(nextStatusHistoryId))
.set(qFacilityStatusHistory.facilityId, facilityId)
.set(qFacilityStatusHistory.status, newStatus)
.set(qFacilityStatusHistory.startTs, currentDate)
.execute();
}
private long insertNewCapacityHistoryEntry(long facilityId, DateTime currentDate, Map<CapacityType, Integer> builtCapacity) {
final Long historyEntryId = queryFactory.query().select(nextCapacityHistoryId).fetchOne();
final SQLInsertClause insert = queryFactory.insert(qFacilityCapacityHistory);
populateCapacity(qFacilityCapacityHistory.capacityCar, builtCapacity.get(CAR), insert);
populateCapacity(qFacilityCapacityHistory.capacityDisabled, builtCapacity.get(DISABLED), insert);
populateCapacity(qFacilityCapacityHistory.capacityElectricCar, builtCapacity.get(ELECTRIC_CAR), insert);
populateCapacity(qFacilityCapacityHistory.capacityMotorcycle, builtCapacity.get(MOTORCYCLE), insert);
populateCapacity(qFacilityCapacityHistory.capacityBicycle, builtCapacity.get(BICYCLE), insert);
populateCapacity(qFacilityCapacityHistory.capacityBicycleSecureSpace, builtCapacity.get(BICYCLE_SECURE_SPACE), insert);
insert.set(qFacilityCapacityHistory.id, historyEntryId)
.set(qFacilityCapacityHistory.facilityId, facilityId)
.set(qFacilityCapacityHistory.startTs, currentDate)
.execute();
return historyEntryId;
}
private void setEndDateForPreviousStateHistoryEntry(long facilityId, DateTime currentDate) {
final Tuple lastHistoryEntry = queryFactory.query()
.select(qFacilityStatusHistory.id, qFacilityStatusHistory.startTs)
.from(qFacilityStatusHistory)
.where(qFacilityStatusHistory.facilityId.eq(facilityId))
.orderBy(qFacilityStatusHistory.endTs.desc().nullsFirst())
.fetchFirst();
if (lastHistoryEntry != null) {
Long lastHistoryEntryId = lastHistoryEntry.get(qFacilityStatusHistory.id);
final DateTime lastEntryStartTs = lastHistoryEntry.get(qFacilityStatusHistory.startTs);
validateTimestamp(facilityId, currentDate, lastEntryStartTs, lastHistoryEntryId);
queryFactory.update(qFacilityStatusHistory)
.set(qFacilityStatusHistory.endTs, currentDate)
.where(qFacilityStatusHistory.id.eq(lastHistoryEntryId))
.execute();
}
}
private void setEndDateForPreviousCapacityHistoryEntry(long facilityId, DateTime currentDate) {
final Tuple lastHistoryEntry = queryFactory.query()
.select(qFacilityCapacityHistory.id, qFacilityCapacityHistory.startTs)
.from(qFacilityCapacityHistory)
.where(qFacilityCapacityHistory.facilityId.eq(facilityId))
.orderBy(qFacilityCapacityHistory.endTs.desc().nullsFirst())
.fetchFirst();
if (lastHistoryEntry != null) {
Long lastHistoryEntryId = lastHistoryEntry.get(qFacilityCapacityHistory.id);
final DateTime lastEntryStartTs = lastHistoryEntry.get(qFacilityCapacityHistory.startTs);
validateTimestamp(facilityId, currentDate, lastEntryStartTs, lastHistoryEntryId);
queryFactory.update(qFacilityCapacityHistory)
.set(qFacilityCapacityHistory.endTs, currentDate)
.where(qFacilityCapacityHistory.id.eq(lastHistoryEntryId))
.execute();
}
}
private static void validateTimestamp(long facilityId, DateTime currentDate, DateTime lastEntryStartTs, Long lastHistoryEntryId) {
if (lastEntryStartTs.isAfter(currentDate)) {
throw new IllegalStateException(String.format(
"Trying to set end date <%s> before start date <%s> for last history entry <id=%d> of facility <id=%d>! Something is really wrong!",
currentDate, lastEntryStartTs, lastHistoryEntryId, facilityId
));
}
}
@Override
@TransactionalRead
public List<FacilityStatusHistory> getStatusHistory(long facilityId) {
return getStatusHistoryQuery(facilityId).fetch();
}
@Override
@TransactionalRead
public List<FacilityStatusHistory> getStatusHistory(long facilityId, LocalDate startInclusive, LocalDate endInclusive) {
return getStatusHistoryQuery(facilityId)
.where(qFacilityStatusHistory.startTs.loe(endInclusive.toDateTimeAtStartOfDay().millisOfDay().withMaximumValue()).andAnyOf(
qFacilityStatusHistory.endTs.isNull(),
qFacilityStatusHistory.endTs.goe(startInclusive.toDateTimeAtStartOfDay())
)).fetch();
}
private PostgreSQLQuery<FacilityStatusHistory> getStatusHistoryQuery(long facilityId) {
return queryFactory.query()
.select(constructor(
FacilityStatusHistory.class,
qFacilityStatusHistory.facilityId,
qFacilityStatusHistory.startTs,
qFacilityStatusHistory.endTs,
qFacilityStatusHistory.status,
statusHistoryDescriptionMapping
))
.from(qFacilityStatusHistory)
.where(qFacilityStatusHistory.facilityId.eq(facilityId))
.orderBy(qFacilityStatusHistory.startTs.asc());
}
@Override
@TransactionalRead
public List<FacilityCapacityHistory> getCapacityHistory(long facilityId) {
return getFacilityCapacityHistories(facilityId, q -> {});
}
@Override
@TransactionalRead
public List<FacilityCapacityHistory> getCapacityHistory(long facilityId, LocalDate startInclusive, LocalDate endInclusive) {
return getFacilityCapacityHistories(facilityId, q -> q.where(
qFacilityCapacityHistory.startTs.loe(endInclusive.toDateTimeAtStartOfDay().millisOfDay().withMaximumValue()).andAnyOf(
qFacilityCapacityHistory.endTs.isNull(),
qFacilityCapacityHistory.endTs.goe(startInclusive.toDateTimeAtStartOfDay())
)));
}
private List<FacilityCapacityHistory> getFacilityCapacityHistories(long facilityId, Consumer<PostgreSQLQuery<ExtendedCapacityHistory>> modifyQuery) {
final List<ExtendedCapacityHistory> capacityHistory = getCapacityHistoryWithoutUnavailableCapacities(facilityId, modifyQuery);
final Set<Long> historyEntryIds = capacityHistory.stream().map(c -> c.id).collect(toSet());
final Map<Long, List<UnavailableCapacity>> unavailableCapacities = queryFactory
.from(qUnavailableCapacityHistory)
.where(qUnavailableCapacityHistory.capacityHistoryId.in(historyEntryIds))
.transform(groupBy(qUnavailableCapacityHistory.capacityHistoryId).as(list(unavailableCapacityHistoryMapping)));
return capacityHistory.stream()
.map(entry -> {
entry.unavailableCapacities = unavailableCapacities.get(entry.id);
return entry.strip();
})
.collect(toList());
}
private List<ExtendedCapacityHistory> getCapacityHistoryWithoutUnavailableCapacities(long facilityId, Consumer<PostgreSQLQuery<ExtendedCapacityHistory>> modifyQuery) {
final PostgreSQLQuery<ExtendedCapacityHistory> q = queryFactory.query()
.select(constructor(
ExtendedCapacityHistory.class,
qFacilityCapacityHistory.id,
qFacilityCapacityHistory.facilityId,
qFacilityCapacityHistory.startTs,
qFacilityCapacityHistory.endTs,
new MappingProjection<Map<CapacityType, Integer>>(Map.class, qFacilityCapacityHistory.all()) {
@Override
protected Map<CapacityType, Integer> map(Tuple row) {
final Map<CapacityType, Integer> map = new HashMap<>();
mapCapacity(map, CAR, row.get(qFacilityCapacityHistory.capacityCar));
mapCapacity(map, DISABLED, row.get(qFacilityCapacityHistory.capacityDisabled));
mapCapacity(map, ELECTRIC_CAR, row.get(qFacilityCapacityHistory.capacityElectricCar));
mapCapacity(map, MOTORCYCLE, row.get(qFacilityCapacityHistory.capacityMotorcycle));
mapCapacity(map, BICYCLE, row.get(qFacilityCapacityHistory.capacityBicycle));
mapCapacity(map, BICYCLE_SECURE_SPACE, row.get(qFacilityCapacityHistory.capacityBicycleSecureSpace));
return map;
}
}
))
.from(qFacilityCapacityHistory)
.where(qFacilityCapacityHistory.facilityId.eq(facilityId))
.orderBy(qFacilityCapacityHistory.startTs.asc());
modifyQuery.accept(q);
return q.fetch();
}
public static class ExtendedCapacityHistory extends FacilityCapacityHistory {
public Long id;
public ExtendedCapacityHistory(Long id, Long facilityId, DateTime startDate, DateTime endDate, Map<CapacityType, Integer> builtCapacity) {
super(facilityId, startDate, endDate, builtCapacity);
this.id = id;
}
public FacilityCapacityHistory strip() {
return new FacilityCapacityHistory(
facilityId,
startDate,
endDate,
builtCapacity,
unavailableCapacities
);
}
}
}