/*
* ModeShape (http://www.modeshape.org)
*
* 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 org.modeshape.jcr;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import org.modeshape.common.util.StringUtil;
import org.modeshape.common.util.StringUtil.Justify;
/**
* @author Randall Hauch (rhauch@redhat.com)
*/
public class ValidateQuery {
public static ValidationBuilder validateQuery() {
return new Builder();
}
public static interface ValidationBuilder {
ValidationBuilder noWarnings();
ValidationBuilder warnings( int expectedCount );
ValidationBuilder onlyQueryPlan();
ValidationBuilder rowCount( int expectedRowCount );
ValidationBuilder rowCount( long expectedRowCount );
ValidationBuilder useIndex( String indexName );
ValidationBuilder useNoIndexes();
ValidationBuilder considerIndex( String indexNames );
ValidationBuilder considerIndexes( String... indexNames );
ValidationBuilder hasColumns( String... columnNames );
RowBuilder withRows();
ValidationBuilder hasNodesAtPaths( String... paths );
ValidationBuilder printDetail();
ValidationBuilder printDetail( boolean print );
ValidationBuilder onEachRow( Predicate predicate );
void validate( Query query,
QueryResult result ) throws RepositoryException;
}
public static interface RowBuilder {
RowBuilder withRow( String... paths );
RowBuilder withRow( Node... nodes );
ValidationBuilder endRows();
}
public static interface Predicate {
void validate( int rowNumber,
Row row ) throws RepositoryException;
}
protected static class Builder implements ValidationBuilder {
private int warningCount = -1;
private boolean print = false;
private boolean checkForQueryPlan = false;
private String[] columnNames;
private long numRows = -1L;
private String nameOfIndexToUse;
private String[] nameOfIndexesToConsider;
private Validator validator;
@Override
public ValidationBuilder noWarnings() {
warningCount = 0;
return this;
}
@Override
public ValidationBuilder warnings( int expectedCount ) {
warningCount = expectedCount;
return this;
}
@Override
public ValidationBuilder onlyQueryPlan() {
rowCount(0);
checkForQueryPlan = true;
return this;
}
@Override
public ValidationBuilder hasColumns( String... columnNames ) {
this.columnNames = columnNames;
return this;
}
@Override
public ValidationBuilder printDetail() {
print = true;
return this;
}
@Override
public ValidationBuilder printDetail( boolean print ) {
this.print = print;
return this;
}
@Override
public ValidationBuilder useIndex( String indexName ) {
this.nameOfIndexToUse = indexName;
checkForQueryPlan = true;
return this;
}
@Override
public ValidationBuilder considerIndexes( String... indexNames ) {
this.nameOfIndexesToConsider = indexNames;
checkForQueryPlan = true;
return this;
}
@Override
public ValidationBuilder considerIndex( String indexName ) {
assert indexName != null;
this.nameOfIndexesToConsider = new String[] {indexName};
checkForQueryPlan = true;
return this;
}
@Override
public ValidationBuilder useNoIndexes() {
this.nameOfIndexesToConsider = new String[0];
this.nameOfIndexToUse = null;
checkForQueryPlan = true;
return this;
}
@Override
public ValidationBuilder rowCount( int expectedRowCount ) {
numRows = expectedRowCount;
return this;
}
@Override
public ValidationBuilder rowCount( long expectedRowCount ) {
numRows = expectedRowCount;
return this;
}
@Override
public ValidationBuilder hasNodesAtPaths( String... paths ) {
List<String> rowPaths = Arrays.asList(paths);
numRows = rowPaths.size();
return setValidator(new SingleSelectorRowValidator(rowPaths.iterator()));
}
protected ValidationBuilder setValidator( Validator validator ) {
assert this.validator == null;
this.validator = validator;
return this;
}
@Override
public ValidationBuilder onEachRow( final Predicate predicate ) {
return setValidator(new Validator() {
private int rowNumber = 0;
@Override
public void checkRow( Row row,
String[] selectorNames ) throws RepositoryException {
predicate.validate(++rowNumber, row);
}
});
}
@Override
public RowBuilder withRows() {
final List<Object[]> rows = new ArrayList<Object[]>();
return new RowBuilder() {
@Override
public RowBuilder withRow( String... paths ) {
rows.add(paths);
return this;
}
@Override
public RowBuilder withRow( Node... nodes ) {
rows.add(nodes);
return this;
}
@Override
public ValidationBuilder endRows() {
return setValidator(new MultiSelectorRowValidator(rows.iterator()));
}
};
}
@Override
public void validate( Query query,
QueryResult result ) throws RepositoryException {
assertThat(query, is(notNullValue()));
assertThat(result, is(notNullValue()));
if (print) print(query, result);
try {
validateWarnings(result);
validateColumnNames(result);
validateQueryPlan(result);
} catch (AssertionError e) {
if (!print) {
// print anyway since this is an error
print(query, result);
}
throw e;
}
NodeIterator nodes = null;
if (result.getSelectorNames().length != 1) {
// Make sure that we cannot get the multi-selector results from this single-selector results ...
try {
nodes = result.getNodes();
if (!print) {
// print anyway since this is an error
print(query, result);
}
fail("should not be able to call this method when the query has multiple selectors");
} catch (RepositoryException e) {
// expected; can't call this when the query uses multiple selectors ...
}
} else {
nodes = result.getNodes();
}
// Check the row count ...
RowIterator iter = result.getRows();
if (!validateRowCount(iter.getSize()) && !print) {
// we're not printing this, but print anyway since this is an error
print(query, result);
print = true;
}
// Now validate the query results ...
Printer printer = new Printer(result.getColumnNames());
if (print) printer.printHeader();
while (iter.hasNext()) {
Row row = iter.nextRow();
assertThat(row, is(notNullValue()));
if (print) printer.printRow(row);
if (validator != null) validator.checkRow(row, result.getSelectorNames());
}
if (print) printer.printFooter();
assertRowCount(iter.getSize());
if (nodes != null) {
// Check the single-selector results via node iterator ...
assertTrue(result.getSelectorNames().length == 1);
while (nodes.hasNext()) {
Node node = nodes.nextNode();
assert node != null || node == null; // duh!
// if (print) printer.printNode(node);
}
} else {
assertTrue(result.getSelectorNames().length != 1);
}
}
protected boolean validateRowCount( long actual ) {
if (actual < 0L || numRows < 0L) return true;
return actual == numRows;
}
protected void assertRowCount( long actual ) {
if (actual >= 0L && numRows >= 0L) assertThat(actual, is(numRows));
}
protected void validateWarnings( QueryResult result ) {
if (warningCount >= 0) {
Collection<String> warnings = ((org.modeshape.jcr.api.query.QueryResult)result).getWarnings();
if (print) {
System.out.println("Warnings on query");
for (String warning : warnings) {
System.out.println(" " + warning);
}
System.out.println();
}
assertThat(warnings.size(), is(warningCount));
}
}
protected void validateColumnNames( QueryResult result ) throws RepositoryException {
if (columnNames != null) {
List<String> expectedNames = new ArrayList<String>();
for (String name : columnNames) {
expectedNames.add(name);
}
List<String> actualNames = new ArrayList<String>();
for (String name : result.getColumnNames()) {
actualNames.add(name);
}
Collections.sort(expectedNames);
Collections.sort(actualNames);
assertThat(actualNames, is(expectedNames));
}
}
protected void validateQueryPlan( QueryResult result ) {
if (checkForQueryPlan) {
String plan = ((org.modeshape.jcr.api.query.QueryResult)result).getPlan();
assertNotNull(plan);
assertTrue(plan.trim().length() > 0);
// Figure out which indexes are expected ...
Set<String> allIndexNames = new HashSet<>();
if (nameOfIndexesToConsider != null) {
for (String indexName : nameOfIndexesToConsider) {
allIndexNames.add(indexName);
}
}
if (nameOfIndexToUse != null) {
allIndexNames.add(nameOfIndexToUse);
}
// Look for the indexes ...
Set<String> allIndexNamesCopy = new HashSet<>(allIndexNames);
boolean foundUsed = false;
if (!allIndexNames.isEmpty()) {
for (String line : StringUtil.splitLines(plan)) {
Matcher matcher = INDEX_NAME_PATTERN.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);
if (allIndexNames.contains(name)) {
allIndexNamesCopy.remove(name);
} else {
fail("Index '" + name + "' was included in plan but not expected");
}
boolean isUsed = INDEX_USED_PATTERN.matcher(line).find();
if (isUsed) {
assertEquals("Index '" + name + "' was used, but '" + nameOfIndexToUse
+ "' was expected to be used", nameOfIndexToUse, name);
foundUsed = true;
}
}
}
}
if (!foundUsed && nameOfIndexToUse != null) {
fail("Index '" + nameOfIndexToUse + "' was not used in query as expected");
}
if (!allIndexNamesCopy.isEmpty()) {
fail("Indexes " + allIndexNames + " were found in query plan but not expected");
}
}
}
protected void print( Query query,
QueryResult result ) {
System.out.println();
System.out.println(query);
System.out.println(" plan -> " + ((org.modeshape.jcr.api.query.QueryResult)result).getPlan());
System.out.println(result);
}
}
protected static class Printer {
protected static final int MAXIMUM_PATH_DISPLAY_LENGTH = 64;
protected static final int MAXIMUM_NAME_DISPLAY_LENGTH = 32;
protected static final int MAXIMUM_REFERENCE_DISPLAY_LENGTH = UUID.randomUUID().toString().length() + 2;
protected static final int MAXIMUM_KNOWN_STRING_DISPLAY_LENGTH = 16;
private int widthOfRowNumber = 4;
private int rowNumber = 0;
private final String[] columnNames;
private final int[] columnWidths;
protected Printer( String[] columnNames ) {
this(columnNames, MAXIMUM_PATH_DISPLAY_LENGTH, MAXIMUM_NAME_DISPLAY_LENGTH, MAXIMUM_REFERENCE_DISPLAY_LENGTH,
MAXIMUM_KNOWN_STRING_DISPLAY_LENGTH);
}
protected Printer( String[] columnNames,
int maxPathLength,
int maxNameLength,
int maxRefLength,
int maxStringLength ) {
this.columnNames = columnNames;
columnWidths = new int[columnNames.length];
for (int i = 0; i != columnNames.length; ++i) {
String columnName = columnNames[i].toLowerCase();
int columnWidth = columnName.length();
if (columnName.endsWith("jcr:path")) {
columnWidths[i] = Math.max(columnWidth, maxPathLength);
} else if (columnName.endsWith("jcr:name") || columnName.endsWith("mode:localName")) {
columnWidths[i] = Math.max(columnWidth, maxNameLength);
} else if (columnName.endsWith("jcr:primaryType") || columnName.endsWith("jcr:mixinTypes")
|| columnName.endsWith("jcr:createdBy")) {
columnWidths[i] = Math.max(columnWidth, maxStringLength);
} else if (columnName.endsWith("reference") || columnName.endsWith("jcr:uuid") || columnName.endsWith("jcr_uuid")) {
columnWidths[i] = Math.max(columnWidth, maxRefLength);
} else {
columnWidths[i] = Math.max(columnWidth, columnWidth);
}
}
}
protected void printHeader() {
printDelimiter();
System.out.print("| " + StringUtil.createString(' ', widthOfRowNumber));
int columnIndex = 0;
for (String columnName : columnNames) {
System.out.print(" | ");
System.out.print(formatForColumn(columnName, columnIndex++, Justify.CENTER));
}
System.out.println(" |");
printDelimiter();
}
protected void printFooter() {
printDelimiter();
}
private void printDelimiter() {
System.out.print("+-" + StringUtil.createString('-', widthOfRowNumber));
for (int i = 0; i != columnNames.length; ++i) {
System.out.print("-+-");
System.out.print(StringUtil.createString('-', columnWidths[i]));
}
System.out.println("-+");
}
private String formatForColumn( String value,
int columnIndex,
Justify justify ) {
return StringUtil.justify(justify, value, columnWidths[columnIndex], ' ');
}
protected String rowNumberStr() {
return StringUtil.justifyRight("" + (++rowNumber), widthOfRowNumber, ' ');
}
protected String valueAsString( Value value,
int columnIndex ) throws RepositoryException {
String str = value != null ? value.getString() : "";
Justify justify = justify(value);
return formatForColumn(str, columnIndex, justify);
}
protected Justify justify( Value value ) {
return Justify.LEFT;
// if (value == null) {
// return Justify.RIGHT;
// }
// switch (value.getType()) {
// case PropertyType.BOOLEAN:
// case PropertyType.DECIMAL:
// case PropertyType.DOUBLE:
// case PropertyType.LONG:
// return Justify.RIGHT;
// default:
// return Justify.LEFT;
// }
}
protected void printRow( Row row ) throws RepositoryException {
System.out.print("| ");
System.out.print(rowNumberStr());
int columnIndex = 0;
for (String columnName : columnNames) {
System.out.print(" | ");
System.out.print(valueAsString(row.getValue(columnName), columnIndex++));
}
System.out.println(" |");
}
protected void printNode( Node node ) throws RepositoryException {
System.out.print("| ");
System.out.print(rowNumberStr());
System.out.print(" | ");
System.out.print(node.getPath());
System.out.println(" |");
}
}
protected static interface Validator {
void checkRow( Row row,
String[] selectorNames ) throws RepositoryException;
}
protected static class SingleSelectorRowValidator implements Validator {
private final Iterator<? extends Object> iterator;
protected SingleSelectorRowValidator( Iterator<? extends Object> iter ) {
this.iterator = iter;
}
@Override
public void checkRow( Row row,
String[] selectorNames ) throws RepositoryException {
Object expected = iterator.next();
if (expected instanceof String) {
assertThat(row.getPath(), is((String)expected));
} else if (expected instanceof Node) {
assertThat(row.getNode(), is(expected));
}
}
}
protected static class MultiSelectorRowValidator implements Validator {
private final Iterator<Object[]> iterator;
protected MultiSelectorRowValidator( Iterator<Object[]> iter ) {
this.iterator = iter;
}
@Override
public void checkRow( Row row,
String[] selectorNames ) throws RepositoryException {
Object[] expected = iterator.next();
int i = 0;
if (expected[0] instanceof String) {
for (String selector : selectorNames) {
assertThat(row.getPath(selector), is(expected[i++]));
}
} else if (expected[0] instanceof Node) {
for (String selector : selectorNames) {
assertThat(row.getNode(selector), is(expected[i++]));
}
}
}
}
protected static final Pattern INDEX_NAME_PATTERN = Pattern.compile("INDEX_SPECIFICATION=([^,]*)");
protected static final Pattern INDEX_USED_PATTERN = Pattern.compile("INDEX_USED=true");
private ValidateQuery() {
}
}