Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
<dependency>
<groupId>org.nanopub</groupId>
<artifactId>nanopub</artifactId>
<version>1.86.0</version>
<version>1.86.1</version>
</dependency>
<!-- Temporary: dependency on Jitpack for snapshot builds -->
<!-- <dependency>
Expand Down
81 changes: 81 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/ApiCache.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.knowledgepixels.nanodash;

import org.eclipse.rdf4j.model.Model;
import org.nanopub.extra.services.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -21,6 +22,7 @@ private ApiCache() {
} // no instances allowed

private transient static ConcurrentMap<String, ApiResponse> cachedResponses = new ConcurrentHashMap<>();
private transient static ConcurrentMap<String, Model> cachedRdfModels = new ConcurrentHashMap<>();
private transient static ConcurrentMap<String, Integer> failed = new ConcurrentHashMap<>();
private transient static ConcurrentMap<String, Map<String, String>> cachedMaps = new ConcurrentHashMap<>();
private transient static ConcurrentMap<String, Long> lastRefresh = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -242,6 +244,85 @@ public static synchronized Map<String, String> retrieveMap(QueryRef queryRef) {
}
}

private static void updateRdfModel(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
final Model[] modelRef = new Model[1];
QueryAccess qa = new QueryAccess() {
@Override
protected void processHeader(String[] line) {}
@Override
protected void processLine(String[] line) {}
@Override
protected void processRdfContent(Model model) {
modelRef[0] = model;
}
};
qa.call(queryRef);
if (modelRef[0] == null) {
throw new FailedApiCallException(new Exception("No RDF content in response for query: " + queryRef.getQueryId()));
}
String cacheId = queryRef.getAsUrlString();
logger.info("Updating cached RDF model for {}", cacheId);
cachedRdfModels.put(cacheId, modelRef[0]);
lastRefresh.put(cacheId, System.currentTimeMillis());
}

/**
* Retrieves a cached RDF model for a CONSTRUCT query, triggering a background fetch if needed.
*
* @param queryRef The QueryRef for the CONSTRUCT query.
* @return The cached RDF Model, or null if not yet available.
*/
public static Model retrieveRdfModelAsync(QueryRef queryRef) {
long timeNow = System.currentTimeMillis();
String cacheId = queryRef.getAsUrlString();
logger.info("Retrieving cached RDF model asynchronously for {}", cacheId);
boolean isCached = false;
boolean needsRefresh = true;
if (cachedRdfModels.containsKey(cacheId) && cachedRdfModels.get(cacheId) != null) {
long cacheAge = timeNow - lastRefresh.get(cacheId);
isCached = cacheAge < 24 * 60 * 60 * 1000;
needsRefresh = cacheAge > 60 * 1000;
}
if (failed.get(cacheId) != null && failed.get(cacheId) > 2) {
failed.remove(cacheId);
throw new RuntimeException("Query failed: " + cacheId);
}
if (needsRefresh && !isRunning(cacheId)) {
refreshStart.put(cacheId, timeNow);
new Thread(() -> {
try {
if (runAfter.containsKey(cacheId)) {
while (System.currentTimeMillis() < runAfter.get(cacheId)) {
Thread.sleep(100);
}
runAfter.remove(cacheId);
}
if (failed.get(cacheId) != null) {
Thread.sleep(1000);
}
Thread.sleep(100 + new Random().nextLong(400));
} catch (InterruptedException ex) {
logger.error("Interrupted while waiting to refresh RDF cache: {}", ex.getMessage());
}
try {
updateRdfModel(queryRef);
} catch (Exception ex) {
logger.error("Failed to update RDF cache for {}: {}", cacheId, ex.getMessage());
cachedRdfModels.remove(cacheId);
failed.merge(cacheId, 1, Integer::sum);
lastRefresh.put(cacheId, System.currentTimeMillis());
} finally {
refreshStart.remove(cacheId);
}
}).start();
}
if (isCached) {
return cachedRdfModels.get(cacheId);
} else {
return null;
}
}

/**
* Clears the cached response for a specific query reference and sets a delay before the next refresh can occur.
*
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/GrlcQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.query.algebra.helpers.AbstractSimpleQueryModelVisitor;
import org.eclipse.rdf4j.query.parser.ParsedGraphQuery;
import org.eclipse.rdf4j.query.parser.ParsedQuery;
import org.eclipse.rdf4j.query.parser.sparql.SPARQLParser;
import org.nanopub.Nanopub;
Expand Down Expand Up @@ -85,6 +86,7 @@ public static GrlcQuery get(String id) {
private String label;
private String description;
private final List<String> placeholdersList;
private boolean constructQuery;

/**
* Constructs a GrlcQuery object by parsing the provided query ID or URI.
Expand Down Expand Up @@ -139,6 +141,7 @@ private GrlcQuery(String id) {

final Set<String> placeholders = new HashSet<>();
ParsedQuery query = new SPARQLParser().parseQuery(sparql, null);
constructQuery = query instanceof ParsedGraphQuery;
try {
query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<Exception>() {

Expand Down Expand Up @@ -249,6 +252,15 @@ public List<String> getPlaceholdersList() {
return placeholdersList;
}

/**
* Returns true if this is a CONSTRUCT query (returns RDF graph data instead of tabular data).
*
* @return true if CONSTRUCT query
*/
public boolean isConstructQuery() {
return constructQuery;
}

/**
* Creates a list of query parameter fields for the placeholders in the query.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html xmlns:wicket="http://wicket.apache.org">

<head>
<link rel="stylesheet" href="../../../../../webapp/style.css?for-local-testing-only" type="text/css" media="screen"
title="Stylesheet"/>
</head>
<body>

<wicket:panel>
<table wicket:id="table"></table>
</wicket:panel>

</body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.knowledgepixels.nanodash.component;

import org.apache.wicket.extensions.ajax.markup.html.repeater.data.table.AjaxNavigationToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
import org.apache.wicket.extensions.markup.html.repeater.data.table.DataTable;
import org.apache.wicket.extensions.markup.html.repeater.data.table.HeadersToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.table.NoRecordsToolbar;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.ISortState;
import org.apache.wicket.extensions.markup.html.repeater.data.table.ISortableDataProvider;
import org.apache.wicket.extensions.markup.html.repeater.util.SingleSortState;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.eclipse.rdf4j.model.IRI;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
* Component for displaying CONSTRUCT query results as a subject/predicate/object table.
*/
public class QueryResultRdf extends Panel {

public QueryResultRdf(String id, org.eclipse.rdf4j.model.Model rdfModel) {
super(id);

List<String[]> rows = new ArrayList<>();
for (org.eclipse.rdf4j.model.Statement st : rdfModel) {
rows.add(new String[]{
st.getSubject().stringValue(),
st.getPredicate().stringValue(),
st.getObject().stringValue()
});
}

List<AbstractColumn<String[], String>> columns = new ArrayList<>();
columns.add(new TripleColumn("Subject", 0));
columns.add(new TripleColumn("Predicate", 1));
columns.add(new TripleColumn("Object", 2));

DataTable<String[], String> table = new DataTable<>("table", columns, new TripleDataProvider(rows), 20);
table.addBottomToolbar(new AjaxNavigationToolbar(table));
table.addBottomToolbar(new NoRecordsToolbar(table));
table.addTopToolbar(new HeadersToolbar<>(table, null));
add(table);
}

private static class TripleDataProvider implements ISortableDataProvider<String[], String> {

private final List<String[]> rows;
private final SingleSortState<String> sortState = new SingleSortState<>();

TripleDataProvider(List<String[]> rows) {
this.rows = rows;
}

@Override
public Iterator<String[]> iterator(long first, long count) {
int f = (int) first;
int t = (int) Math.min(first + count, rows.size());
return rows.subList(f, t).iterator();
}

@Override
public long size() {
return rows.size();
}

@Override
public IModel<String[]> model(String[] object) {
return Model.of(object);
}

@Override
public ISortState<String> getSortState() {
return sortState;
}

@Override
public void detach() {
}

}

private static class TripleColumn extends AbstractColumn<String[], String> {

private final int index;

TripleColumn(String title, int index) {
super(new Model<>(title));
this.index = index;
}

@Override
public void populateItem(Item<ICellPopulator<String[]>> cellItem, String componentId, IModel<String[]> rowModel) {
String value = rowModel.getObject()[index];
if (value.matches("https?://.+")) {
cellItem.add(new NanodashLink(componentId, value));
} else {
cellItem.add(new Label(componentId, value));
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.knowledgepixels.nanodash.component;

import com.knowledgepixels.nanodash.ApiCache;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
import org.eclipse.rdf4j.model.Model;
import org.nanopub.extra.services.QueryRef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A component that asynchronously loads and displays the RDF result of a CONSTRUCT query.
*/
public abstract class RdfResultComponent extends ResultComponent {

private final QueryRef queryRef;
private Model model = null;
private static final Logger logger = LoggerFactory.getLogger(RdfResultComponent.class);

/**
* Constructor.
*
* @param id the component id
* @param queryRef the QueryRef for the CONSTRUCT query
*/
public RdfResultComponent(String id, QueryRef queryRef) {
super(id);
this.queryRef = queryRef;
}

@Override
public Component getLazyLoadComponent(String markupId) {
while (true) {
if (!ApiCache.isRunning(queryRef)) {
try {
model = ApiCache.retrieveRdfModelAsync(queryRef);
if (model != null) break;
} catch (Exception ex) {
return new Label(markupId, "<span class=\"negative\">API call failed.</span>").setEscapeModelStrings(false);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
logger.error("Interrupted while waiting for RDF response", ex);
}
}
return getRdfResultComponent(markupId, model);
}

@Override
protected boolean isContentReady() {
return model != null || !ApiCache.isRunning(queryRef);
}

/**
* Implement to return the component that displays the RDF result.
*
* @param markupId the markup ID for the component
* @param model the RDF model from the CONSTRUCT query
* @return a Component that displays the RDF model
*/
public abstract Component getRdfResultComponent(String markupId, Model model);

}
18 changes: 18 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/page/QueryPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
import com.github.jsonldjava.shaded.com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.knowledgepixels.nanodash.ApiCache;
import com.knowledgepixels.nanodash.GrlcQuery;
import com.knowledgepixels.nanodash.Utils;
import com.knowledgepixels.nanodash.ViewDisplay;
import com.knowledgepixels.nanodash.component.QueryParamField;
import com.knowledgepixels.nanodash.component.QueryResultRdf;
import com.knowledgepixels.nanodash.component.QueryResultTableBuilder;
import com.knowledgepixels.nanodash.component.RdfResultComponent;
import com.knowledgepixels.nanodash.component.TitleBar;
import org.apache.wicket.Component;
import org.eclipse.rdf4j.model.Model;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.markup.html.WebMarkupContainer;
Expand Down Expand Up @@ -174,6 +179,19 @@ public void onSubmit() {

if (queryId == null) {
add(new Label("resulttable").setVisible(false));
} else if (q.isConstructQuery()) {
QueryRef constructQueryRef = new QueryRef(queryId, queryParams);
Model rdfModel = ApiCache.retrieveRdfModelAsync(constructQueryRef);
if (rdfModel != null) {
add(new QueryResultRdf("resulttable", rdfModel));
} else {
add(new RdfResultComponent("resulttable", constructQueryRef) {
@Override
public Component getRdfResultComponent(String markupId, Model model) {
return new QueryResultRdf(markupId, model);
}
});
}
} else {
add(QueryResultTableBuilder.create("resulttable", new QueryRef(queryId, queryParams), new ViewDisplay(20)).plain(true).build());
}
Expand Down