/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community 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://opensource.org/licenses/ecl2.txt * * 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.opencastproject.assetmanager.impl.query; import static com.entwinemedia.fn.Stream.$; import org.opencastproject.assetmanager.api.Property; import org.opencastproject.assetmanager.api.Snapshot; import org.opencastproject.assetmanager.api.query.ARecord; import org.opencastproject.assetmanager.api.query.AResult; import org.opencastproject.assetmanager.api.query.ASelectQuery; import org.opencastproject.assetmanager.api.query.Order; import org.opencastproject.assetmanager.api.query.Predicate; import org.opencastproject.assetmanager.impl.AbstractAssetManager; import org.opencastproject.assetmanager.impl.RuntimeTypes; import org.opencastproject.assetmanager.impl.persistence.EntityPaths; import org.opencastproject.assetmanager.impl.persistence.PropertyDto; import org.opencastproject.assetmanager.impl.persistence.QPropertyDto; import org.opencastproject.assetmanager.impl.persistence.SnapshotDto; import org.opencastproject.util.RequireUtil; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.Fn2; import com.entwinemedia.fn.Stream; import com.entwinemedia.fn.data.Opt; import com.entwinemedia.fn.data.SetB; import com.entwinemedia.fn.fns.Booleans; import com.mysema.query.Tuple; import com.mysema.query.jpa.impl.JPAQuery; import com.mysema.query.jpa.impl.JPAQueryFactory; import com.mysema.query.types.EntityPath; import com.mysema.query.types.Expression; import com.mysema.query.types.OrderSpecifier; import com.mysema.query.types.expr.BooleanExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; public abstract class AbstractASelectQuery implements ASelectQuery, SelectQueryContributor, EntityPaths { protected static final Logger logger = LoggerFactory.getLogger(AbstractASelectQuery.class); private final AbstractASelectQuery self = this; private final AbstractAssetManager am; public AbstractASelectQuery(AbstractAssetManager am) { this.am = am; } @Override public ASelectQuery where(final Predicate predicate) { return new AbstractASelectQuery(am) { @Override public SelectQueryContribution contributeSelect(JPAQueryFactory f) { final SelectQueryContribution predicateContrib = RuntimeTypes.convert(predicate).contributeSelect(f); return self.contributeSelect(f) .addFrom(predicateContrib.from) .addJoin(predicateContrib.join) .andWhere(predicateContrib.where); } @Override public String toString() { return "where " + predicate; } }; } @Override public ASelectQuery page(final int offset, final int size) { return new AbstractASelectQuery(am) { @Override public SelectQueryContribution contributeSelect(JPAQueryFactory f) { return self.contributeSelect(f).offset(offset).limit(size); } }; } @Override public ASelectQuery orderBy(final Order order) { return new AbstractASelectQuery(am) { @Override public SelectQueryContribution contributeSelect(JPAQueryFactory f) { final SelectQueryContribution orderContrib = RuntimeTypes.convert(order).contributeSelect(f); return self.contributeSelect(f).addOrder(orderContrib.order).andWhere(orderContrib.where); } }; } @Override public AResult run() { return am.getDb().run(new Fn<JPAQueryFactory, AResult>() { @Override public AResult apply(JPAQueryFactory f) { return run(f); } }); } private AResult run(JPAQueryFactory f) { // run query and map the result to records final long startTime = System.nanoTime(); // resolve AST final SelectQueryContribution r = contributeSelect(f); final boolean toFetchProperties = r.fetch.exists(Booleans.<Expression<?>>eq(QPropertyDto.propertyDto)); // # create Querydsl query final JPAQuery q = f.query(); // # from { // Make sure that the snapshotDto is always contained in the from clause because the media package ID and // the ID are always selected. // Use a mutable hash set to be able to use the removeAll operation. final Set<EntityPath<?>> from = Stream.<EntityPath<?>>mk(Q_SNAPSHOT) .append(r.from) // all collected from clauses .append(r.join.map(Join.getFrom)) // all from clauses from the joins .toSet(SetB.MH); // Now remove everything that will be joined. Adding them in both the from and a join // clause is not allowed. from.removeAll(r.join.map(Join.getJoin).toSet()); q.from(JpaFns.toEntityPathArray(from)); } // # join if (!r.join.isEmpty()) { // Group joins by entity and combine all "on" clauses with "or" expressions. // This way there is only one join clause per distinct entity which eliminates the need to alias entities // like this `new QPropertyDto("alias")`. // Entity aliasing produces many issues which seem to cause a huge rewrite of the query building mechanism // so it should be prevented at all costs. final Map<EntityPath<?>, BooleanExpression> joins = r.join.foldl( new HashMap<EntityPath<?>, BooleanExpression>(), new Fn2<Map<EntityPath<?>, BooleanExpression>, Join, Map<EntityPath<?>, BooleanExpression>>() { @Override public Map<EntityPath<?>, BooleanExpression> apply(Map<EntityPath<?>, BooleanExpression> sum, Join join) { // get the on expression saved with the join, may be null final BooleanExpression existing = sum.get(join.join); final BooleanExpression combined; // combine the existing and the current expression if (existing == null) { combined = join.on; } else if (existing.equals(join.on)) { // if both expressions are equal there is no need to combine them combined = existing; } else { // if different combine with logical "or" combined = existing.or(join.on); } sum.put(join.join, combined); return sum; } }); for (final Map.Entry<EntityPath<?>, BooleanExpression> j : joins.entrySet()) { q.leftJoin(j.getKey()).on(j.getValue()); } } // # where q.where(r.where.orNull()); // # paging for (Integer a : r.offset) { q.offset(a); } for (Integer a : r.limit) { q.limit(a); } // # order for (OrderSpecifier<?> a : r.order) { q.orderBy(a); } // # distinct if (!toFetchProperties) { // if no properties shall be fetched the result set can be distinct q.distinct(); } // # fetch // create parameters for fetch clause, i.e. Querydsl's list() method final List<Expression<?>> fetch; { // check if the media package ID needs to be selected separately if (r.fetch.exists(MandatoryFetch.exists)) { fetch = r.fetch.toList(); } else { fetch = r.fetch.append(MandatoryFetch.fetch).toList(); } } // Run the query and transform the result into records final Stream<ARecordImpl> records; { // run query am.getDb().logQuery(q); final List<Tuple> result = q.list(JpaFns.toExpressionArray(fetch)); logger.debug("Pure query ms " + (System.nanoTime() - startTime) / 1000000); // map result based on the fact whether properties have been fetched or not if (!toFetchProperties) { // No properties have been fetched -> each result row (tuple) is a distinct record (snapshot). records = $(result).map(toARecord(r)); } else { logger.trace("Fetched properties"); // Properties have been fetched -> there may be multiple rows (tuples) per snapshot because of the join with the property table. // Extract records and properties and link them together. // group properties after their media package ID and make sure that no duplicate properties occur final Map<String, Set<Property>> propertiesPerMp = $(result).bind(toProperty) .foldl(new HashMap<String, Set<Property>>(), new Fn2<Map<String, Set<Property>>, Property, Map<String, Set<Property>>>() { @Override public Map<String, Set<Property>> apply(Map<String, Set<Property>> sum, Property p) { final String mpId = p.getId().getMediaPackageId(); final Set<Property> props = sum.get(mpId); if (props != null) { props.add(p); } else { sum.put(mpId, SetB.MH.mk(p)); } return sum; } }); // group records after their media package ID final Map<String, List<ARecordImpl>> distinctRecords = $($(result).map(toARecord(r)).toSet()).groupMulti(ARecordImpl.getMediaPackageId); records = $(distinctRecords.values()).bind(new Fn<List<ARecordImpl>, Iterable<ARecordImpl>>() { @Override public Iterable<ARecordImpl> apply(List<ARecordImpl> records) { return $(records).map(new Fn<ARecordImpl, ARecordImpl>() { @Override public ARecordImpl apply(ARecordImpl record) { final Set<Property> properties = propertiesPerMp.get(record.getMediaPackageId()); final Stream<Property> p = properties != null ? $(properties) : Stream.<Property>empty(); return new ARecordImpl(record.getSnapshotId(), record.getMediaPackageId(), p, record.getSnapshot()); } }); } }); } } final long searchTime = (System.nanoTime() - startTime) / 1000000; logger.debug("Complete query ms " + searchTime); return new AResultImpl(AbstractASelectQuery.<ARecord>vary(records), sizeOf(records), r.offset.getOr(0), r.limit.getOr(-1), searchTime); } /** * Transform a Querydsl result {@link Tuple} into an {@link ARecord}. * To do the transformation I need to know what targets have been selected. */ private Fn<Tuple, ARecordImpl> toARecord(final SelectQueryContribution c) { return new Fn<Tuple, ARecordImpl>() { @Override public ARecordImpl apply(Tuple tuple) { final String mediaPackageId; final Opt<Snapshot> snapshot; final long id; // Only fetch the snapshot if it is in the fetch list. if (c.fetch.exists(Booleans.<Expression<?>>eq(Q_SNAPSHOT))) { final SnapshotDto snapshotDto = RequireUtil.notNull(tuple.get(Q_SNAPSHOT), "[BUG] snapshot table data"); id = snapshotDto.getId(); mediaPackageId = snapshotDto.getMediaPackageId(); // make sure the delivered media package has valid URIs snapshot = Opt.some(am.getHttpAssetProvider().prepareForDelivery(snapshotDto.toSnapshot())); } else { // The media package ID and the snapshot's database ID must always be fetched. id = RequireUtil.notNull(tuple.get(Q_SNAPSHOT.id), "[BUG] snapshot table id"); mediaPackageId = RequireUtil.notNull(tuple.get(Q_SNAPSHOT.mediaPackageId), "[BUG] snapshot table media package id"); snapshot = Opt.none(); } return new ARecordImpl(id, mediaPackageId, Stream.<Property>empty(), snapshot); } }; } private static Fn<Tuple, Opt<Property>> toProperty = new Fn<Tuple, Opt<Property>>() { @Override public Opt<Property> apply(Tuple tuple) { final PropertyDto dto = tuple.get(Q_PROPERTY); return dto != null ? Opt.some(dto.toProperty()) : Opt.<Property>none(); } }; /** * Specification of fields whose fetch is mandatory. */ private static final class MandatoryFetch { static final Fn<Expression<?>, Boolean> exists = Booleans.<Expression<?>>eq(Q_SNAPSHOT) .or(Booleans.<Expression<?>>eq(Q_SNAPSHOT.mediaPackageId)) .or(Booleans.<Expression<?>>eq(Q_SNAPSHOT.id)); static final Stream<Expression<?>> fetch = Stream.<Expression<?>>mk(Q_SNAPSHOT.mediaPackageId, Q_SNAPSHOT.id); } private static <A> Stream<A> vary(Stream<? extends A> a) { return (Stream<A>) a; } private static <A> int sizeOf(Stream<A> stream) { int count = 0; for (A ignore : stream) { count++; } return count; } }