org.javersion.store.jdbc.AbstractVersionStoreJdbc.java Source code

Java tutorial

Introduction

Here is the source code for org.javersion.store.jdbc.AbstractVersionStoreJdbc.java

Source

/*
 * Copyright 2015 Samppa Saarela
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.javersion.store.jdbc;

import com.google.common.collect.*;
import com.querydsl.core.ResultTransformer;
import com.querydsl.core.Tuple;
import com.querydsl.core.dml.StoreClause;
import com.querydsl.core.group.Group;
import com.querydsl.core.group.GroupBy;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.BooleanOperation;
import com.querydsl.sql.Configuration;
import com.querydsl.sql.SQLQuery;
import com.querydsl.sql.dml.SQLUpdateClause;
import com.querydsl.sql.types.EnumByNameType;
import com.querydsl.sql.types.EnumByOrdinalType;
import org.javersion.core.*;
import org.javersion.object.ObjectVersion;
import org.javersion.object.ObjectVersionGraph;
import org.javersion.path.PropertyPath;
import org.javersion.util.Check;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.querydsl.core.group.GroupBy.groupBy;
import static com.querydsl.core.types.Ops.EQ;
import static com.querydsl.core.types.Ops.IN;
import static com.querydsl.core.types.Projections.tuple;
import static com.querydsl.core.types.dsl.Expressions.constant;
import static com.querydsl.core.types.dsl.Expressions.predicate;
import static java.lang.System.arraycopy;
import static java.util.Arrays.asList;
import static java.util.Collections.*;
import static org.javersion.store.jdbc.RevisionType.REVISION_TYPE;
import static org.javersion.store.jdbc.VersionStatus.*;

public abstract class AbstractVersionStoreJdbc<Id, M, V extends JVersion<Id>, Batch extends AbstractUpdateBatch<Id, M, V, Options, Batch>, Options extends StoreOptions<Id, M, V>>
        implements VersionStore<Id, M> {

    private final Logger log = LoggerFactory.getLogger(AbstractVersionStoreJdbc.class);

    public static EnumByOrdinalType<VersionStatus> VERSION_STATUS_TYPE = new EnumByOrdinalType<>(
            VersionStatus.class);

    public static void registerTypes(String tablePrefix, Configuration configuration) {
        configuration.register(tablePrefix + "VERSION", "TYPE", new EnumByNameType<>(VersionType.class));
        configuration.register(tablePrefix + "VERSION", "REVISION", REVISION_TYPE);
        configuration.register(tablePrefix + "VERSION", "STATUS", VERSION_STATUS_TYPE);

        configuration.register(tablePrefix + "VERSION_PARENT", "REVISION", REVISION_TYPE);
        configuration.register(tablePrefix + "VERSION_PARENT", "PARENT_REVISION", REVISION_TYPE);
        configuration.register(tablePrefix + "VERSION_PARENT", "STATUS", VERSION_STATUS_TYPE);

        configuration.register(tablePrefix + "VERSION_PROPERTY", "REVISION", REVISION_TYPE);
        configuration.register(tablePrefix + "VERSION_PROPERTY", "STATUS", VERSION_STATUS_TYPE);
    }

    protected final Options options;

    protected final Expression<?>[] versionAndParentColumns;

    protected final ResultTransformer<List<Group>> versionAndParents;

    protected final ResultTransformer<Map<Revision, List<Tuple>>> properties;

    protected final FetchResults<Id, M> noResults = new FetchResults<>();

    protected final Set<Id> runningOptimizations = newSetFromMap(new ConcurrentHashMap<>());

    protected final GraphCache<Id, M> cache;

    protected final Function<Id, ObjectVersionGraph<M>> cacheLoader;

    /**
     * No-args constructor for proxies
     */
    protected AbstractVersionStoreJdbc() {
        options = null;
        versionAndParentColumns = null;
        versionAndParents = null;
        properties = null;
        cache = null;
        cacheLoader = null;
    }

    public AbstractVersionStoreJdbc(Options options) {
        this.options = options;

        versionAndParentColumns = without(concat(options.version.all(), GroupBy.set(options.parent.parentRevision)),
                options.version.revision);
        versionAndParents = groupBy(options.version.revision).list(versionAndParentColumns);

        Expression<?>[] propertyColumns = without(options.property.all(), options.property.revision);
        properties = groupBy(options.property.revision).as(GroupBy.list(tuple(propertyColumns)));

        this.cache = options.cacheBuilder.apply(this);
        this.cacheLoader = this.cache != null ? this.cache::load : this::getOptimizedGraph;
    }

    @Override
    public ObjectVersionGraph<M> getFullGraph(Id docId) {
        return options.transactions.readOnly(() -> doLoad(docId));
    }

    @Override
    public ObjectVersionGraph<M> getGraph(Id docId) {
        return getGraph(docId, emptyList());
    }

    @Override
    public ObjectVersionGraph<M> getGraph(Id docId, Iterable<Revision> revisions) {
        ObjectVersionGraph<M> graph = cacheLoader.apply(docId);
        if (!graph.containsAll(revisions)) {
            return getFullGraph(docId);
        }
        return graph;
    }

    @Override
    public ObjectVersionGraph<M> getOptimizedGraph(Id docId) {
        return options.transactions.readOnly(() -> doLoadOptimized(docId));
    }

    @Override
    public GraphResults<Id, M> getGraphs(Collection<Id> docIds) {
        return options.transactions.readOnly(() -> doLoad(docIds));
    }

    @Override
    public List<ObjectVersion<M>> fetchUpdates(Id docId, Revision since) {
        return options.transactions.readOnly(() -> doFetchUpdates(docId, since));
    }

    /**
     * NOTE: publish() is called in a new transaction to ensure it sees only committed versions.
     */
    @Override
    public Multimap<Id, Revision> publish() {
        Multimap<Id, Revision> result = options.transactions.writeNewRequired(this::doPublish);
        if (this.cache != null) {
            result.keySet().forEach(this.cache::refresh);
        }
        return result;
    }

    @Override
    public void prune(Id docId,
            Function<ObjectVersionGraph<M>, Predicate<VersionNode<PropertyPath, Object, M>>> keep) {
        options.transactions.writeRequired(() -> {
            doPrune(docId, keep);
            return null;
        });
    }

    @Override
    public void optimize(Id docId,
            Function<ObjectVersionGraph<M>, Predicate<VersionNode<PropertyPath, Object, M>>> keep) {
        options.transactions.writeRequired(() -> {
            ObjectVersionGraph<M> graph;
            try {
                FetchResults<Id, M> fetchResults = doFetch(docId, true);
                graph = ObjectVersionGraph.init(fetchResults.getVersions(docId));
                doOptimize(docId, graph, keep, false);
            } catch (VersionNotFoundException e) {
                graph = doLoad(docId);
                doOptimize(docId, graph, keep, true);
            }
            return null;
        });
    }

    @Override
    public void reset(Id docId) {
        options.transactions.writeRequired(() -> {
            lockAndReset(docId);
            return null;
        });
    }

    @Override
    public Batch updateBatch(Id id) {
        return updateBatch(singleton(id));
    }

    @Override
    public abstract Batch updateBatch(Collection<Id> ids);

    protected abstract FetchResults<Id, M> doFetch(Id docId, boolean optimized);

    protected abstract List<ObjectVersion<M>> doFetchUpdates(Id docId, Revision since);

    protected abstract SQLUpdateClause setOrdinal(SQLUpdateClause versionUpdateBatch, long ordinal);

    protected abstract Map<Revision, Id> getUnpublishedRevisionsForUpdate();

    protected ObjectVersionGraph<M> doLoad(Id docId) {
        FetchResults<Id, M> results = doFetch(docId, false);
        return results.containsKey(docId) ? results.getVersionGraph(docId) : ObjectVersionGraph.init();
    }

    protected ObjectVersionGraph<M> doLoadOptimized(Id docId) {
        return toVersionGraph(docId, doFetch(docId, true));
    }

    protected GraphResults<Id, M> doLoad(Collection<Id> docIds) {
        Check.notNull(docIds, "docIds");
        final boolean optimized = true;

        BooleanExpression predicate = predicate(IN, options.version.docId, constant(docIds))
                .and(options.version.ordinal.isNotNull());

        List<Group> versionsAndParents = fetchVersionsAndParents(optimized, predicate,
                options.version.ordinal.asc());

        return toGraphResults(fetch(versionsAndParents, optimized, predicate));
    }

    protected GraphResults<Id, M> toGraphResults(FetchResults<Id, M> fetchResults) {
        ImmutableMap.Builder<Id, ObjectVersionGraph<M>> graphs = ImmutableMap.builder();
        for (Id docId : fetchResults.getDocIds()) {
            graphs.put(docId, toVersionGraph(docId, fetchResults));
        }
        return new GraphResults<>(graphs.build(), fetchResults.latestRevision);
    }

    protected ObjectVersionGraph<M> toVersionGraph(Id docId, FetchResults<Id, M> fetchResults) {
        if (fetchResults.containsKey(docId)) {
            ObjectVersionGraph<M> graph;
            try {
                graph = ObjectVersionGraph.init(fetchResults.getVersions(docId));
                if (options.optimizeWhen.test(graph)) {
                    optimizeAsync(docId, graph, false);
                }
            } catch (VersionNotFoundException e) {
                graph = doLoad(docId);
                optimizeAsync(docId, graph, true);
            }
            return graph;
        } else {
            return ObjectVersionGraph.init();
        }
    }

    protected Multimap<Id, Revision> doPublish() {
        Map<Revision, Id> uncommittedRevisions = getUnpublishedRevisionsForUpdate();
        long lastOrdinal = getMaxOrdinal();

        log.debug("publish({})", uncommittedRevisions.size());
        if (uncommittedRevisions.isEmpty()) {
            return ImmutableMultimap.of();
        }

        Multimap<Id, Revision> publishedDocs = ArrayListMultimap.create();

        SQLUpdateClause versionUpdateBatch = options.queryFactory.update(options.version);

        for (Map.Entry<Revision, Id> entry : uncommittedRevisions.entrySet()) {
            Revision revision = entry.getKey();
            Id docId = entry.getValue();
            publishedDocs.put(docId, revision);
            setOrdinal(versionUpdateBatch, ++lastOrdinal).where(options.version.revision.eq(revision)).addBatch();
        }

        versionUpdateBatch.execute();

        afterPublish(publishedDocs);
        return publishedDocs;
    }

    protected void doPrune(Id docId,
            Function<ObjectVersionGraph<M>, Predicate<VersionNode<PropertyPath, Object, M>>> keep) {
        lockForMaintenance(docId);
        doReset(docId);
        ObjectVersionGraph<M> graph = doLoad(docId);
        updateBatch(ImmutableSet.of()).prune(graph, keep.apply(graph)).execute();
    }

    protected void optimizeAsync(Id docId, ObjectVersionGraph<M> baseGraph, boolean reset) {
        if (options.optimizer != null && runningOptimizations.add(docId)) {
            options.optimizer.execute(() -> {
                try {
                    options.transactions.writeNewRequired(() -> {
                        doOptimize(docId, baseGraph, options.optimizeKeep, reset);
                        return null;
                    });
                } finally {
                    runningOptimizations.remove(docId);
                }
            });
        }
    }

    protected void doOptimize(Id docId, ObjectVersionGraph<M> graph,
            Function<ObjectVersionGraph<M>, Predicate<VersionNode<PropertyPath, Object, M>>> keep, boolean reset) {
        if (reset) {
            log.info("reoptimize({})", docId);
        } else {
            log.debug("optimize({})", docId);
        }
        lockForMaintenance(docId);
        if (reset) {
            long revived = doReset(docId);
            if (revived == 0) {
                throw new ConcurrentMaintenanceException("Expected to revive some versions");
            }
        }
        updateBatch(ImmutableSet.of()).optimize(graph, keep.apply(graph)).execute();
    }

    protected abstract void lockForMaintenance(Id docId);

    protected void lockAndReset(Id docId) {
        lockForMaintenance(docId);
        doReset(docId);
    }

    protected long doReset(Id docId) {
        final BooleanOperation docIdEquals = predicate(EQ, options.version.docId, constant(docId));

        final SQLQuery<Revision> docRevisions = options.queryFactory.select(options.version.revision)
                .from(options.version).where(docIdEquals);

        // Revive squashed versions
        long revived = options.queryFactory.update(options.version).set(options.version.status, ACTIVE)
                .where(docIdEquals, options.version.status.eq(SQUASHED)).execute();

        // Delete redundant parents
        options.queryFactory.delete(options.parent)
                .where(options.parent.revision.in(docRevisions), options.parent.status.eq(REDUNDANT)).execute();

        // Revive squashed parents
        options.queryFactory.update(options.parent).set(options.parent.status, ACTIVE)
                .where(options.parent.revision.in(docRevisions), options.parent.status.eq(SQUASHED)).execute();

        // Delete redundant properties
        options.queryFactory.delete(options.property)
                .where(options.property.revision.in(docRevisions), options.property.status.eq(REDUNDANT)).execute();

        // Revive squashed properties
        options.queryFactory.update(options.property).set(options.property.status, ACTIVE)
                .where(options.property.revision.in(docRevisions), options.property.status.eq(SQUASHED)).execute();

        return revived;
    }

    /**
     * Override to read metadata from VERSION table.
     *
     * @see AbstractUpdateBatch#setMeta(Object, StoreClause)
     */
    protected M getMeta(Group versionAndParents) {
        return null;
    }

    @SuppressWarnings("unused")
    protected void afterPublish(Multimap<Id, Revision> publishedDocs) {
        // After publish hook for sub classes to override
    }

    protected long getMaxOrdinal() {
        Long maxOrdinal = options.queryFactory.select(options.version.ordinal.max()).from(options.version)
                .fetchFirst();
        return maxOrdinal != null ? maxOrdinal : 0;
    }

    protected FetchResults<Id, M> fetch(List<Group> versionsAndParents, boolean optimized,
            BooleanExpression predicate) {
        if (versionsAndParents.isEmpty()) {
            return noResults;
        }

        Map<Revision, List<Tuple>> properties = fetchProperties(optimized, predicate);
        ListMultimap<Id, ObjectVersion<M>> results = ArrayListMultimap.create();
        Revision latestRevision = null;

        for (Group versionAndParents : versionsAndParents) {
            Id id = versionAndParents.getOne(options.version.docId);
            latestRevision = versionAndParents.getOne(options.version.revision);
            Map<PropertyPath, Object> changeset = toChangeSet(properties.get(latestRevision));

            results.put(id, buildVersion(latestRevision, versionAndParents, changeset));
        }
        return new FetchResults<>(results, latestRevision);
    }

    protected Map<Revision, List<Tuple>> fetchProperties(boolean optimized, BooleanExpression predicate) {
        SQLQuery<?> qry = options.queryFactory.from(options.property).where(predicate);

        if (optimized) {
            qry.innerJoin(options.version).on(options.version.revision.eq(options.property.revision),
                    options.version.status.goe(ACTIVE));
            qry.where(options.property.status.goe(ACTIVE));
        } else {
            qry.innerJoin(options.version).on(options.version.revision.eq(options.property.revision));
            qry.where(options.property.status.loe(ACTIVE));
        }
        return qry.transform(properties);
    }

    protected List<Group> fetchVersionsAndParents(boolean optimized, BooleanExpression predicate,
            OrderSpecifier<?> orderBy) {
        SQLQuery<?> qry = options.queryFactory.from(options.version).where(predicate).orderBy(orderBy);

        if (optimized) {
            qry.leftJoin(options.parent).on(options.parent.revision.eq(options.version.revision),
                    options.parent.status.goe(ACTIVE));
            qry.where(options.version.status.goe(ACTIVE));
        } else {
            qry.leftJoin(options.parent).on(options.parent.revision.eq(options.version.revision),
                    options.parent.status.loe(ACTIVE));
            qry.where(options.version.status.loe(ACTIVE));
        }

        return qry.transform(versionAndParents);
    }

    protected List<Group> verifyVersionsAndParentsSince(List<Group> versionsAndParents, Revision since) {
        if (versionsAndParents.isEmpty()) {
            throw new VersionNotFoundException(since);
        }
        if (versionsAndParents.size() == 1 && versionsAndParents.get(0).getOne(options.version.revision) == null) {
            return ImmutableList.of();
        }
        return versionsAndParents;
    }

    protected ObjectVersion<M> buildVersion(Revision rev, Group versionAndParents,
            Map<PropertyPath, Object> changeset) {
        if (!options.versionTableProperties.isEmpty()) {
            if (changeset == null) {
                changeset = new HashMap<>();
            }
            for (Map.Entry<PropertyPath, Path<?>> entry : options.versionTableProperties.entrySet()) {
                PropertyPath path = entry.getKey();
                @SuppressWarnings("unchecked")
                Path<Object> column = (Path<Object>) entry.getValue();
                changeset.put(path, versionAndParents.getOne(column));
            }
        }
        return new ObjectVersion.Builder<M>(rev).branch(versionAndParents.getOne(options.version.branch))
                .type(versionAndParents.getOne(options.version.type))
                .parents(versionAndParents.getSet(options.parent.parentRevision)).changeset(changeset)
                .meta(getMeta(versionAndParents)).build();
    }

    protected Map<PropertyPath, Object> toChangeSet(List<Tuple> properties) {
        if (properties == null) {
            return null;
        }
        Map<PropertyPath, Object> changeset = Maps.newHashMapWithExpectedSize(properties.size());
        for (Tuple tuple : properties) {
            PropertyPath path = PropertyPath.parse(tuple.get(options.property.path));
            Object value = getPropertyValue(path, tuple);
            changeset.put(path, value);
        }
        return changeset;
    }

    @SuppressWarnings("unused")
    protected Object getPropertyValue(PropertyPath path, Tuple tuple) {
        String type = firstNonNull(tuple.get(options.property.type), "N");
        String str = tuple.get(options.property.str);
        Long nbr = tuple.get(options.property.nbr);

        switch (type.charAt(0)) {
        case 'O':
            return Persistent.object(str);
        case 'A':
            return Persistent.array();
        case 's':
            return str;
        case 'b':
            return nbr != null ? nbr != 0 : null;
        case 'l':
            return nbr;
        case 'd':
            return nbr != null ? Double.longBitsToDouble(nbr) : null;
        case 'D':
            return !isNullOrEmpty(str) ? new BigDecimal(str) : null;
        case 'N':
            return Persistent.NULL;
        case 'n':
            return null;
        default:
            throw new IllegalArgumentException("Unsupported type: " + type);
        }
    }

    protected static Expression<?>[] concat(Expression<?>[] expr1, Expression<?>... expr2) {
        Expression<?>[] expressions = new Expression<?>[expr1.length + expr2.length];
        arraycopy(expr1, 0, expressions, 0, expr1.length);
        arraycopy(expr2, 0, expressions, expr1.length, expr2.length);
        return expressions;
    }

    @Nonnull
    protected static Expression<?>[] without(Expression<?>[] expressions, Expression<?> expr) {
        List<Expression<?>> list = new ArrayList<>(asList(expressions));
        list.remove(expr);
        return list.toArray(new Expression<?>[list.size()]);
    }

}