ExploreServlet.java
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.workbench.commands;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rdf4j.common.exception.RDF4JException;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryResult;
import org.eclipse.rdf4j.workbench.base.TupleServlet;
import org.eclipse.rdf4j.workbench.exceptions.BadRequestException;
import org.eclipse.rdf4j.workbench.util.TupleResultBuilder;
import org.eclipse.rdf4j.workbench.util.WorkbenchRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExploreServlet extends TupleServlet {
private final Logger logger = LoggerFactory.getLogger(ExploreServlet.class);
protected static final String LIMIT = "limit_explore";
protected static final int LIMIT_DEFAULT = 100;
public ExploreServlet() {
super("explore.xsl", "subject", "predicate", "object", "context");
}
@Override
public String[] getCookieNames() {
return new String[] { LIMIT, "total_result_count", "show-datatypes" };
}
@Override
public void service(final WorkbenchRequest req, final HttpServletResponse resp, final String xslPath)
throws Exception {
try {
super.service(req, resp, xslPath);
} catch (BadRequestException exc) {
logger.warn(exc.toString(), exc);
final TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
builder.transform(xslPath, "explore.xsl");
builder.start("error-message");
builder.link(List.of(INFO));
builder.result(exc.getMessage());
builder.end();
}
}
@Override
protected void service(final WorkbenchRequest req, final HttpServletResponse resp, final TupleResultBuilder builder,
final RepositoryConnection con) throws BadRequestException, RDF4JException {
final Value value = req.getValue("resource");
logger.debug("resource = {}", value);
// At worst, malicious parameter value could cause inaccurate
// reporting of count in page.
int count = req.getInt("know_total");
if (count == 0) {
count = this.processResource(con, builder, value, 0, Integer.MAX_VALUE, false).getTotalResultCount();
}
this.cookies.addTotalResultCountCookie(req, resp, (int) count);
final int offset = req.getInt("offset");
int limit = LIMIT_DEFAULT;
if (req.isParameterPresent(LIMIT)) {
limit = req.getInt(LIMIT);
if (0 == limit) {
limit = Integer.MAX_VALUE;
}
}
this.processResource(con, builder, value, offset, limit, true);
}
/**
* Query the repository for all instances of the given value, optionally writing the results into the HTTP response.
*
* @param con the connection to the repository
* @param builder used for writing to the HTTP response
* @param value the value to query the repository for
* @param offset The result at which to start rendering results.
* @param limit The limit on the number of results to render.
* @param render If false, suppresses output to the HTTP response.
* @throws RDF4JException if there is an issue iterating through results
* @return The count of all triples in the repository using the given value.
*/
protected ResultCursor processResource(final RepositoryConnection con, final TupleResultBuilder builder,
final Value value, final int offset, final int limit, final boolean render) throws RDF4JException {
final ResultCursor cursor = new ResultCursor(offset, limit, render);
boolean resource = value instanceof Resource;
if (resource) {
export(con, builder, cursor, (Resource) value, null, null);
logger.debug("After subject, total = {}", cursor.getTotalResultCount());
}
if (value instanceof IRI) {
export(con, builder, cursor, null, (IRI) value, null);
logger.debug("After predicate, total = {}", cursor.getTotalResultCount());
}
if (value != null) {
export(con, builder, cursor, null, null, value);
logger.debug("After object, total = {}", cursor.getTotalResultCount());
}
if (resource) {
export(con, builder, cursor, null, null, null, (Resource) value);
logger.debug("After context, total = {}", cursor.getTotalResultCount());
}
return cursor;
}
/**
* <p>
* Render statements in the repository matching the given pattern to the HTTP response. It is an implicit assumption
* when this calls {@link #isFirstTimeSeen} that {@link #processResource} 's calls into here have been made in the
* following order:
* </p>
* <ol>
* <li>export(*, subject, null, null, null)</li>
* <li>export(*, null, predicate, null, null)</li>
* <li>export(*, null, null, object, null)</li>
* <li>export(*, null, null, null, context)</li>
* </ol>
*
* @param con the connection to the repository
* @param builder used for writing to the HTTP response
* @param cursor used for keeping track of our location in the result set
* @param subj the triple subject
* @param pred the triple predicate
* @param obj the triple object
* @param context the triple context
*/
private void export(RepositoryConnection con, TupleResultBuilder builder, ResultCursor cursor, Resource subj,
IRI pred, Value obj, Resource... context)
throws RDF4JException, MalformedQueryException, QueryEvaluationException {
try (RepositoryResult<Statement> result = con.getStatements(subj, pred, obj, true, context)) {
while (result.hasNext()) {
Statement statement = result.next();
if (isFirstTimeSeen(statement, pred, obj, context)) {
if (cursor.mayRender()) {
builder.result(statement.getSubject(), statement.getPredicate(), statement.getObject(),
statement.getContext());
}
cursor.advance();
}
}
}
}
/**
* Gets whether this is the first time the result quad has been seen.
*
* @param patternPredicate the predicate asked for, or null if another quad element was asked for
* @param patternObject the object asked for, or null if another quad element was asked for
* @param result the result statement to determine if we've already seen
* @param patternContext the context asked for, or null if another quad element was asked for
* @return true, if this is the first time the quad has been seen, false otherwise
*/
private boolean isFirstTimeSeen(Statement result, IRI patternPredicate, Value patternObject,
Resource... patternContext) {
Resource resultSubject = result.getSubject();
IRI resultPredicate = result.getPredicate();
Value resultObject = result.getObject();
boolean firstTimeSeen;
if (1 == patternContext.length) {
// I.e., when context matches explore value.
Resource ctx = patternContext[0];
firstTimeSeen = !(ctx.equals(resultSubject) || ctx.equals(resultPredicate) || ctx.equals(resultObject));
} else if (null != patternObject) {
// I.e., when object matches explore value.
firstTimeSeen = !(resultObject.equals(resultSubject) || resultObject.equals(resultPredicate));
} else if (null != patternPredicate) {
// I.e., when predicate matches explore value.
firstTimeSeen = !(resultPredicate.equals(resultSubject));
} else {
// I.e., when subject matches explore value.
firstTimeSeen = true;
}
return firstTimeSeen;
}
/**
* Class for keeping track of location within the result set, relative to offset and limit.
*
* @author Dale Visser
*/
protected class ResultCursor {
private int untilFirst;
private int totalResults = 0;
private int renderedResults = 0;
private final int limit;
private final boolean render;
/**
* @param offset the desired offset at which rendering should start
* @param limit the desired maximum number of results to render
* @param render if false, suppresses any rendering
*/
public ResultCursor(final int offset, final int limit, final boolean render) {
this.render = render;
this.limit = limit > 0 ? limit : Integer.MAX_VALUE;
this.untilFirst = offset >= 0 ? offset : 0;
}
/**
* Gets the total number of results. Only meant to be called after advance() has been called for all results in
* the set.
*
* @return the number of times advance() has been called
*/
public int getTotalResultCount() {
return this.totalResults;
}
/**
* Gets the number of results that were actually rendered. Only meant to be called after advance() has been
* called for all results in the set.
*
* @return the number of times advance() has been called when this.mayRender() evaluated to true
*/
public int getRenderedResultCount() {
return this.renderedResults;
}
/**
* @return whether it is allowed to render the next result
*/
public boolean mayRender() {
return this.render && (this.untilFirst == 0 && this.renderedResults < this.limit);
}
/**
* Advances the cursor, incrementing the total count, and moving other internal counters.
*/
public void advance() {
this.totalResults++;
if (this.mayRender()) {
this.renderedResults++;
}
if (this.untilFirst > 0) {
this.untilFirst--;
}
}
}
}