You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by ju...@apache.org on 2010/10/18 12:16:44 UTC
svn commit: r1023728 - in
/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query:
QueryEngine.java SimpleRow.java ValueComparator.java
Author: jukka
Date: Mon Oct 18 10:16:44 2010
New Revision: 1023728
URL: http://svn.apache.org/viewvc?rev=1023728&view=rev
Log:
JCR-2715: Improved join query performance
First working join evaluation. Work in progress...
Modified:
jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryEngine.java
jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SimpleRow.java
jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/ValueComparator.java
Modified: jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryEngine.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryEngine.java?rev=1023728&r1=1023727&r2=1023728&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryEngine.java (original)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryEngine.java Mon Oct 18 10:16:44 2010
@@ -17,12 +17,14 @@
package org.apache.jackrabbit.commons.query;
import static java.util.Locale.ENGLISH;
+import static javax.jcr.PropertyType.NAME;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_ORDER_DESCENDING;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -33,7 +35,6 @@ import java.util.Set;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
-import javax.jcr.PropertyType;
import javax.jcr.RangeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
@@ -72,6 +73,7 @@ import javax.jcr.query.qom.Or;
import javax.jcr.query.qom.Ordering;
import javax.jcr.query.qom.PropertyExistence;
import javax.jcr.query.qom.PropertyValue;
+import javax.jcr.query.qom.QueryObjectModelConstants;
import javax.jcr.query.qom.QueryObjectModelFactory;
import javax.jcr.query.qom.SameNode;
import javax.jcr.query.qom.SameNodeJoinCondition;
@@ -113,6 +115,10 @@ public class QueryEngine {
this.variables = variables;
}
+ public QueryEngine(Session session) throws RepositoryException {
+ this(session, new HashMap<String, Value>());
+ }
+
public QueryResult execute(
Column[] columns, Source source,
Constraint constraint, Ordering[] orderings)
@@ -134,22 +140,83 @@ public class QueryEngine {
Constraint constraint, Ordering[] orderings)
throws RepositoryException {
Source left = join.getLeft();
- Set<String> leftSelectors = getSelectorNames(left).keySet();
+ Map<String, NodeType> leftSelectors = getSelectorNames(left);
Source right = join.getRight();
- Set<String> rightSelectors = getSelectorNames(right).keySet();
+ Map<String, NodeType> rightSelectors = getSelectorNames(right);
+
+ Constraint leftConstraint =
+ mapConstraintToSelectors(constraint, leftSelectors.keySet());
+ System.out.println("FROM " + left + " WHERE " + leftConstraint);
+ QueryResult leftResult = execute(null, left, leftConstraint, null);
List<Row> leftRows = new ArrayList<Row>();
- QueryResult leftResult =
- execute(null, left, mapConstraintToSelectors(constraint, leftSelectors), null);
for (Row row : JcrUtils.getRows(leftResult)) {
+ System.out.println(row);
leftRows.add(row);
}
- QueryResult rightResult =
- execute(null, right, mapConstraintToSelectors(constraint, rightSelectors), null);
+ Constraint joinConstraint = getJoinConstraint(
+ join.getJoinCondition(), leftSelectors.keySet(), leftRows);
+ Constraint rightConstraint = Constraints.and(
+ qomFactory, joinConstraint,
+ mapConstraintToSelectors(constraint, rightSelectors.keySet()));
+ System.out.println("FROM " + right + " WHERE " + rightConstraint);
+ QueryResult rightResult = execute(null, right, rightConstraint, null);
+
+ List<Row> rightRows = new ArrayList<Row>();
+ for (Row row : JcrUtils.getRows(rightResult)) {
+ System.out.println(row);
+ rightRows.add(row);
+ }
+
+ Map<String, NodeType> selectors = new LinkedHashMap<String, NodeType>();
+ selectors.putAll(leftSelectors);
+ selectors.putAll(rightSelectors);
+ String[] selectorNames =
+ selectors.keySet().toArray(new String[selectors.size()]);
+
+ Map<String, PropertyValue> columnMap = getColumnMap(columns, selectors);
+ String[] columnNames =
+ columnMap.keySet().toArray(new String[columnMap.size()]);
+ PropertyValue[] operands =
+ columnMap.values().toArray(new PropertyValue[columnMap.size()]);
- return null;
+ double[] scores = new double[selectorNames.length];
+ for (int i = 0; i < scores.length; i++) {
+ scores[i] = 1.0;
+ }
+
+ Predicate predicate = Predicates.and(
+ getPredicate(join.getJoinCondition()),
+ getPredicate(constraint));
+
+ List<Row> joinRows = new ArrayList<Row>();
+ for (Row leftRow : leftRows) {
+ for (Row rightRow : rightRows) {
+ Node[] nodes = new Node[selectorNames.length];
+ for (int i = 0; i < selectorNames.length; i++) {
+ String selector = selectorNames[i];
+ if (leftSelectors.containsKey(selector)) {
+ nodes[i] = leftRow.getNode(selector);
+ } else {
+ nodes[i] = rightRow.getNode(selector);
+ }
+ }
+ Value[] values = new Value[operands.length];
+ Row row = new SimpleRow(
+ columnNames, values, selectorNames, nodes, scores);
+ for (int i = 0; i < operands.length; i++) {
+ values[i] = combine(getPropertyValues(operands[i], row));
+ }
+ if (predicate.evaluate(row)) {
+ joinRows.add(row);
+ }
+ }
+ }
+
+ return new SimpleQueryResult(
+ columnNames, selectorNames, new RowIteratorAdapter(joinRows));
}
/**
@@ -268,26 +335,57 @@ public class QueryEngine {
}
}
- protected Constraint narrow(
- Constraint constraint, Set<String> selectors,
- JoinCondition condition, List<Row> rows)
+ private Predicate getPredicate(JoinCondition condition)
throws RepositoryException {
- Constraint joinConstraint = Constraints.TRUE;
if (condition instanceof EquiJoinCondition) {
- joinConstraint = getJoinConstraint(
+ return getEquiJoinPredicate((EquiJoinCondition) condition);
+ } else if (condition instanceof SameNodeJoinCondition) {
+ return Predicate.TRUE; // FIXME
+ } else if (condition instanceof ChildNodeJoinCondition) {
+ return Predicate.TRUE; // FIXME
+ } else if (condition instanceof DescendantNodeJoinCondition) {
+ return Predicate.TRUE; // FIXME
+ } else {
+ return Predicate.TRUE; // FIXME
+ }
+ }
+
+ private Predicate getEquiJoinPredicate(final EquiJoinCondition condition)
+ throws RepositoryException {
+ final Operand operand1 = qomFactory.propertyValue(
+ condition.getSelector1Name(),
+ condition.getProperty1Name());
+ final Operand operand2 = qomFactory.propertyValue(
+ condition.getSelector2Name(),
+ condition.getProperty2Name());
+ return new RowPredicate() {
+ @Override
+ protected boolean evaluate(Row row) throws RepositoryException {
+ return new ValueComparator().evaluate(
+ QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO,
+ getValues(operand1, row), getValues(operand2, row));
+ }
+ };
+ }
+
+ protected Constraint getJoinConstraint(
+ JoinCondition condition, Set<String> selectors, List<Row> rows)
+ throws RepositoryException {
+ if (condition instanceof EquiJoinCondition) {
+ return getJoinConstraint(
(EquiJoinCondition) condition, rows, selectors);
} else if (condition instanceof SameNodeJoinCondition) {
- joinConstraint = getJoinConstraint(
+ return getJoinConstraint(
(SameNodeJoinCondition) condition, rows, selectors);
} else if (condition instanceof ChildNodeJoinCondition) {
- joinConstraint = getJoinConstraint(
+ return getJoinConstraint(
(ChildNodeJoinCondition) condition, rows, selectors);
} else if (condition instanceof DescendantNodeJoinCondition) {
- joinConstraint = getJoinConstraint(
+ return getJoinConstraint(
(DescendantNodeJoinCondition) condition, rows, selectors);
+ } else {
+ return Constraints.TRUE;
}
- return Constraints.and(
- qomFactory, joinConstraint, mapConstraintToSelectors(constraint, selectors));
}
/**
@@ -322,7 +420,9 @@ public class QueryEngine {
Set<Value> values = new HashSet<Value>();
PropertyValue left = qomFactory.propertyValue(selector2, property2);
for (Row row : leftRows) {
- values.add(getValue(left, row));
+ for (Value value : getPropertyValues(left, row)) {
+ values.add(value);
+ }
}
// Convert each distinct value into a comparison constraint
@@ -449,8 +549,8 @@ public class QueryEngine {
columnNames, values, selectorNames,
new Node[] { (Node) super.next() }, scores);
for (int i = 0; i < values.length; i++) {
- values[i] =
- getValue(columnMap.get(columnNames[i]), row);
+ values[i] = combine(getPropertyValues(
+ columnMap.get(columnNames[i]), row));
}
return row;
} catch (RepositoryException e) {
@@ -466,6 +566,21 @@ public class QueryEngine {
return sort(result, orderings);
}
+ private Value combine(Value[] values) throws RepositoryException {
+ if (values.length == 1) {
+ return values[0];
+ } else {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) {
+ builder.append('\n');
+ }
+ builder.append(values[i].getString());
+ }
+ return valueFactory.createValue(builder.toString());
+ }
+ }
+
private Predicate getPredicate(Selector selector, Constraint constraint)
throws RepositoryException {
final String name = selector.getNodeTypeName();
@@ -529,8 +644,9 @@ public class QueryEngine {
throws RepositoryException {
if (source instanceof Selector) {
Selector selector = (Selector) source;
- String name = selector.getSelectorName();
- return Collections.singletonMap(name, ntManager.getNodeType(name));
+ return Collections.singletonMap(
+ selector.getSelectorName(),
+ ntManager.getNodeType(selector.getNodeTypeName()));
} else if (source instanceof Join) {
Join join = (Join) source;
Map<String, NodeType> map = new LinkedHashMap<String, NodeType>();
@@ -567,16 +683,19 @@ public class QueryEngine {
Collections.sort(rows, new Comparator<Row>() {
public int compare(Row a, Row b) {
try {
+ ValueComparator comparator = new ValueComparator();
for (Ordering ordering : orderings) {
Operand operand = ordering.getOperand();
- Value va = getValue(operand, a);
- Value vb = getValue(operand, b);
- int order = new ValueComparator().compare(va, vb);
- if (JCR_ORDER_DESCENDING.equals(ordering.getOrder())) {
- order = -order;
- }
- if (order != 0) {
- return order;
+ Value[] va = getValues(operand, a);
+ Value[] vb = getValues(operand, b);
+ if (va.length == 1 && vb.length == 1) {
+ int order = comparator.compare(va[0], vb[0]);
+ if (JCR_ORDER_DESCENDING.equals(ordering.getOrder())) {
+ order = -order;
+ }
+ if (order != 0) {
+ return order;
+ }
}
}
return 0;
@@ -632,8 +751,8 @@ public class QueryEngine {
throws RepositoryException {
return new ValueComparator().evaluate(
c.getOperator(),
- getValue(c.getOperand1(), row),
- getValue(c.getOperand2(), row));
+ getValues(c.getOperand1(), row),
+ getValues(c.getOperand2(), row));
}
};
} else if (constraint instanceof SameNode) {
@@ -694,26 +813,27 @@ public class QueryEngine {
* @return value of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- public Value getValue(Operand operand, Row row)
+ public Value[] getValues(Operand operand, Row row)
throws RepositoryException {
if (operand instanceof BindVariableValue) {
- return getValue((BindVariableValue) operand, row);
+ return getBindVariableValues((BindVariableValue) operand);
} else if (operand instanceof FullTextSearchScore) {
- return getValue((FullTextSearchScore) operand, row);
+ return getFullTextSearchScoreValues(
+ (FullTextSearchScore) operand, row);
} else if (operand instanceof Length) {
- return getValue((Length) operand, row);
+ return getLengthValues((Length) operand, row);
} else if (operand instanceof Literal) {
- return getValue((Literal) operand, row);
+ return getLiteralValues((Literal) operand);
} else if (operand instanceof LowerCase) {
- return getValue((LowerCase) operand, row);
+ return getLowerCaseValues((LowerCase) operand, row);
} else if (operand instanceof NodeLocalName) {
- return getValue((NodeLocalName) operand, row);
+ return getNodeLocalNameValues((NodeLocalName) operand, row);
} else if (operand instanceof NodeName) {
- return getValue((NodeName) operand, row);
+ return getNodeNameValues((NodeName) operand, row);
} else if (operand instanceof PropertyValue) {
- return getValue((PropertyValue) operand, row);
+ return getPropertyValues((PropertyValue) operand, row);
} else if (operand instanceof UpperCase) {
- return getValue((UpperCase) operand, row);
+ return getUpperCaseValues((UpperCase) operand, row);
} else {
throw new UnsupportedRepositoryOperationException(
"Unknown operand type: " + operand);
@@ -722,144 +842,163 @@ public class QueryEngine {
/**
* Returns the value of the given variable value operand at the given row.
- * Subclasses can override this method to customise the evaluation.
*
* @param operand variable value operand
- * @param row row
* @return value of the operand at the given row
- * @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(BindVariableValue operand, Row row)
- throws RepositoryException {
- String name = operand.getBindVariableName();
- Value value = variables.get(name);
+ private Value[] getBindVariableValues(BindVariableValue operand) {
+ Value value = variables.get(operand.getBindVariableName());
if (value != null) {
- return value;
+ return new Value[] { value };
} else {
- throw new RepositoryException("Unknown bind variable: $" + name);
+ return new Value[0];
}
}
/**
* Returns the value of the given search score operand at the given row.
- * Subclasses can override this method to customise the evaluation.
*
* @param operand search score operand
* @param row row
* @return value of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(FullTextSearchScore operand, Row row)
- throws RepositoryException {
- return valueFactory.createValue(row.getScore(operand.getSelectorName()));
+ private Value[] getFullTextSearchScoreValues(
+ FullTextSearchScore operand, Row row) throws RepositoryException {
+ double score = row.getScore(operand.getSelectorName());
+ return new Value[] { valueFactory.createValue(score) };
}
/**
- * Returns the value of the given value length operand at the given row.
- * Subclasses can override this method to customise the evaluation.
+ * Returns the values of the given value length operand at the given row.
*
* @see #getProperty(PropertyValue, Row)
* @param operand value length operand
* @param row row
- * @return value of the operand at the given row
+ * @return values of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(Length operand, Row row)
+ private Value[] getLengthValues(Length operand, Row row)
throws RepositoryException {
Property property = getProperty(operand.getPropertyValue(), row);
- return valueFactory.createValue(property.getLength());
+ if (property == null) {
+ return new Value[0];
+ } else if (property.isMultiple()) {
+ long[] lengths = property.getLengths();
+ Value[] values = new Value[lengths.length];
+ for (int i = 0; i < lengths.length; i++) {
+ values[i] = valueFactory.createValue(lengths[i]);
+ }
+ return values;
+ } else {
+ long length = property.getLength();
+ return new Value[] { valueFactory.createValue(length) };
+ }
}
/**
- * Returns the value of the given literal value operand at the given row.
- * Subclasses can override this method to customise the evaluation.
+ * Returns the value of the given literal value operand.
*
* @param operand literal value operand
- * @param row row
- * @return value of the operand at the given row
- * @throws RepositoryException if the operand can't be evaluated
+ * @return value of the operand
*/
- protected Value getValue(Literal operand, Row row) {
- return operand.getLiteralValue();
+ protected Value[] getLiteralValues(Literal operand) {
+ return new Value[] { operand.getLiteralValue() };
}
/**
- * Returns the value of the given lower case operand at the given row.
- * Subclasses can override this method to customise the evaluation.
+ * Returns the values of the given lower case operand at the given row.
*
* @param operand lower case operand
* @param row row
- * @return value of the operand at the given row
+ * @return values of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(LowerCase operand, Row row)
+ private Value[] getLowerCaseValues(LowerCase operand, Row row)
throws RepositoryException {
- Value value = getValue(operand.getOperand(), row);
- return valueFactory.createValue(value.getString().toLowerCase(ENGLISH));
+ Value[] values = getValues(operand.getOperand(), row);
+ for (int i = 0; i < values.length; i++) {
+ String value = values[i].getString();
+ String lower = value.toLowerCase(ENGLISH);
+ if (!value.equals(lower)) {
+ values[i] = valueFactory.createValue(lower);
+ }
+ }
+ return values;
}
/**
* Returns the value of the given local name operand at the given row.
- * Subclasses can override this method to customise the evaluation.
*
* @param operand local name operand
* @param row row
* @return value of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(NodeLocalName operand, Row row)
+ private Value[] getNodeLocalNameValues(NodeLocalName operand, Row row)
throws RepositoryException {
String name = row.getNode(operand.getSelectorName()).getName();
int colon = name.indexOf(':');
if (colon != -1) {
name = name.substring(colon + 1);
}
- return valueFactory.createValue(name, PropertyType.NAME);
+ return new Value[] { valueFactory.createValue(name, NAME) };
}
/**
* Returns the value of the given node name operand at the given row.
- * Subclasses can override this method to customise the evaluation.
*
* @param operand node name operand
* @param row row
* @return value of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(NodeName operand, Row row)
+ private Value[] getNodeNameValues(NodeName operand, Row row)
throws RepositoryException {
Node node = row.getNode(operand.getSelectorName());
- return valueFactory.createValue(node.getName(), PropertyType.NAME);
+ return new Value[] { valueFactory.createValue(node.getName(), NAME) };
}
/**
- * Returns the value of the given property value operand at the given row.
- * Subclasses can override this method to customise the evaluation.
+ * Returns the values of the given property value operand at the given row.
*
* @see #getProperty(PropertyValue, Row)
* @param operand property value operand
* @param row row
- * @return value of the operand at the given row
+ * @return values of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(PropertyValue operand, Row row)
+ private Value[] getPropertyValues(PropertyValue operand, Row row)
throws RepositoryException {
- return getProperty(operand, row).getValue();
+ Property property = getProperty(operand, row);
+ if (property == null) {
+ return new Value[0];
+ } else if (property.isMultiple()) {
+ return property.getValues();
+ } else {
+ return new Value[] { property.getValue() };
+ }
}
/**
- * Returns the value of the given upper case operand at the given row.
- * Subclasses can override this method to customise the evaluation.
+ * Returns the values of the given upper case operand at the given row.
*
* @param operand upper case operand
* @param row row
- * @return value of the operand at the given row
+ * @return values of the operand at the given row
* @throws RepositoryException if the operand can't be evaluated
*/
- protected Value getValue(UpperCase operand, Row row)
+ private Value[] getUpperCaseValues(UpperCase operand, Row row)
throws RepositoryException {
- Value value = getValue(operand.getOperand(), row);
- return valueFactory.createValue(value.getString().toUpperCase(ENGLISH));
+ Value[] values = getValues(operand.getOperand(), row);
+ for (int i = 0; i < values.length; i++) {
+ String value = values[i].getString();
+ String upper = value.toLowerCase(ENGLISH);
+ if (!value.equals(upper)) {
+ values[i] = valueFactory.createValue(upper);
+ }
+ }
+ return values;
}
/**
@@ -870,14 +1009,18 @@ public class QueryEngine {
*
* @param operand property value operand
* @param row row
- * @return the identified property
+ * @return the identified property, or <code>null</code>
* @throws RepositoryException if the property can't be accessed
*/
protected Property getProperty(PropertyValue operand, Row row)
throws RepositoryException {
- String selector = operand.getSelectorName();
- String property = operand.getPropertyName();
- return row.getNode(selector).getProperty(property);
+ try {
+ String selector = operand.getSelectorName();
+ String property = operand.getPropertyName();
+ return row.getNode(selector).getProperty(property);
+ } catch (PathNotFoundException e) {
+ return null;
+ }
}
}
Modified: jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SimpleRow.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SimpleRow.java?rev=1023728&r1=1023727&r2=1023728&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SimpleRow.java (original)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/SimpleRow.java Mon Oct 18 10:16:44 2010
@@ -97,4 +97,22 @@ public class SimpleRow implements Row {
}
throw new RepositoryException("Unknown selector name: " + selector);
}
+
+ //--------------------------------------------------------------< Object >
+
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("{ ");
+ for (int i = 0; i < nodes.length; i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+ builder.append(selectorNames[i]);
+ builder.append(": ");
+ builder.append(nodes[i]);
+ }
+ builder.append(" }");
+ return builder.toString();
+ }
+
}
Modified: jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/ValueComparator.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/ValueComparator.java?rev=1023728&r1=1023727&r2=1023728&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/ValueComparator.java (original)
+++ jackrabbit/trunk/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/ValueComparator.java Mon Oct 18 10:16:44 2010
@@ -88,4 +88,25 @@ public class ValueComparator implements
}
}
+ /**
+ * Evaluates the given QOM comparison operation with the given value arrays.
+ *
+ * @param operator QOM comparison operator
+ * @param a left values
+ * @param b right values
+ * @return result of the comparison
+ */
+ public boolean evaluate(String operator, Value[] a, Value[] b) {
+ if (JCR_OPERATOR_EQUAL_TO.equals(operator)) {
+ for (int i = 0; i < a.length; i++) {
+ for (int j = 0; j < b.length; j++) {
+ if (compare(a[i], b[j]) == 0) {
+ return true;
+ }
+ }
+ }
+ }
+ return false; // FIXME
+ }
+
}
\ No newline at end of file