Java tutorial
/* * 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()]); } }