Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,60 @@ public String updateMailSettings(@RequestParam Map<String, String> allParams, Ht
return "Mail settings updated";
}

/**
* Triplestore endpoints and auth mode from merged config ({@code data/config.local.json} overrides
* {@code src/main/resources/config.json} via {@link ConfigUtil#get(String)}).
*/
@Operation(summary = "Get database endpoints", description = "Returns sparqlEndpoint, graphStoreEndpoint, and triplestoreAuth from merged config.")
@GetMapping(value = "/admin/database", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String getDatabaseConfig() throws IOException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode out = mapper.createObjectNode();
JsonNode sparqlEndpoint = ConfigUtil.get("sparqlEndpoint");
JsonNode graphStoreEndpoint = ConfigUtil.get("graphStoreEndpoint");
JsonNode triplestoreAuth = ConfigUtil.get("triplestoreAuth");
out.put("sparqlEndpoint", sparqlEndpoint != null && !sparqlEndpoint.isNull()
? sparqlEndpoint.asText("") : "");
out.put("graphStoreEndpoint", graphStoreEndpoint != null && !graphStoreEndpoint.isNull()
? graphStoreEndpoint.asText("") : "");
String auth = triplestoreAuth != null && !triplestoreAuth.isNull()
? triplestoreAuth.asText("digest") : "digest";
if (!"basic".equalsIgnoreCase(auth) && !"digest".equalsIgnoreCase(auth)) {
auth = "digest";
}
out.put("triplestoreAuth", auth.toLowerCase());
return mapper.writeValueAsString(out);
}

/**
* Persists {@code sparqlEndpoint}, {@code graphStoreEndpoint}, and {@code triplestoreAuth}
* to {@code data/config.local.json}.
*/
@Operation(summary = "Update database endpoints", description = "Persists sparqlEndpoint, graphStoreEndpoint, and triplestoreAuth to config.local.json.")
@PostMapping(value = "/admin/database")
@ResponseBody
public String updateDatabaseConfig(@RequestParam Map<String, String> allParams) throws IOException {
String sparqlEndpoint = allParams.getOrDefault("sparqlEndpoint", "");
String graphStoreEndpoint = allParams.getOrDefault("graphStoreEndpoint", "");
String triplestoreAuth = allParams.getOrDefault("triplestoreAuth", "digest").trim().toLowerCase();
if (!"basic".equals(triplestoreAuth) && !"digest".equals(triplestoreAuth)) {
return "Invalid triplestoreAuth; use basic or digest";
}

ObjectNode local = (ObjectNode) ConfigUtil.getLocaljson();
local.put("sparqlEndpoint", sparqlEndpoint);
local.put("graphStoreEndpoint", graphStoreEndpoint);
local.put("triplestoreAuth", triplestoreAuth);

ObjectMapper mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter()
.writeValue(new File("data/config.local.json"), local);
ConfigUtil.refreshLocalJson();

return "Database configuration updated";
}

//TODO: get admin plugins needs to be public, post admin plugins need to be admin only
@Operation(summary = "Get plugins", description = "Returns all configured plugins.")
@PreAuthorize("permitAll()")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Map;

@Tag(name = "Submissions", description = "Endpoints for creating and submitting registry objects")
Expand All @@ -36,7 +37,7 @@ public class SubmitController {
@PreAuthorize("hasAnyAuthority('USER', 'CURATOR', 'ADMIN')")
public ResponseEntity<String> submit(
@ModelAttribute SubmitPayload allParams,
@RequestPart(value = "file", required = false) MultipartFile file) throws IOException, SBOLValidationException {
@RequestPart(value = "file", required = false) MultipartFile file) throws IOException, SBOLValidationException, URISyntaxException {
return submitService.submit(allParams, file);
}

Expand Down
17 changes: 9 additions & 8 deletions backend/src/main/java/com/synbiohub/sbh3/dao/SparqlService.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -32,28 +33,28 @@ public class SparqlService {
* Virtuoso DELETE templates return one row per batch; loop until nothing remains.
* Legacy {@code sparql.deleteStaggered}.
*/
public void deleteCollection(Map<String, String> params, String graphUri) throws IOException {
public void deleteCollection(Map<String, String> params, String graphUri) throws IOException, URISyntaxException {
deleteStaggered(sparqlRepository.REMOVE_COLLECTION_SPARQL, params, graphUri);
}

public void delete(Map<String, String> params, String graphUri) throws IOException {
public void delete(Map<String, String> params, String graphUri) throws IOException, URISyntaxException {
deleteStaggered(sparqlRepository.REMOVE_SPARQL, params, graphUri);
}

public void uploadGraphStore(String graphUri, Path file) throws IOException {
public void uploadGraphStore(String graphUri, Path file) throws IOException, URISyntaxException {
sparqlRepository.save(graphUri, file);
}

public void uploadAttachment(Map<String, String> params, String graphUri) throws IOException {
public void uploadAttachment(Map<String, String> params, String graphUri) throws IOException, URISyntaxException {
String query = new SPARQLQuery(sparqlRepository.ATTACHMENT_UPDATE_SPARQL).loadTemplate(params);
sparqlRepository.update(query, graphUri, false);
}

public void update(String query, String graphUri, boolean jsonResults) throws IOException {
public void update(String query, String graphUri, boolean jsonResults) throws IOException, URISyntaxException {
sparqlRepository.update(query, graphUri, jsonResults);
}

private void deleteStaggered(String queryTemplate, Map<String, String> params, String graphUri) throws IOException {
private void deleteStaggered(String queryTemplate, Map<String, String> params, String graphUri) throws IOException, URISyntaxException {
String query = new SPARQLQuery(queryTemplate).loadTemplate(params);
while (true) {
String raw = sparqlRepository.update(query, graphUri, true);
Expand Down Expand Up @@ -90,7 +91,7 @@ public Map<String, String> loadAttachmentSources(String collectionUri, String gr
return sources;
}

public void attachUpload(Map<String, String> params, String graphUri, boolean jsonResults) throws IOException {
public void attachUpload(Map<String, String> params, String graphUri, boolean jsonResults) throws IOException, URISyntaxException {
String query = new SPARQLQuery(sparqlRepository.ATTACH_UPLOAD_SPARQL).loadTemplate(params);
update(query, graphUri, false);
}
Expand All @@ -99,7 +100,7 @@ public void attachUpload(Map<String, String> params, String graphUri, boolean js
* Replaces hash/size on an existing attachment when re-uploading the same {@code file:} source.
*/
public void updateAttachment(String graphUri, String attachmentUri, String uploadHash, long size)
throws IOException {
throws IOException, URISyntaxException {
String query = new SPARQLQuery(sparqlRepository.UPDATE_ATTACHMENT_SPARQL).loadTemplate(Map.of(
"attachmentURI", attachmentUri,
"attachmentSource", attachmentUri + "/download",
Expand Down
121 changes: 83 additions & 38 deletions backend/src/main/java/com/synbiohub/sbh3/repo/SparqlRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
import com.synbiohub.sbh3.utils.ConfigUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.ContextBuilder;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand All @@ -21,6 +26,8 @@
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
Expand All @@ -45,6 +52,9 @@ public class SparqlRepository {
public static final String SHARED_VIEW_SPARQL = "src/main/java/com/synbiohub/sbh3/sparql/GetSharedCanView.sparql";
public static final String TOPLEVEL_METADATA_SPARQL = "src/main/java/com/synbiohub/sbh3/sparql/GetTopLevelMetadata.sparql";

private static final String AUTH_BASIC = "basic";
private static final String AUTH_DIGEST = "digest";

private final RestClient restClient;


Expand Down Expand Up @@ -85,11 +95,12 @@ public <T> ResponseEntity<T> postJson(String uri, Object requestBody, Class<T> r
}

/**
* POST a SPARQL update to sparql-auth with digest auth (not preemptive basic auth).
* POST a SPARQL update to sparql-auth.
* Auth mode is {@code triplestoreAuth}: {@code digest} (Virtuoso) or {@code basic} (sbol-db).
*
* @param jsonResults when {@code true}, requests {@code application/sparql-results+json}
*/
public String update(String query, String graphUri, boolean jsonResults) throws IOException {
public String update(String query, String graphUri, boolean jsonResults) throws IOException, URISyntaxException {
StringBuilder url = new StringBuilder(sparqlAuthEndpoint());
url.append("?query=").append(URLEncoder.encode(query, StandardCharsets.UTF_8));
url.append("&default-graph-uri=").append(URLEncoder.encode(graphUri, StandardCharsets.UTF_8));
Expand All @@ -98,43 +109,40 @@ public String update(String query, String graphUri, boolean jsonResults) throws
.append(URLEncoder.encode("application/sparql-results+json", StandardCharsets.UTF_8));
}

try (CloseableHttpClient client = virtuosoDigestClient()) {
HttpPost post = new HttpPost(url.toString());
return client.execute(post, response -> {
int code = response.getCode();
if (code >= 300) {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY,
"SPARQL update failed (" + code + "): " + readResponseBody(response));
}
return readResponseBody(response);
});
}
HttpPost post = new HttpPost(url.toString());
return executeAuthenticated(post, response -> {
int code = response.getCode();
if (code >= 300) {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY,
"SPARQL update failed (" + code + "): " + readResponseBody(response));
}
return readResponseBody(response);
});
}

/**
* POST RDF/XML to the Virtuoso graph store. Uses digest auth (not preemptive basic auth),
* matching legacy {@code sparql.uploadSmallFile}.
* POST RDF/XML to the graph store endpoint.
* Auth mode is {@code triplestoreAuth}: {@code digest} (Virtuoso) or {@code basic} (sbol-db).
*/
public void save(String graphUri, Path file) throws IOException {
public void save(String graphUri, Path file) throws IOException, URISyntaxException {
String endpoint = ConfigUtil.get("graphStoreEndpoint").asText();
String url = endpoint
+ (endpoint.contains("?") ? "&" : "?")
+ "graph-uri=" + URLEncoder.encode(graphUri, StandardCharsets.UTF_8);

byte[] body = Files.readAllBytes(file);
try (CloseableHttpClient client = virtuosoDigestClient()) {
HttpPost post = new HttpPost(url);
post.setHeader(HttpHeaders.CONTENT_TYPE, "application/rdf+xml");
post.setEntity(new ByteArrayEntity(body, ContentType.APPLICATION_XML));
client.execute(post, response -> {
int code = response.getCode();
if (code >= 300) {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY,
"Graph store upload failed (" + code + "): " + readResponseBody(response));
}
return null;
});
}
HttpPost post = new HttpPost(url);
post.setHeader(HttpHeaders.CONTENT_TYPE, "application/rdf+xml");
post.setEntity(new ByteArrayEntity(body, ContentType.APPLICATION_XML));
executeAuthenticated(post, response -> {
int code = response.getCode();
if (code >= 300) {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY,
"Graph store upload failed (" + code + "): " + readResponseBody(response));
}
EntityUtils.consume(response.getEntity());
return null;
});
}

private String sparqlAuthEndpoint() throws IOException {
Expand All @@ -149,17 +157,54 @@ private String sparqlAuthEndpoint() throws IOException {
return base.replaceAll("/sparql/?$", "/sparql-auth");
}

/** HttpClient configured for Virtuoso digest auth (waits for 401 challenge). */
private static CloseableHttpClient virtuosoDigestClient() throws IOException {
/**
* Resolves {@code triplestoreAuth} from config ({@code basic} or {@code digest}; default {@code digest}).
*/
private static String resolveTriplestoreAuth() throws IOException {
JsonNode configured = ConfigUtil.get("triplestoreAuth");
if (configured == null || configured.isNull()) {
return AUTH_DIGEST;
}
String value = configured.asText("").trim().toLowerCase();
if (AUTH_BASIC.equals(value)) {
return AUTH_BASIC;
}
return AUTH_DIGEST;
}

/**
* Authenticated POST for triplestore write endpoints.
* <ul>
* <li>{@code basic} — preemptive Basic auth for sbol-db (avoids 401 + large-body disconnects).</li>
* <li>{@code digest} — challenge Digest auth for Virtuoso (legacy behavior).</li>
* </ul>
*/
private <T> T executeAuthenticated(
HttpPost post,
HttpClientResponseHandler<T> handler) throws IOException, URISyntaxException {
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
ConfigUtil.get("username").asText(),
ConfigUtil.get("password").asText().toCharArray());

BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope(null, -1),
new UsernamePasswordCredentials(
ConfigUtil.get("username").asText(),
ConfigUtil.get("password").asText().toCharArray()));
return HttpClients.custom()
credsProvider.setCredentials(new AuthScope(null, -1), credentials);

String authMode = resolveTriplestoreAuth();
try (CloseableHttpClient client = HttpClients.custom()
.setDefaultCredentialsProvider(credsProvider)
.build();
.build()) {
if (AUTH_BASIC.equals(authMode)) {
URI uri = post.getUri();
HttpHost target = HttpHost.create(uri);
HttpClientContext context = ContextBuilder.create()
.useCredentialsProvider(credsProvider)
.preemptiveBasicAuth(target, credentials)
.build();
return client.execute(target, post, context, handler);
}
// Digest (Virtuoso): wait for WWW-Authenticate challenge, then retry with Digest.
return client.execute(post, handler);
}
}

private String readResponseBody(org.apache.hc.core5.http.ClassicHttpResponse response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
Expand All @@ -36,7 +37,7 @@ public class AttachmentService {
* For each non-SBOL attachment: gzip+hash to {@code ./uploads/}, insert or update triples,
* then rewrite {@code file:filename} placeholders to real attachment URIs.
*/
public void uploadAttachments(SubmitPayload payload, String graphUri) throws IOException {
public void uploadAttachments(SubmitPayload payload, String graphUri) throws IOException, URISyntaxException {
String collectionUri = payload.getCollectionUri();
String baseUri = attachmentBaseUri(payload);
Map<String, String> existingSources = sparqlService.loadAttachmentSources(collectionUri, graphUri);
Expand Down Expand Up @@ -86,7 +87,7 @@ private String attachmentBaseUri(SubmitPayload payload) throws IOException {
/** Inserts attachment triples and links them to the root collection ({@code AttachUpload.sparql}). */
private String addAttachmentToTopLevel(String graphUri, String baseUri, String topLevelUri,
String name, String uploadHash, long size, String attachmentType,
String owner) throws IOException {
String owner) throws IOException, URISyntaxException {
String displayId = "attachment_" + UUID.randomUUID().toString().replace("-", "");
String persistentIdentity = baseUri + "/" + displayId;
String version = "1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ private SBOLDocument readModelAsSbolDocument(Model model) {
if (model == null || model.isEmpty()) {
return null;
}
// Drop invalid xmlns (e.g. sbol-db's its=…/its) that fail SBOL sbol-10106.
applyLegacySynbiohubRdfXmlPrefixes(model);
var modelOutput = new ByteArrayOutputStream();
RDFDataMgr.write(modelOutput, model, RDFFormat.RDFXML_PLAIN);
try {
Expand Down
Loading
Loading