net.ontopia.topicmaps.query.impl.basic.DynamicAssociationPredicate.java Source code

Java tutorial

Introduction

Here is the source code for net.ontopia.topicmaps.query.impl.basic.DynamicAssociationPredicate.java

Source

/*
 * #!
 * 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?

}