/*
* #!
* 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.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.core.QueryResultIF;
import net.ontopia.topicmaps.query.parser.Parameter;
import net.ontopia.topicmaps.query.parser.Variable;
import net.ontopia.utils.CompactHashSet;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.utils.StringUtils;
/**
* INTERNAL: Object used to hold query results during computation.
*/
public class QueryMatches {
public static int initialSize = 100;
public int last;
public int size;
public Object[][] data;
// either a Variable or a TMObjectIF/String/Integer/Float constant, defining the
// contents of the column
public Object[] columnDefinitions;
public int colcount;
private QueryContext context;
/**
* INTERNAL: Creates a new matches object with the given column
* definitions.
*/
public QueryMatches(Collection columnDefs, QueryContext context) {
colcount = columnDefs.size();
data = new Object[initialSize][colcount];
size = initialSize;
last = -1;
columnDefinitions = columnDefs.toArray();
this.context = context;
}
/**
* INTERNAL: Creates a new (empty) matches object with the same
* column definitions as the QueryMatches object passed in the
* parameter.
*/
public QueryMatches(QueryMatches matches) {
colcount = matches.colcount;
data = new Object[initialSize][colcount];
size = initialSize;
last = -1;
columnDefinitions = matches.columnDefinitions;
context = matches.getQueryContext();
}
/**
* INTERNAL: Returns the index of the given variable in the table.
*/
public int getVariableIndex(String varname) {
for (int ix = 0; ix < colcount; ix++) {
if (columnDefinitions[ix] instanceof Variable &&
varname.equals(((Variable)columnDefinitions[ix]).getName()))
return ix;
}
return -1;
}
/**
* INTERNAL: Returns the index of the given constant in the table.
*/
public int getIndex(TMObjectIF constant) {
for (int ix = 0; ix < colcount; ix++)
if (constant.equals(columnDefinitions[ix]))
return ix;
return -1;
}
/**
* INTERNAL: Returns the index of the given variable in the table.
*/
public int getIndex(Variable var) {
for (int ix = 0; ix < colcount; ix++)
if (var.equals(columnDefinitions[ix]))
return ix;
return -1;
}
/**
* INTERNAL: Returns the index of the given string constant in the table.
*/
public int getIndex(String str) {
for (int ix = 0; ix < colcount; ix++)
if (str.equals(columnDefinitions[ix]))
return ix;
return -1;
}
/**
* INTERNAL: Returns the index of the given integer constant in the table.
*/
public int getIndex(Integer num) {
for (int ix = 0; ix < colcount; ix++)
if (num.equals(columnDefinitions[ix]))
return ix;
return -1;
}
/**
* INTERNAL: Returns the index of the given float constant in the table.
*/
public int getIndex(Float num) {
for (int ix = 0; ix < colcount; ix++)
if (num.equals(columnDefinitions[ix]))
return ix;
return -1;
}
/**
* INTERNAL: Returns the column index of the given object in the
* table.
*/
public int getIndex(Object argument) {
if (argument instanceof Variable)
return getIndex((Variable) argument);
else if (argument instanceof TMObjectIF)
return getIndex((TMObjectIF) argument);
else if (argument instanceof Parameter)
return getIndex(context.getParameterValue(((Parameter) argument).getName()));
else if (argument instanceof String)
return getIndex((String) argument);
else if (argument instanceof Integer)
return getIndex((Integer) argument);
else if (argument instanceof Float)
return getIndex((Float) argument);
else
throw new OntopiaRuntimeException("Argument of unknown type: " + argument);
}
/**
* INTERNAL: Returns definition of column.
*/
public Object getColumnDefinition(int ix) {
return columnDefinitions[ix];
}
/**
* INTERNAL: Used to increase the size of the table when full.
*/
public void increaseCapacity() {
ensureCapacity(size * 2);
}
/**
* INTERNAL: Ensures that the table has at least the given size.
*/
public void ensureCapacity(int requirement) {
while (size < requirement)
size *= 2;
Object[][] newdata = new Object[size][colcount];
System.arraycopy(data, 0, newdata, 0, last+1);
data = newdata;
}
/**
* INTERNAL: Empties the table.
*/
public void clear() {
last = -1;
}
/**
* INTERNAL: Returns the query context.
*/
public QueryContext getQueryContext() {
return context;
}
/**
* INTERNAL: Checks whether any of the columns are literal columns
* representing a literal in the query.
*/
public boolean hasLiteralColumns() {
for (int ix = 0; ix < colcount; ix++)
if (!(columnDefinitions[ix] instanceof Variable))
return true;
return false;
}
/**
* INTERNAL: Returns true if there are no matches.
*/
public boolean isEmpty() {
return last == -1;
}
/**
* INTERNAL: Inserts the constant values in the constant columns.
* Uses the information in the column definitions to do this.
*/
public void insertConstants() {
Object[] template = new Object[colcount];
for (int col = 0; col < colcount; col++) {
if (columnDefinitions[col] instanceof TMObjectIF ||
columnDefinitions[col] instanceof String ||
columnDefinitions[col] instanceof Integer ||
columnDefinitions[col] instanceof Float)
template[col] = columnDefinitions[col];
}
for (int ix = 0; ix <= last; ix++) {
for (int i = 0; i < template.length; i++) {
if (template[i] != null)
data[ix][i] = template[i];
}
}
}
// ===== QUERY INTROSPECTION ===============================================
/**
* INTERNAL: Checks whether the variable represented by the indexed
* column is bound.
*/
public boolean bound(int colix) {
return data[0][colix] != null;
}
// ===== QUERY MATCH ALGEBRA ===============================================
/**
* INTERNAL: Adds all the matches in the given table to this table.
* Note that the two tables must have the same layout.
*/
public void add(QueryMatches extra) {
if (extra.last == -1)
return;
ensureCapacity(last + extra.last + 2);
System.arraycopy(extra.data, 0, data, last+1, extra.last+1);
last += extra.last + 1;
}
/**
* EXPERIMENTAL: Adds input array to this table.
*/
public void add(Object[][] newdata, int length) {
if (length < 1) return;
// Add to query matches
ensureCapacity(last + length + 2);
System.arraycopy(newdata, 0, data, last+1, length);
last += length;
}
/**
* EXPERIMENTAL: Adds QueryResultIF matches to this table.
*/
public void add(QueryResultIF extra) {
int spec_width = columnDefinitions.length;
int[] spec = new int[spec_width];
Arrays.fill(spec, -1);
int extra_width = extra.getWidth();
for (int i=0; i < extra_width; i++) {
// skip column if not in query matches
int index = getVariableIndex(extra.getColumnName(i));
if (index > -1)
spec[index] = i;
}
int batch_size = 50; // number of rows
int rowidx = 0;
// Add to query matches
ensureCapacity(last + batch_size + 2);
Object[] frow = (last == 0 ? data[0] : data[last-1]); // feeding row
Object[] crow = (last == 0 ? data[0] : data[last]); // current row
while (extra.next()) {
// Transform columns and add values.
for (int i=0; i < spec_width; i++) {
int idx = spec[i];
// If index specified read value from extra, otherwise use feeding row.
if (idx == -1)
crow[i] = frow[i];
else
crow[i] = extra.getValue(idx);
}
if (rowidx == batch_size - 1) {
// Prepare for new batch
ensureCapacity(last + batch_size + 2);
rowidx = 0;
} else {
rowidx++;
}
// Read next row
last++;
crow = data[last];
}
last--;
}
/**
* INTERNAL: Removes the rows which have matching counterparts in
* the argument.
*/
protected void remove(QueryMatches matches) {
int cols = columnDefinitions.length;
int next = 0;
if (last * matches.last < 1000000) {
// naive approach
for (int resrow = 0; resrow <= last; resrow++) {
Object[] row = data[resrow];
// if we can find a matching row in matches, skip this one
// if not, keep it
boolean ok = true;
for (int mrow = 0; ok && mrow <= matches.last; mrow++) {
boolean eq = true;
for (int col = 0; eq && col < cols; col++)
if (row[col] != null)
eq = row[col].equals(matches.data[mrow][col]);
ok = !eq; // we're ok if it didn't match
}
if (ok) // the row is ok, so keep it
data[next++] = row;
}
last = next-1;
} else {
// too many results for a naive scan. instead, build a Set from
// the other result set and use it to see which rows in this one
// also exist in the other.
// first step: we only compare bound columns. find out which
// columns are bound. we will always have fewer columns bound
// than the not-ed result set, so use our own count.
int count = 0;
for (int ix = 0; ix < colcount; ix++)
if (bound(ix))
count++;
int compare[] = new int[count];
count = 0;
for (int ix = 0; ix < colcount; ix++)
if (bound(ix))
compare[count++] = ix;
// second step: build the set
Set set = new CompactHashSet(last + 1);
ArrayWrapper wrapper = new SelectiveArrayWrapper(compare);
for (int row = 0; row <= matches.last; row++) {
wrapper.setArray(matches.data[row]); // reuse previous wrapper
if (!set.contains(wrapper)) {
set.add(wrapper);
// can't reuse, so make new wrapper
wrapper = new SelectiveArrayWrapper(compare);
}
}
// third step: filter the rows
wrapper = new SelectiveArrayWrapper(compare);
for (int row = 0; row <= last; row++) {
wrapper.setArray(data[row]);
if (!set.contains(wrapper))
data[next++] = data[row]; // keep it
}
last = next - 1;
}
}
/**
* INTERNAL: Adds all rows which do not already exist. Matching
* ignores nulls in the rows being added.
*/
protected void addNonRedundant(QueryMatches matches) {
int cols = columnDefinitions.length;
int orgLast = last;
// for each input row
for (int mrow = 0; mrow <= matches.last; mrow++) {
// look for an existing match
boolean found = false;
for (int resrow = 0; !found && resrow <= orgLast; resrow++) {
Object[] row = data[resrow];
boolean eq = true;
for (int col = 0; eq && col < cols; col++)
if (matches.data[mrow][col] != null)
eq = matches.data[mrow][col].equals(row[col]);
found = eq;
}
if (!found) { // the row is ok, so add it
if (last+1 == size)
increaseCapacity();
last++;
data[last] = matches.data[mrow];
}
}
}
// ===== QUERY MATCH TRANSLATION ===========================================
// FIXME: in method below int* and ext* are wrong way around
/**
* INTERNAL: Computes the translation specification array, which
* gives the connection between this and the other match table.
*
* @param intarguments Actual received parameters in rule invocation.
* @param extarguments Declared parameters in rule declaration.
* @return an array of type int[][] that looks like [intspec,
* extspec], where intspec is the specification for this match and
* extspec is the specification for the other match.
*/
public int[][] getTranslationSpec(Object[] intarguments,
QueryMatches extmatches,
Object[] extarguments)
throws InvalidQueryException {
int width = intarguments.length;
int[] intcols = new int[width];
int[] extcols = new int[width];
for (int ix = 0; ix < width; ix++) {
intcols[ix] = getIndex(intarguments[ix]);
extcols[ix] = extmatches.getIndex(extarguments[ix]);
if (extcols[ix] == -1)
throw new InvalidQueryException("Unused argument " +
extarguments[ix]);
}
int[][] spec = new int[2][];
spec[0] = intcols;
spec[1] = extcols;
return spec;
}
/**
* INTERNAL: Translates matches in this table into corresponding
* matches in the other.
*/
public void translate(int[] fromCols,
QueryMatches toQM, int[] toCols) {
Object[][] from = data;
Object[][] to = toQM.data;
int fromlast = last;
int cols = fromCols.length;
for (int fromrow = 0; fromrow <= fromlast; fromrow++) {
if (toQM.last+1 == toQM.size) {
toQM.increaseCapacity();
to = toQM.data;
}
toQM.last++;
for (int col = 0; col < cols; col++)
to[toQM.last][toCols[col]] = from[fromrow][fromCols[col]];
}
}
/**
* INTERNAL: Merges this match table (from inside a rule) with
* another match table (from the calling context), producing a new
* set of matches (corresponding to the result of the rule, as
* viewed from the outside).
* @param intspec Mapping from general column no (?) to column no in
* this QM object.
* @param extspec Mapping from general column no (?) to column no in
* extmatches.
* @param equalpairs See RulePredicate.getEqualPairs() for explanation.
* Numbers are argument numbers.
*/
public QueryMatches merge(int[] intspec, QueryMatches extmatches,
int[] extspec, int[] equalpairs) {
int intspec_length = intspec.length;
int extspec_length = extspec.length;
// find out what columns to compare by creating intcols+extcols, which
// is really a list of pairs of columns to compare.
int compcount = 0;
for (int ix = 0; ix < intspec_length; ix++)
if (data[0][intspec[ix]] != null &&
extmatches.data[0][extspec[ix]] != null)
compcount++;
int[] intcols = new int[compcount];
int[] extcols = new int[compcount];
compcount = 0;
for (int ix = 0; ix < intspec_length; ix++) {
if (data[0][intspec[ix]] != null &&
extmatches.data[0][extspec[ix]] != null) {
intcols[compcount] = intspec[ix];
extcols[compcount] = extspec[ix];
compcount++;
}
}
// do the merging
QueryMatches result = new QueryMatches(this);
int width = columnDefinitions.length;
for (int introw = 0; introw <= last; introw++) {
if (introw > 0) {
int col = 0;
for (; col < width &&
((data[introw][col] == null && data[introw-1][col] == null) ||
(data[introw][col] != null && data[introw][col].equals(data[introw-1][col]))); col++)
;
if (col == width)
continue;
}
externalrow:
for (int extrow = 0; extrow <= extmatches.last; extrow++) {
// check that internal and external values match
for (int col = 0; col < compcount; col++) {
if (data[introw][intcols[col]] == null ||
!data[introw][intcols[col]].equals(extmatches.data[extrow][extcols[col]]))
continue externalrow;
}
// check equal pairs
for (int ix = 0; ix+1 < equalpairs.length; ix += 2)
if (extmatches.data[extrow][extspec[equalpairs[ix]]] != null &&
!extmatches.data[extrow][extspec[equalpairs[ix]]].
equals(extmatches.data[extrow][extspec[equalpairs[ix+1]]]))
continue externalrow;
// generate output match
if (result.last+1 == result.size)
result.increaseCapacity();
result.last++;
// copy internal match row
System.arraycopy(data[introw], 0,
result.data[result.last], 0,
width);
// fill in results from internal match
for (int col = 0; col < extspec_length; col++)
result.data[result.last][intspec[col]] = extmatches.data[extrow][extspec[col]];
}
}
return result;
}
// ===== REMOVING DUPLICATES ===========================================
public QueryMatches removeDuplicates() {
QueryMatches result = new QueryMatches(this);
Set alreadyAdded = new CompactHashSet();
Object[][] mdata = data;
Object[][] rdata = result.data;
ArrayWrapper wrapper = new ArrayWrapper(); // for instance reuse...
int colcount = result.colcount;
for (int row = 0; row <= last; row++) {
wrapper.setArray(mdata[row]); // reuse previous wrapper
if (!alreadyAdded.contains(wrapper)) {
alreadyAdded.add(wrapper);
wrapper = new ArrayWrapper(); // can't reuse, so make new wrapper
if (result.last+1 == result.size) {
result.increaseCapacity();
rdata = result.data;
}
result.last++;
rdata[result.last] = mdata[row];
}
}
return result;
}
/// FIXME: Copied from QueryProcessor
// We have to use this to get meaningful implementations of
// hashCode() and equals() for arrays. Arrays have these methods,
// but they are, stupidly, the same as for Object.
class ArrayWrapper {
protected Object[] row;
protected int hashCode;
public void setArray(Object[] row) {
this.row = row;
hashCode = 0;
for (int ix = 0; ix < row.length; ix++)
if (row[ix] != null)
hashCode = (hashCode + row[ix].hashCode()) & 0x7FFFFFFF;
}
public int hashCode() {
return hashCode;
}
public boolean equals(Object o) {
// this class is only used here, so we are making some simplifying
// assumptions:
// - o is not null
// - o is an ArrayWrapper
// - o contains an Object[] array of the same length as row
Object[] orow = ((ArrayWrapper) o).row;
for (int ix = 0; ix < orow.length; ix++)
if (orow[ix] != null && !orow[ix].equals(row[ix]))
return false;
return true;
}
}
// doesn't compare all columns; only indicated ones
class SelectiveArrayWrapper extends ArrayWrapper {
private int comparedColumns[];
public SelectiveArrayWrapper(int comparedColumns[]) {
this.comparedColumns = comparedColumns;
}
public void setArray(Object[] row) {
this.row = row;
hashCode = 0;
for (int i = 0; i < comparedColumns.length; i++) {
int ix = comparedColumns[i];
if (row[ix] != null)
hashCode = (hashCode + row[ix].hashCode()) & 0x7FFFFFFF;
}
}
public boolean equals(Object o) {
// this class is only used here, so we are making some simplifying
// assumptions:
// - o is not null
// - o is an ArrayWrapper
// - o contains an Object[] array of the same length as row
Object[] orow = ((ArrayWrapper) o).row;
for (int i = 0; i < comparedColumns.length; i++) {
int ix = comparedColumns[i];
if (orow[ix] != null && !orow[ix].equals(row[ix]))
return false;
}
return true;
}
}
// ===== DEBUG METHODS =====================================================
public String dump() {
StringBuilder sb = new StringBuilder();
sb.append("------------------------------------------------------------------------------\n");
StringUtils.join(columnDefinitions, " | ", sb);
sb.append("\n");
sb.append("------------------------------------------------------------------------------\n");
for (int r=0; r <= last; r++) {
sb.append("[");
for (int c = 0; c < columnDefinitions.length-1; c++) {
sb.append(toString(data[r][c]));
sb.append(", ");
}
sb.append(toString(data[r][columnDefinitions.length-1]));
sb.append("]\n");
}
sb.append((last+1) + " rows");
return sb.toString();
}
private String toString(Object obj) {
if (obj instanceof TopicIF) {
Iterator it = ((TopicIF) obj).getItemIdentifiers().iterator();
while (it.hasNext()) {
LocatorIF loc = (LocatorIF) it.next();
String addr = loc.getAddress();
int ix = addr.indexOf('#');
//! int ix = addr.lastIndexOf('/'); // include file name
if (ix != -1)
return addr.substring(ix + 1);
}
return ((TopicIF) obj).getObjectId();
} else if (obj == null)
return "null";
else
return obj.toString();
}
}