fi.hsl.parkandride.back.FacilityHistoryDao.java Source code

Java tutorial

Introduction

Here is the source code for fi.hsl.parkandride.back.FacilityHistoryDao.java

Source

// 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);
        }
    }
}