/* * #! * Ontopia Engine * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * 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 net.ontopia.topicmaps.query.impl.basic; import java.util.ArrayList; import java.util.Collection; import java.util.List; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.index.ClassInstanceIndexIF; import net.ontopia.topicmaps.query.core.InvalidQueryException; import net.ontopia.topicmaps.query.impl.utils.PredicateDrivenCostEstimator; import net.ontopia.topicmaps.query.impl.utils.Prefetcher; import net.ontopia.topicmaps.query.parser.Pair; import net.ontopia.topicmaps.query.parser.Variable; /** * INTERNAL: Implements association type predicates. */ public class DynamicAssociationPredicate extends AbstractDynamicPredicate { protected TopicMapIF topicmap; protected ClassInstanceIndexIF index; public DynamicAssociationPredicate(TopicMapIF topicmap, LocatorIF base, TopicIF type) { super(type, base); this.topicmap = topicmap; index = (ClassInstanceIndexIF) topicmap.getIndex("net.ontopia.topicmaps.core.index.ClassInstanceIndexIF"); } public String getSignature() { return "p+"; } public int getCost(boolean[] boundparams) { int open = 0; int closed = 0; for (int ix = 0; ix < boundparams.length; ix++) { if (!boundparams[ix]) open++; else closed++; } if (open == 0) return PredicateDrivenCostEstimator.FILTER_RESULT; else if (closed > 0) return PredicateDrivenCostEstimator.MEDIUM_RESULT - closed; else return PredicateDrivenCostEstimator.BIG_RESULT - closed; } public QueryMatches satisfy(QueryMatches matches, Object[] arguments) throws InvalidQueryException { // check whether to use a faster implementation int argix = -1; for (int i = 0; i < arguments.length; i++) { Pair pair = (Pair) arguments[i]; int colno = matches.getIndex(pair.getFirst()); if (matches.bound(colno)) { argix = i; break; } } if (argix != -1) return satisfyWhenBound(matches, arguments, argix); // initialize QueryMatches result = new QueryMatches(matches); AssociationRoleIF[] seed2 = new AssociationRoleIF[2]; // most assocs are binary ArgumentPair[] bound = getBoundArguments(matches, arguments, -1); ArgumentPair[] unbound = getUnboundArguments(matches, arguments); int colcount = matches.colcount; // time-saving shortcut Object[][] data = matches.data; // ditto // loop over associations AssociationIF[] assocs = index.getAssociations(type).toArray(new AssociationIF[0]); // prefetch roles Prefetcher.prefetch(topicmap, assocs, Prefetcher.AssociationIF, Prefetcher.AssociationIF_roles, false); int bound_length = bound.length; int unbound_length = unbound.length; boolean[] roleused = new boolean[10]; for (AssociationIF assoc : assocs) { Collection<AssociationRoleIF> rolecoll = assoc.getRoles(); AssociationRoleIF[] roles = rolecoll.toArray(seed2); int roles_length = rolecoll.size(); if (roles_length > roleused.length) roleused = new boolean[roles_length]; // loop over existing matches for (int row = 0; row <= matches.last; row++) { // blank out array of used roles for (int roleix = 0; roleix < roles_length; roleix++) roleused[roleix] = false; // check bound columns against association boolean ok = true; for (int colix = 0; colix < bound_length; colix++) { TopicIF roleType = bound[colix].roleType; int col = bound[colix].ix; // find corresponding role ok = false; for (int roleix = 0; roleix < roles_length; roleix++) { if (!roleused[roleix] && roleType.equals(roles[roleix].getType()) && data[row][col].equals(roles[roleix].getPlayer())) { ok = true; roleused[roleix] = true; break; } } if (!ok) // no matching role, so don't bother checking more columns break; } if (!ok) // match failed, so try next row continue; // produce all possible combinations of role bindings while (true) { boolean one_unused_role = false; // it's ok so long as *one* role was unused // produce match by binding unbound columns for (int colix = 0; colix < unbound_length; colix++) { TopicIF roleType = unbound[colix].roleType; // find corresponding role int role = -1; for (int roleix = 0; roleix < roles_length; roleix++) { if (roleType.equals(roles[roleix].getType())) { if (roles[roleix].getPlayer() == null) { roleused[roleix] = true; // don't touch this again continue; // keep looking } role = roleix; // this role is a candidate for use if (!roleused[roleix]) { one_unused_role = true; break; // this role was unused, so let's use it; // otherwise keep looking } } } if (role == -1) { one_unused_role = false; // this makes sure no match is produced break; // no role found, so give up } // ok, there is an association role matching this unbound column roleused[role] = true; unbound[colix].boundTo = roles[role].getPlayer(); } if (!one_unused_role) break; // no combos where one role unused // ok, the row/assoc combo is fine; now make a match for it if (result.last+1 == result.size) result.increaseCapacity(); result.last++; System.arraycopy(data[row], 0, result.data[result.last], 0, colcount); for (int colix = 0; colix < unbound_length; colix++) { // we may have had multiple arguments for the same unbound // column, so have to check whether they matched up Object value = result.data[result.last][unbound[colix].ix]; if ((value == null || value.equals(unbound[colix].boundTo)) && unbound[colix].boundTo != null) result.data[result.last][unbound[colix].ix] = unbound[colix].boundTo; else { // this match is bad. we need to retract it result.last--; // all cols reset when new matches made, anyway } } } // while(true) } // for each row in existing matches } // for each association // check if we have a symmetrical query, and if so, mirror the // symmetrical values mirrorIfSymmetrical(result, arguments); return result; } private String roleDebug(AssociationRoleIF role) { return "[" + role.getObjectId() + ", " + role.getPlayer() + "]"; } /** * INTERNAL: Faster version of satisfy for use when one variable has * already been bound, because it is much faster in that case. It is * faster because it does not need to do the full all associations x * all matches comparison. It is used instead of the naive one when * heuristics indicate that this is the best approach. * * @param matches The query matches passed in to us. * @param arguments The arguments passed to the predicate. * @param argix The argument to start from. */ private QueryMatches satisfyWhenBound(QueryMatches matches, Object[] arguments, int argix) throws InvalidQueryException { // initialize // boundcol: column in matches where start argument is bound int boundcol = matches.getIndex(((Pair) arguments[argix]).getFirst()); QueryMatches result = new QueryMatches(matches); AssociationRoleIF[] seed2 = new AssociationRoleIF[2]; // most assocs are binary ArgumentPair[] bound = getBoundArguments(matches, arguments, argix); ArgumentPair[] unbound = getUnboundArguments(matches, arguments); int colcount = matches.colcount; // time-saving shortcut Object[][] data = matches.data; // ditto int bound_length = bound.length; int unbound_length = unbound.length; TopicIF rtype = (TopicIF) ((Pair) arguments[argix]).getSecond(); // pre-allocating this to save time boolean[] roleused = new boolean[25]; Prefetcher.prefetchRolesByType(topicmap, matches, boundcol, rtype, type, Prefetcher_RBT_fields, Prefetcher_RBT_traverse); // // in the in-memory implementation the getRolesByType() call often consumes // // much of the time needed to run a query. we solve this by implementing a // // simple role cache. using an LRUMap to avoid making a cache that grows // // beyond all reasonable bounds. // java.util.Map rolecache = // new org.apache.commons.collections.map.LRUMap(100); // loop over existing matches for (int row = 0; row <= matches.last; row++) { // verify that we're looking at a topic if (!(data[row][boundcol] instanceof TopicIF)) continue; // this can't be a valid row // now, test if this row is really valid TopicIF topic = (TopicIF) data[row][boundcol]; // first, look for roles in assocs of the type we're supposed to have for (AssociationRoleIF arole : topic.getRolesByType(rtype, type)) { // ok, we've found the role; now let's see if the association // can produce a match // -------------------------------------------------------------- Collection<AssociationRoleIF> rolecoll = arole.getAssociation().getRoles(); AssociationRoleIF[] roles = rolecoll.toArray(seed2); int roles_length = rolecoll.size(); // check bound arguments against association boolean ok = true; for (int arg = 0; arg < bound_length; arg++) { TopicIF roleType = bound[arg].roleType; int col = bound[arg].ix; // find corresponding role int role = -1; for (int roleix = 0; roleix < roles_length; roleix++) { if (roleType.equals(roles[roleix].getType()) && data[row][col] != null && // bug #2001 data[row][col].equals(roles[roleix].getPlayer())) { role = roleix; break; } } if (role == -1) { // no matching role found ok = false; break; } } if (!ok) continue; // this assoc didn't match // produce match by binding unbound columns if (roles_length > roleused.length) roleused = new boolean[roles_length]; for (int roleix = 0; roleix < roles_length; roleix++) roleused[roleix] = // if this is the start role then that's already used topic.equals(roles[roleix].getPlayer()) && rtype.equals(roles[roleix].getType()); for (int arg = 0; arg < unbound_length; arg++) { TopicIF roleType = unbound[arg].roleType; // find corresponding role int role = -1; for (int roleix = 0; roleix < roles_length; roleix++) { if (roleType.equals(roles[roleix].getType()) && !roleused[roleix]) { role = roleix; break; } } if (role == -1) { ok = false; break; } // won't accept null players if (roles[role].getPlayer() == null) { ok = false; break; } // ok, there is an association role matching this unbound column unbound[arg].boundTo = roles[role].getPlayer(); roleused[role] = true; } if (ok) { // ok, the row/assoc combo is fine; now make a match for it if (result.last+1 == result.size) result.increaseCapacity(); result.last++; System.arraycopy(data[row], 0, result.data[result.last], 0, colcount); for (int arg = 0; arg < unbound_length && ok; arg++) { result.data[result.last][unbound[arg].ix] = unbound[arg].boundTo; unbound[arg].boundTo = null; } } // ---------------------------------------------------------------------- } } QueryTracer.trace(" results: " + result.last); return result; } // --- Internal methods private void mirrorIfSymmetrical(QueryMatches result, Object[] arguments) { int col1 = -1; int col2 = -1; for (int ix1 = 0; ix1 < arguments.length; ix1++) { Pair arg1 = (Pair) arguments[ix1]; if (!(arg1.getFirst() instanceof Variable)) continue; for (int ix2 = ix1+1; ix2 < arguments.length; ix2++) { Pair arg2 = (Pair) arguments[ix2]; if (!(arg2.getFirst() instanceof Variable)) continue; if (arg1.getSecond().equals(arg2.getSecond())) { col1 = result.getIndex((Variable) arg1.getFirst()); col2 = result.getIndex((Variable) arg2.getFirst()); break; // FIXME: should really produce all combinations and repeat op for // each combination } } } if (col1 == -1) return; // no symmetry, so nothing to do result.ensureCapacity((result.last+1) * 2); Object[][] data = result.data; int next = result.last + 1; int width = result.colcount; for (int ix = 0; ix <= result.last; ix++) { data[next] = new Object[width]; System.arraycopy(data[ix], 0, data[next], 0, width); data[next][col1] = data[ix][col2]; data[next][col2] = data[ix][col1]; next++; } result.last = next-1; } protected ArgumentPair[] getBoundArguments(QueryMatches matches, Object[] arguments, int boundarg) throws InvalidQueryException { int width = arguments.length; List<ArgumentPair> args = new ArrayList<ArgumentPair>(width); for (int ix = 0; ix < width; ix++) { if (ix == boundarg) continue; // yes, this is bound, but since we're starting from it // we can ignore it Object arg = arguments[ix]; if (!(arg instanceof Pair)) throw new InvalidQueryException("Invalid argument to association predicate (only pairs allowed)"); Pair pair = (Pair) arg; if (!(pair.getSecond() instanceof TopicIF)) throw new InvalidQueryException("Second half of association predicate pair argument must be a topic constant; found '" + pair + "'"); int colno = matches.getIndex(pair.getFirst()); if (matches.bound(colno)) { args.add(new ArgumentPair(colno, (TopicIF) pair.getSecond())); } } if (args.isEmpty()) return new ArgumentPair[0]; return args.toArray(new ArgumentPair[args.size()]); } protected ArgumentPair[] getUnboundArguments(QueryMatches matches, Object[] arguments) throws InvalidQueryException { int width = arguments.length; List<ArgumentPair> args = new ArrayList<ArgumentPair>(width); for (int ix = 0; ix < width; ix++) { Pair pair = (Pair) arguments[ix]; if (!(pair.getSecond() instanceof TopicIF)) throw new InvalidQueryException("Second half of association predicate pair argument must be a topic constant"); int colno = matches.getIndex(pair.getFirst()); if (matches.data[0][colno] == null) { args.add(new ArgumentPair(colno, (TopicIF) pair.getSecond())); } } if (args.isEmpty()) return new ArgumentPair[0]; return args.toArray(new ArgumentPair[args.size()]); } // --- Argument class protected class ArgumentPair { public int ix; public TopicIF roleType; public TopicIF boundTo; // used to store binding during evaluation public ArgumentPair(int ix, TopicIF roleType) { this.ix = ix; this.roleType = roleType; } @Override public String toString() { return "<AP$ArgPair " + ix + ":" + roleType + ">"; } } // -- Prefetcher constants private final static int[] Prefetcher_RBT_fields = new int[] { Prefetcher.AssociationRoleIF_association, Prefetcher.AssociationIF_roles, Prefetcher.AssociationRoleIF_player }; private final static boolean[] Prefetcher_RBT_traverse = new boolean[] { true, false, false }; // ISSUE: traverse R.player? }