VIVO-869 Drastically simplify the search configuration.

Create LabelsAcrossContextNodes to replace AdditionalURIsForContextNodes. It's more configurable.
Create SelectQueryUriFinder to replace AdditionalUrisForVCards. It's more configurable.
Rename SimpleSparqlQueryDocumentModifier to SelectQueryDocumentModifier, to conform.
Radically simplify the VIVO search configuration, omitting all special cases except those for VCards.
This commit is contained in:
Jim Blake 2015-02-06 15:22:42 -05:00
parent 8e97ff3678
commit d25176298d
7 changed files with 607 additions and 68 deletions

View file

@ -61,11 +61,11 @@
# ------------------------------------
:documentModifier_AllNames
a <java:edu.cornell.mannlib.vitro.webapp.searchindex.documentBuilding.SimpleSparqlQueryDocumentModifier> ,
a <java:edu.cornell.mannlib.vitro.webapp.searchindex.documentBuilding.SelectQueryDocumentModifier> ,
<java:edu.cornell.mannlib.vitro.webapp.searchindex.documentBuilding.DocumentModifier> ;
rdfs:label "All labels are added to name fields." ;
:hasTargetField "nameRaw" ;
:hasSparqlQuery """
:hasSelectQuery """
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?label
WHERE {

View file

@ -5,34 +5,32 @@ package edu.cornell.mannlib.vitro.webapp.searchindex.documentBuilding;
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONTENT;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.ALLTEXT;
import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.ALLTEXTUNSTEMMED;
import static edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.createQueryContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.hp.hpl.jena.query.QuerySolution;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.rdf.model.RDFNode;
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
import edu.cornell.mannlib.vitro.webapp.beans.VClass;
import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchInputDocument;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.ContextModelsUser;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Validation;
import edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryHolder;
/**
* If the individual qualifies, execute the SPARQL queries and add the results
* to the specified search fields.
* Modify the document, adding the results of one or more select queries.
*
* If the individual qualifies, execute the queries and add the results to the
* specified search fields.
*
* If there are no specified search fields, ALLTEXT and ALLTEXTUNSTEMMED are
* assumed.
@ -44,16 +42,15 @@ import edu.cornell.mannlib.vitro.webapp.utils.configuration.Validation;
* of the individual.
*
* All of the result fields of all result rows of all of the queries will be
* concatenated into a single result, separated by spaces. That result will be
* added to each of the specified search fields.
* converted to strings and added to each of the specified search fields.
*
* A label may be supplied to the instance, for use in logging. If no label is
* supplied, one will be generated.
*/
public class SimpleSparqlQueryDocumentModifier implements DocumentModifier,
public class SelectQueryDocumentModifier implements DocumentModifier,
ContextModelsUser {
private static final Log log = LogFactory
.getLog(SimpleSparqlQueryDocumentModifier.class);
.getLog(SelectQueryDocumentModifier.class);
private RDFService rdfService;
@ -85,7 +82,7 @@ public class SimpleSparqlQueryDocumentModifier implements DocumentModifier,
label = l;
}
@Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasSparqlQuery")
@Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasSelectQuery")
public void addQuery(String query) {
queries.add(query);
}
@ -122,14 +119,12 @@ public class SimpleSparqlQueryDocumentModifier implements DocumentModifier,
@Override
public void modifyDocument(Individual ind, SearchInputDocument doc) {
if (!passesTypeRestrictions(ind)) {
return;
}
String text = getTextForQueries(ind);
if (passesTypeRestrictions(ind)) {
List<String> values = getTextForQueries(ind);
for (String fieldName : fieldNames) {
doc.addField(fieldName, text);
doc.addField(fieldName, values);
}
}
}
@ -146,59 +141,25 @@ public class SimpleSparqlQueryDocumentModifier implements DocumentModifier,
return false;
}
private String getTextForQueries(Individual ind) {
private List<String> getTextForQueries(Individual ind) {
List<String> list = new ArrayList<>();
for (String query : queries) {
String text = getTextForQuery(substituteUri(ind, query));
if (StringUtils.isNotBlank(text)) {
list.add(text);
list.addAll(getTextForQuery(query, ind));
}
}
return StringUtils.join(list, " ");
return list;
}
private String substituteUri(Individual ind, String query) {
return query.replace("?uri", "<" + ind.getURI() + "> ");
}
private String getTextForQuery(String query) {
List<String> list = new ArrayList<>();
private List<String> getTextForQuery(String query, Individual ind) {
try {
ResultSet results = RDFServiceUtils.sparqlSelectQuery(query,
rdfService);
while (results.hasNext()) {
String text = getTextForRow(results.nextSolution());
if (StringUtils.isNotBlank(text)) {
list.add(text);
}
}
SelectQueryHolder queryHolder = new SelectQueryHolder(query)
.bindToUri("uri", ind.getURI());
List<String> list = createQueryContext(rdfService, queryHolder)
.execute().getStringFields().flatten();
log.debug(label + " - query: '" + query + "' returns " + list);
return list;
} catch (Throwable t) {
log.error("problem while running query '" + query + "'", t);
}
log.debug(label + " - query: '" + query + "' returns " + list);
return StringUtils.join(list, " ");
}
private String getTextForRow(QuerySolution row) {
List<String> list = new ArrayList<>();
Iterator<String> names = row.varNames();
while (names.hasNext()) {
RDFNode node = row.get(names.next());
String text = getTextForNode(node);
if (StringUtils.isNotBlank(text)) {
list.add(text);
}
}
return StringUtils.join(list, " ");
}
private String getTextForNode(RDFNode node) {
if (node == null) {
return "";
} else if (node.isLiteral()) {
return node.asLiteral().getString().trim();
} else {
return node.toString().trim();
return Collections.emptyList();
}
}

View file

@ -0,0 +1,157 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.searchindex.indexing;
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONTENT;
import static edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.createQueryContext;
import static edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.selectQuery;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Statement;
import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.ContextModelsUser;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Validation;
import edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryHolder;
/**
* Find URIs based on one or more select queries.
*
* If the statement qualifies, execute the queries and return the accumulated
* results.
*
* A statement qualifies if the predicate matches any of the restrictions, or if
* there are no restrictions.
*
* If a query contains a ?subject, ?predicate, or ?object variable, it will be
* bound to the URI of the subject, predicate, or object of the statement,
* respectively. If the subject or object has no URI and the query expects one,
* then the query will be ignored. (Predicates always have URIs.)
*
* All of the result fields of all result rows of all of the queries will be
* returned.
*
* A label may be supplied to the instance, for use in logging. If no label is
* supplied, one will be generated.
*/
public class SelectQueryUriFinder implements IndexingUriFinder,
ContextModelsUser {
private static final Log log = LogFactory
.getLog(SelectQueryUriFinder.class);
private RDFService rdfService;
/** A name to be used in logging, to identify this instance. */
private String label;
/** The queries to be executed. There must be at least one. */
private List<String> queries = new ArrayList<>();
/**
* URIs of the predicates that will trigger these queries. If empty, then
* the queries apply to all statements.
*/
private Set<String> predicateRestrictions = new HashSet<>();
@Override
public void setContextModels(ContextModelAccess models) {
this.rdfService = models.getRDFService(CONTENT);
}
@Property(uri = "http://www.w3.org/2000/01/rdf-schema#label")
public void setLabel(String l) {
label = l;
}
@Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasSelectQuery")
public void addQuery(String query) {
queries.add(query);
}
@Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasPredicateRestriction")
public void addPredicateRestriction(String predicateUri) {
predicateRestrictions.add(predicateUri);
}
@Validation
public void validate() {
if (label == null) {
label = this.getClass().getSimpleName() + ":" + this.hashCode();
}
if (queries.isEmpty()) {
throw new IllegalStateException(
"Configuration contains no queries for " + label);
}
}
@Override
public String toString() {
return (label == null) ? super.toString() : label;
}
@Override
public void startIndexing() {
// Nothing to do.
}
@Override
public List<String> findAdditionalURIsToIndex(Statement stmt) {
List<String> list = new ArrayList<>();
if (passesTypePredicateRestrictions(stmt)) {
for (String query : queries) {
list.addAll(getUrisForQuery(stmt, query));
}
}
return list;
}
private boolean passesTypePredicateRestrictions(Statement stmt) {
return predicateRestrictions.isEmpty()
|| predicateRestrictions.contains(stmt.getPredicate().getURI());
}
private List<String> getUrisForQuery(Statement stmt, String queryString) {
SelectQueryHolder query = selectQuery(queryString);
query = query.bindToUri("predicate", stmt.getPredicate().getURI());
query = tryToBindUri(query, "subject", stmt.getSubject());
query = tryToBindUri(query, "object", stmt.getObject());
if (query == null) {
return Collections.emptyList();
}
return createQueryContext(rdfService, query).execute()
.getStringFields().flatten();
}
private SelectQueryHolder tryToBindUri(SelectQueryHolder query,
String name, RDFNode node) {
if (query == null) {
return null;
}
if (!query.hasVariable(name)) {
return query;
}
if (!node.isURIResource()) {
return null;
}
return query.bindToUri(name, node.asResource().getURI());
}
@Override
public void endIndexing() {
// Nothing to do.
}
}

View file

@ -0,0 +1,125 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.sparql;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.hp.hpl.jena.query.QuerySolution;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.rdf.model.RDFNode;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
import edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.ExecutingSelectQueryContext;
import edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.SelectQueryContext;
import edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.StringResultsMapping;
import edu.cornell.mannlib.vitro.webapp.utils.sparql.SelectQueryRunner.StringResultsMappingImpl;
/**
* An implementation of QueryContext based on an RDFService.
*
* Package access. Instances should be created only by SelectQueryRunner, or by
* a method on this class.
*/
class RdfServiceQueryContext implements SelectQueryContext {
private static final Log log = LogFactory
.getLog(RdfServiceQueryContext.class);
private final RDFService rdfService;
private final SelectQueryHolder query;
RdfServiceQueryContext(RDFService rdfService, SelectQueryHolder query) {
this.rdfService = rdfService;
this.query = query;
}
@Override
public RdfServiceQueryContext bindVariableToUri(String name, String uri) {
return new RdfServiceQueryContext(rdfService,
query.bindToUri(name, uri));
}
@Override
public ExecutingSelectQueryContext execute() {
return new RdfServiceExecutingQueryContext(rdfService, query);
}
private static class RdfServiceExecutingQueryContext implements
ExecutingSelectQueryContext {
private final RDFService rdfService;
private final SelectQueryHolder query;
public RdfServiceExecutingQueryContext(RDFService rdfService,
SelectQueryHolder query) {
this.rdfService = rdfService;
this.query = query;
}
@Override
public StringResultsMapping getStringFields(String... names) {
Set<String> fieldNames = new HashSet<>(Arrays.asList(names));
StringResultsMappingImpl mapping = new StringResultsMappingImpl();
try {
ResultSet results = RDFServiceUtils.sparqlSelectQuery(
query.getQueryString(), rdfService);
return mapResultsForQuery(results, fieldNames);
} catch (Exception e) {
log.error(
"problem while running query '"
+ query.getQueryString() + "'", e);
}
return mapping;
}
private StringResultsMapping mapResultsForQuery(ResultSet results,
Set<String> fieldNames) {
StringResultsMappingImpl mapping = new StringResultsMappingImpl();
while (results.hasNext()) {
Map<String, String> rowMapping = mapResultsForRow(
results.nextSolution(), fieldNames);
if (!rowMapping.isEmpty()) {
mapping.add(rowMapping);
}
}
return mapping;
}
private Map<String, String> mapResultsForRow(QuerySolution row,
Set<String> fieldNames) {
Map<String, String> map = new HashMap<>();
for (Iterator<String> names = row.varNames(); names.hasNext();) {
String name = names.next();
RDFNode node = row.get(name);
String text = getTextForNode(node);
if (StringUtils.isNotBlank(text)) {
map.put(name, text);
}
}
if (!fieldNames.isEmpty()) {
map.keySet().retainAll(fieldNames);
}
return map;
}
private String getTextForNode(RDFNode node) {
if (node == null) {
return "";
} else if (node.isLiteral()) {
return node.asLiteral().getString().trim();
} else {
return node.toString().trim();
}
}
}
}

View file

@ -0,0 +1,36 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.sparql;
import java.util.regex.Pattern;
/**
* Holds the text of a SPARQL Select query, and allows you to perform some lexical
* operations on it.
*
* This is immutable, so don't forget to get the result of the operations.
*/
public class SelectQueryHolder {
private final String queryString;
public SelectQueryHolder(String queryString) {
this.queryString = queryString;
}
public String getQueryString() {
return queryString;
}
public boolean hasVariable(String name) {
String regex = "\\?" + name + "\\b";
return Pattern.compile(regex).matcher(queryString).find();
}
public SelectQueryHolder bindToUri(String name, String uri) {
String regex = "\\?" + name + "\\b";
String replacement = "<" + uri + ">";
String bound = queryString.replaceAll(regex, replacement);
return new SelectQueryHolder(bound);
}
}

View file

@ -0,0 +1,104 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.sparql;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
/**
* A conversational tool for handling SPARQL queries.
*
* <pre>
* Examples:
* List<String> values = createQueryContext(rdfService, queryString)
* .bindVariableToUri("uri", uri)
* .execute()
* .getStringFields("partner")
* .flatten();
*
* SelectQueryHolder q = selectQuery(queryString)
* .bindToUri("uri", uri));
* List<Map<String, String> map = createQueryContext(rdfService, q)
* .execute()
* .getStringFields();
* </pre>
*
* The execute() method does not actually execute the query: it merely sets it
* up syntactically.
*
* If you don't supply any field names to getStringFields(), you get all of
* them.
*
* Any string value that returns a blank or empty string is omitted from the
* results. Any row that returns no values is omitted from the results.
*/
public final class SelectQueryRunner {
private static final Log log = LogFactory.getLog(SelectQueryRunner.class);
private SelectQueryRunner() {
// No need to create an instance.
}
public static SelectQueryHolder selectQuery(String queryString) {
return new SelectQueryHolder(queryString);
}
public static SelectQueryContext createQueryContext(RDFService rdfService,
String queryString) {
return createQueryContext(rdfService, selectQuery(queryString));
}
public static SelectQueryContext createQueryContext(RDFService rdfService,
SelectQueryHolder query) {
return new RdfServiceQueryContext(rdfService, query);
}
public static interface SelectQueryContext {
public SelectQueryContext bindVariableToUri(String name, String uri);
public ExecutingSelectQueryContext execute();
}
public static interface ExecutingSelectQueryContext {
public StringResultsMapping getStringFields(String... fieldNames);
}
public static interface StringResultsMapping extends
List<Map<String, String>> {
public List<String> flatten();
public Set<String> flattenToSet();
}
// ----------------------------------------------------------------------
// Helper classes
// ----------------------------------------------------------------------
static class StringResultsMappingImpl extends
ArrayList<Map<String, String>> implements StringResultsMapping {
@Override
public List<String> flatten() {
List<String> flat = new ArrayList<>();
for (Map<String, String> map : this) {
flat.addAll(map.values());
}
return flat;
}
@Override
public Set<String> flattenToSet() {
return new HashSet<>(flatten());
}
}
}

View file

@ -0,0 +1,156 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.searchindex.indexing;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createPlainLiteral;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createStatement;
import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.WhichService.CONTENT;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.Test;
import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccessStub;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel;
/**
* TODO
*
* If the statement qualifies, execute the queries and return the accumulated
* results.
*
* A statement qualifies if the predicate matches any of the restrictions, or if
* there are no restrictions.
*
* If a query contains a ?subject or ?object variable, it will be bound to the
* URI of the subject or object of the statement, respectively. If the subject
* or object has no URI for the query, then the query will be ignored.
*
* All of the result fields of all result rows of all of the queries will be
* returned.
*
* A label may be supplied to the instance, for use in logging. If no label is
* supplied, one will be generated.
*/
public class SelectQueryUriFinderTest extends AbstractTestClass {
private static final Log log = LogFactory
.getLog(SelectQueryUriFinderTest.class);
private static final String BOB_URI = "http://ns#Bob";
private static final String BETTY_URI = "http://ns#Betty";
private static final String DICK_URI = "http://ns#Dick";
private static final String JANE_URI = "http://ns#Jane";
private static final String FRIEND_URI = "http://ns#Friend";
private static final String SEES_URI = "http://ns#Sees";
private static final String OTHER_URI = "http://ns#Other";
private static final Resource BOB = createResource(BOB_URI);
private static final Resource BETTY = createResource(BETTY_URI);
private static final Resource DICK = createResource(DICK_URI);
private static final Resource JANE = createResource(JANE_URI);
private static final Property FRIEND = createProperty(FRIEND_URI);
private static final Property SEES = createProperty(SEES_URI);
private static final String QUERY1 = "SELECT ?friend WHERE {?subject <"
+ FRIEND_URI + "> ?friend}";
private static final String QUERY2 = "SELECT ?partner WHERE {?object <"
+ FRIEND_URI + "> ?partner}";
private Model m;
private RDFService rdfService;
private SelectQueryUriFinder finder;
private List<String> foundUris;
@Before
public void populateModel() {
m = ModelFactory.createDefaultModel();
m.add(createStatement(BOB, FRIEND, BETTY));
m.add(createStatement(DICK, FRIEND, JANE));
rdfService = new RDFServiceModel(m);
ContextModelAccessStub models = new ContextModelAccessStub();
models.setRDFService(CONTENT, rdfService);
finder = new SelectQueryUriFinder();
finder.setContextModels(models);
finder.addQuery(QUERY1);
finder.addQuery(QUERY2);
}
@Test
public void fullSuccess_bothResults() {
setPredicateRestrictions();
exerciseUriFinder(BOB, SEES, DICK);
assertExpectedUris(BETTY_URI, JANE_URI);
}
@Test
public void acceptableRestriction_bothResults() {
setPredicateRestrictions(SEES_URI);
exerciseUriFinder(BOB, SEES, DICK);
assertExpectedUris(BETTY_URI, JANE_URI);
}
@Test
public void excludingRestriction_noResults() {
setPredicateRestrictions(OTHER_URI);
exerciseUriFinder(BOB, SEES, DICK);
assertExpectedUris();
}
@Test
public void blankSubject_justObjectResult() {
setPredicateRestrictions();
exerciseUriFinder(createResource(), SEES, DICK);
assertExpectedUris(JANE_URI);
}
@Test
public void literalObject_justSubjectResult() {
setPredicateRestrictions();
exerciseUriFinder(BOB, SEES, createPlainLiteral("Bogus"));
assertExpectedUris(BETTY_URI);
}
// ----------------------------------------------------------------------
// Helper methods
// ----------------------------------------------------------------------
private void setPredicateRestrictions(String... uris) {
for (String uri : uris) {
finder.addPredicateRestriction(uri);
}
}
private void exerciseUriFinder(Resource subject, Property predicate,
RDFNode object) {
foundUris = finder.findAdditionalURIsToIndex(createStatement(subject,
predicate, object));
}
private void assertExpectedUris(String... expectedArray) {
Set<String> expected = new HashSet<>(Arrays.asList(expectedArray));
Set<String> actual = new HashSet<>(foundUris);
assertEquals("found URIs", expected, actual);
}
}