/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*
* $Id: QueryEngine.java 511426 2007-02-25 03:25:02Z vgritsenko $
*/
package org.apache.xindice.core.query;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xindice.core.Collection;
import org.apache.xindice.core.DBException;
import org.apache.xindice.core.Database;
import org.apache.xindice.core.data.Key;
import org.apache.xindice.core.data.NodeSet;
import org.apache.xindice.core.indexer.IndexMatch;
import org.apache.xindice.util.Configuration;
import org.apache.xindice.util.ConfigurationCallback;
import org.apache.xindice.util.SimpleConfigurable;
import org.apache.xindice.util.XindiceException;
import org.apache.xindice.xml.NamespaceMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* QueryEngine is the Xindice Query Engine. Its purpose is to orchestrate
* query operations against the Xindice repository. The QueryEngine
* basically just manages a set of QueryResolvers that actually perform
* the work.
*
* @version $Revision: 511426 $, $Date: 2007-02-24 22:25:02 -0500 (Sat, 24 Feb 2007) $
*/
public class QueryEngine extends SimpleConfigurable {
private static final Log log = LogFactory.getLog(QueryEngine.class);
private static final String[] EmptyStrings = new String[0];
private static final Key[] EmptyKeys = new Key[0];
private static final String RESOLVER = "resolver";
private static final String CLASS = "class";
private final Database db;
private final Map resolvers;
public QueryEngine(Database db) {
this.db = db;
this.resolvers = new HashMap();
}
public void setConfig(Configuration config) throws XindiceException {
super.setConfig(config);
config.processChildren(RESOLVER, new ConfigurationCallback() {
public void process(Configuration cfg) {
final String className = cfg.getAttribute(CLASS);
try {
QueryResolver res = (QueryResolver) Class.forName(className).newInstance();
res.setConfig(cfg);
res.setQueryEngine(QueryEngine.this);
resolvers.put(res.getQueryStyle(), res);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Unable to load query resolver: " + className + ". " +
"This query resolver will not be available.", e);
}
}
}
});
}
public Database getDatabase() {
return db;
}
/**
* listStyles returns a list of styles supported by the
* QueryEngine (ex: XPath, XUpdate)
*
* @return The supported styles
*/
public String[] listStyles() {
return (String[]) resolvers.keySet().toArray(EmptyStrings);
}
private QueryResolver getResolver(String style) throws QueryException {
QueryResolver res = (QueryResolver) resolvers.get(style);
if (res == null) {
throw new StyleNotFoundException("No Resolver available for '" + style + "' queries");
}
return res;
}
/**
* query performs the specified query and returns a NodeSet with
* any possible results from that query. The query is performed
* in the context of a Collection.
*
* @param col The Collection context
* @param style The query style (XPath, Fulltext, etc...)
* @param query The Query
* @param nsMap The namespace Map (if any)
* @param keys The initial Key set to use (if any)
* @return A NodeSet with the query results
*/
public NodeSet query(Collection col, String style, String query, NamespaceMap nsMap, Key[] keys) throws DBException, QueryException {
QueryResolver res = getResolver(style);
return res.query(col, query, nsMap, keys);
}
/**
* compileQuery compiles a Query against the specified Collection
* context and returns the compiled Query. This DOES NOT actually
* run the query, merely just parses it and primes any possible
* Indexers that the query might need.
*
* @param col The Collection context
* @param style The query style (XPath, Fulltext, etc...)
* @param query The Query
* @param nsMap The namespace Map (if any)
* @param keys The initial Key set to use (if any)
* @return The compiled Query
*/
public Query compileQuery(Collection col, String style, String query, NamespaceMap nsMap, Key[] keys) throws DBException, QueryException {
QueryResolver res = getResolver(style);
return res.compileQuery(col, query, nsMap, keys);
}
// Utility methods
/**
* getUniqueKeys takes a set of IndexMatch objects and extracts
* all of its unique Keys in sorted order.
*
* @param matches The Match Set
* @return The unique Keys in order
*/
public static Key[] getUniqueKeys(IndexMatch[] matches) {
SortedSet set = new TreeSet();
for (int i = 0; i < matches.length; i++) {
set.add(matches[i].getKey());
}
return (Key[]) set.toArray(EmptyKeys);
}
/**
* andKeySets takes several sets of unique Keys and returns the
* ANDed set (elements that exist in all sets). The first dimension
* of the array holds the individual sets, the second holds the
* actual Keys.
*
* @param keySets 2-dimensional set of Keys
* @return The ANDed set of Keys in order
*/
public static Key[] andKeySets(Key[][] keySets) {
int[] ptrs;
if (keySets.length == 0) {
return EmptyKeys;
} else if (keySets.length == 1) {
return keySets[0];
} else {
ptrs = new int[keySets.length];
for (int i = 0; i < keySets.length; i++) {
if (keySets[i].length == 0) {
return EmptyKeys;
} else {
ptrs[i] = 0;
}
}
}
SortedSet set = new TreeSet();
boolean done = false;
List highs = new ArrayList();
Key highest = null;
while (!done) {
boolean eq = true;
for (int i = 0; i < ptrs.length; i++) {
Key comp = keySets[i][ptrs[i]];
if (highest == null) {
highest = comp;
highs.add(new Integer(i));
} else {
int c = highest.compareTo(comp);
if (c != 0) {
eq = false;
}
if (c < 0) {
highest = comp;
highs.clear();
highs.add(new Integer(i));
} else if (c == 0) {
highs.add(new Integer(i));
}
}
}
if (eq) {
set.add(highest);
highs.clear();
highest = null;
}
for (int i = 0; i < ptrs.length; i++) {
if (!highs.contains(new Integer(i))) {
ptrs[i]++;
}
if (ptrs[i] >= keySets[i].length) {
done = true;
}
}
if (!eq) {
highs.clear();
}
}
return (Key[]) set.toArray(EmptyKeys);
}
/**
* orKeySets takes several sets of unique Keys and returns the
* ORed set (all unique elements). The first dimension of the
* array holds the individual sets, the second holds the actual
* Keys.
*
* @param keySets 2-dimensional set of Keys
* @return The ORed set of Keys in order
*/
public static Key[] orKeySets(Key[][] keySets) {
if (keySets.length == 0) {
return EmptyKeys;
} else if (keySets.length == 1) {
return keySets[0];
} else if (keySets.length == 2) {
// Optimization since most ORs will be 2 sets only
if (keySets[1].length == 0) {
return keySets[0];
} else if (keySets[0].length == 0) {
return keySets[1];
}
}
SortedSet set = new TreeSet();
for (int i = 0; i < keySets.length; i++) {
for (int j = 0; j < keySets[i].length; j++) {
set.add(keySets[i][j]);
}
}
return (Key[]) set.toArray(EmptyKeys);
}
/**
* normalizeString normalizes the specific String by stripping
* all leading, trailing, and continuous runs of white space.
*
* @param value The value to normalize
* @return The result
*/
public static String normalizeString(String value) {
char[] c = value.toCharArray();
char[] n = new char[c.length];
boolean white = true;
int pos = 0;
for (int i = 0; i < c.length; i++) {
if (" \t\n\r".indexOf(c[i]) != -1) {
if (!white) {
n[pos++] = ' ';
white = true;
}
} else {
n[pos++] = c[i];
white = false;
}
}
if (white && pos > 0) {
pos--;
}
return new String(n, 0, pos);
}
/**
* expandEntities expands the String's pre-defined XML entities
* (<, >, etc...) into their actual character representations.
*
* @param value The value to expand entities for
* @return The expanded String
*/
public static String expandEntities(String value) {
int idx = value.indexOf('&');
if (idx == -1) {
return value;
}
StringBuffer sb = new StringBuffer(value.length());
int pos = 0;
while (pos < value.length()) {
if (idx != -1) {
if (idx > pos) {
sb.append(value.substring(pos, idx));
}
int end = value.indexOf(';', idx) + 1;
if (end == 0) {
// Some sort of error
return value;
}
String token = value.substring(idx + 1, end - 1);
if (token.equals("apos")) {
sb.append("'");
} else if (token.equals("quot")) {
sb.append("\"");
} else if (token.equals("amp")) {
sb.append("&");
} else if (token.equals("lt")) {
sb.append("<");
} else if (token.equals("gt")) {
sb.append(">");
} else {
// Some sort of error
return value;
}
pos = end;
idx = value.indexOf('&', pos);
} else {
sb.append(value.substring(pos));
break;
}
}
return sb.toString();
}
}