From f03bb4ce00fe5d32c5467c5e2402417be3162ec9 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Wed, 3 Jun 2026 15:39:15 +0100 Subject: [PATCH 01/15] experimental local upload handling added --- .../org/javastro/ivoa/tap/QueryResource.java | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 188b2c4..b1f584b 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -10,11 +10,9 @@ import io.smallrye.mutiny.infrastructure.Infrastructure; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; +import jakarta.ws.rs.*; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -25,10 +23,17 @@ import org.javastro.ivoacore.uws.UWSException; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.resteasy.reactive.server.multipart.FormValue; +import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; import uk.ac.starlink.table.ColumnInfo; import uk.ac.starlink.table.RowListStarTable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.time.Duration; +import java.util.Optional; +import java.util.regex.Pattern; /** * Main TAP Query. @@ -48,6 +53,8 @@ public class QueryResource { @Inject TAPHelper tapHelper; + private static final Pattern FORMAT_PATTERN = Pattern.compile("^[^,:]+,param:[^,:]+$"); + @GET @Produces("application/x-votable+xml") public Uni syncGet(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @@ -57,10 +64,33 @@ public Uni syncGet(@RestQuery String query, @RestQuery Strin } @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces("application/x-votable+xml") public Uni syncPost(@RestForm("QUERY") String query, @RestForm("LANG") String lang, @RestForm("RESPONSEFORMAT") String responseformat, @RestForm("MAXREC") Long maxrec, @RestForm("RUNID") String runid, @RestForm("UPLOAD") String upload, + MultipartFormDataInput input, @Context UriInfo uriInfo) { + + if (isValidUploadParam(upload)) { + String[] parts = upload.split("[,:]"); + String tableName = parts[0]; + String tableParam = parts[2]; + + Optional value = + Optional.ofNullable(input.getValues().get(tableParam)) + .flatMap(list -> list.stream().findFirst()); + + if (value.isPresent() && value.get().isFileItem()) { + java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); + + try (InputStream in = Files.newInputStream(uploadedFile)) { + // Parse VOTable + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + return handleJob(query, lang, responseformat, maxrec, runid, upload, uriInfo); } @@ -144,5 +174,7 @@ protected java.nio.file.Path buildErrorVOTable(TAPJob job, UWSException exceptio } } - + private static boolean isValidUploadParam(String input) { + return input != null && FORMAT_PATTERN.matcher(input).matches(); + } } From 7c15ab88849c47a5507fa98cf93bd0f6ba253086 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Thu, 4 Jun 2026 16:15:43 +0100 Subject: [PATCH 02/15] some separation of upload param types added so they can be handled differently if required). --- .../org/javastro/ivoa/tap/QueryResource.java | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index b1f584b..2c1ddd1 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -25,11 +25,16 @@ import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.server.multipart.FormValue; import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; -import uk.ac.starlink.table.ColumnInfo; -import uk.ac.starlink.table.RowListStarTable; +import uk.ac.starlink.table.*; +import uk.ac.starlink.votable.VOTableBuilder; +import uk.ac.starlink.votable.VOTableWriter; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.time.Duration; import java.util.Optional; @@ -53,7 +58,8 @@ public class QueryResource { @Inject TAPHelper tapHelper; - private static final Pattern FORMAT_PATTERN = Pattern.compile("^[^,:]+,param:[^,:]+$"); + private static final Pattern UPLOAD_PATTERN = Pattern.compile("^[^,:]+,[a-zA-Z][a-zA-Z0-9+.-]*:.+$"); + @GET @Produces("application/x-votable+xml") @@ -63,6 +69,11 @@ public Uni syncGet(@RestQuery String query, @RestQuery Strin return handleJob(query, lang, responseformat, maxrec, runid, upload, uriInfo); } + + //UPLOAD param details - https://www.ivoa.net/documents/DALI/20170517/REC-DALI-1.1.html#tth_sEc3.4.5 + //UPLOAD=table1,http://example.com/t1.xml + //UPLOAD=image1,vos://example.authority!tempSpace/foo.fits + //UPLOAD=table3,param:t3 @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces("application/x-votable+xml") @@ -72,21 +83,52 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo @Context UriInfo uriInfo) { if (isValidUploadParam(upload)) { - String[] parts = upload.split("[,:]"); + String[] parts = upload.split(","); String tableName = parts[0]; - String tableParam = parts[2]; - - Optional value = - Optional.ofNullable(input.getValues().get(tableParam)) - .flatMap(list -> list.stream().findFirst()); - - if (value.isPresent() && value.get().isFileItem()) { - java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); - - try (InputStream in = Files.newInputStream(uploadedFile)) { - // Parse VOTable - } catch (IOException e) { - throw new RuntimeException(e); + String uploadParam = parts[1]; + + if (uploadParam.startsWith("param:")) { + String paramName = uploadParam.split(":")[1]; + + Optional value = + Optional.ofNullable(input.getValues().get(paramName)) + .flatMap(list -> list.stream().findFirst()); + + if (value.isPresent() && value.get().isFileItem()) { + java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); + + try (InputStream in = Files.newInputStream(uploadedFile)) { + + StarTable t = new StarTableFactory().makeStarTable(in, new VOTableBuilder()); + + //test output + StarTableWriter writer = new VOTableWriter(); + // Alternatives: + // StarTableWriter writer = new uk.ac.starlink.table.formats.CsvTableWriter(); + // StarTableWriter writer = new uk.ac.starlink.table.formats.TextTableWriter(); + + // 3. Pipe the stream into a ByteArrayOutputStream + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (os) { + writer.writeStarTable(t, os); + } + + // 4. Convert the byte stream to a String + String tableString = os.toString(StandardCharsets.UTF_8); + + // Now you can print it or use it as needed + System.out.println(tableString); + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + //URI located VOTable + switch (URI.create(uploadParam).getScheme()) { + case "http": + case "https": + case "file": + case "vos"://might need handling differently to explicit https + } } } } @@ -175,6 +217,6 @@ protected java.nio.file.Path buildErrorVOTable(TAPJob job, UWSException exceptio } private static boolean isValidUploadParam(String input) { - return input != null && FORMAT_PATTERN.matcher(input).matches(); + return input != null && UPLOAD_PATTERN.matcher(input).matches(); } } From bfbbfb0cdf000b63edfa96a0edaf1dd67af2c663 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Thu, 4 Jun 2026 17:21:06 +0100 Subject: [PATCH 03/15] Separated out param: handling (reading file) --- .../org/javastro/ivoa/tap/QueryResource.java | 86 ++++++++++--------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 2c1ddd1..726eae0 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -88,47 +88,14 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo String uploadParam = parts[1]; if (uploadParam.startsWith("param:")) { - String paramName = uploadParam.split(":")[1]; - - Optional value = - Optional.ofNullable(input.getValues().get(paramName)) - .flatMap(list -> list.stream().findFirst()); - - if (value.isPresent() && value.get().isFileItem()) { - java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); - - try (InputStream in = Files.newInputStream(uploadedFile)) { - - StarTable t = new StarTableFactory().makeStarTable(in, new VOTableBuilder()); - - //test output - StarTableWriter writer = new VOTableWriter(); - // Alternatives: - // StarTableWriter writer = new uk.ac.starlink.table.formats.CsvTableWriter(); - // StarTableWriter writer = new uk.ac.starlink.table.formats.TextTableWriter(); - - // 3. Pipe the stream into a ByteArrayOutputStream - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try (os) { - writer.writeStarTable(t, os); - } - - // 4. Convert the byte stream to a String - String tableString = os.toString(StandardCharsets.UTF_8); - - // Now you can print it or use it as needed - System.out.println(tableString); - } catch (IOException e) { - throw new RuntimeException(e); - } - } else { - //URI located VOTable - switch (URI.create(uploadParam).getScheme()) { - case "http": - case "https": - case "file": - case "vos"://might need handling differently to explicit https - } + storeVOTable(uploadParam, input); + } else { + //URI located VOTable - probably only need to handle the param: version as https: etc will be handled in the actual job + switch (URI.create(uploadParam).getScheme()) { + case "http": + case "https": + case "file": + case "vos"://might need handling differently to explicit https } } } @@ -219,4 +186,41 @@ protected java.nio.file.Path buildErrorVOTable(TAPJob job, UWSException exceptio private static boolean isValidUploadParam(String input) { return input != null && UPLOAD_PATTERN.matcher(input).matches(); } + + private void storeVOTable(String uploadParam, MultipartFormDataInput input){ + String paramName = uploadParam.split(":")[1]; + + Optional value = + Optional.ofNullable(input.getValues().get(paramName)) + .flatMap(list -> list.stream().findFirst()); + + if (value.isPresent() && value.get().isFileItem()) { + java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); + + try (InputStream in = Files.newInputStream(uploadedFile)) { + + StarTable t = new StarTableFactory().makeStarTable(in, new VOTableBuilder()); + + //test output + StarTableWriter writer = new VOTableWriter(); + // Alternatives: + // StarTableWriter writer = new uk.ac.starlink.table.formats.CsvTableWriter(); + // StarTableWriter writer = new uk.ac.starlink.table.formats.TextTableWriter(); + + // 3. Pipe the stream into a ByteArrayOutputStream + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (os) { + writer.writeStarTable(t, os); + } + + // 4. Convert the byte stream to a String + String tableString = os.toString(StandardCharsets.UTF_8); + + // Now you can print it or use it as needed + System.out.println(tableString); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } } From 1098cc21a98ed895eda3126d77fb84522102a96a Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Fri, 5 Jun 2026 11:28:46 +0100 Subject: [PATCH 04/15] removed test code added the functionality to copy the uploaded file to a /tmp file so that it's URI can be passed directly to the handleJob/TapJobSpecification processes --- .../org/javastro/ivoa/tap/QueryResource.java | 64 ++++++------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 726eae0..6c486c0 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -25,19 +25,17 @@ import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.server.multipart.FormValue; import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; +import org.jspecify.annotations.NonNull; import uk.ac.starlink.table.*; -import uk.ac.starlink.votable.VOTableBuilder; -import uk.ac.starlink.votable.VOTableWriter; -import java.io.ByteArrayOutputStream; + import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.time.Duration; import java.util.Optional; +import java.util.UUID; import java.util.regex.Pattern; /** @@ -82,26 +80,24 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo MultipartFormDataInput input, @Context UriInfo uriInfo) { + // If there's a "~,param:~~" upload parameter supplied then upload the file to tmp and create a file: URI to it. if (isValidUploadParam(upload)) { String[] parts = upload.split(","); - String tableName = parts[0]; String uploadParam = parts[1]; if (uploadParam.startsWith("param:")) { - storeVOTable(uploadParam, input); - } else { - //URI located VOTable - probably only need to handle the param: version as https: etc will be handled in the actual job - switch (URI.create(uploadParam).getScheme()) { - case "http": - case "https": - case "file": - case "vos"://might need handling differently to explicit https - } + try { + URI fileUri = storeVOTable(uploadParam, input); + if (fileUri != null){ + upload = fileUri.toString(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } } return handleJob(query, lang, responseformat, maxrec, runid, upload, uriInfo); - } @@ -187,40 +183,22 @@ private static boolean isValidUploadParam(String input) { return input != null && UPLOAD_PATTERN.matcher(input).matches(); } - private void storeVOTable(String uploadParam, MultipartFormDataInput input){ + private URI storeVOTable(@NonNull String uploadParam, @NonNull MultipartFormDataInput input) throws IOException { String paramName = uploadParam.split(":")[1]; - Optional value = - Optional.ofNullable(input.getValues().get(paramName)) - .flatMap(list -> list.stream().findFirst()); + Optional value = Optional.ofNullable(input.getValues().get(paramName)) + .flatMap(list -> list.stream().findFirst()); if (value.isPresent() && value.get().isFileItem()) { java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); - try (InputStream in = Files.newInputStream(uploadedFile)) { + UUID uuid = UUID.randomUUID(); + java.nio.file.Path persistent = Files.createTempFile("tap-upload-" + uuid, ".vot"); - StarTable t = new StarTableFactory().makeStarTable(in, new VOTableBuilder()); + Files.copy(uploadedFile, persistent, StandardCopyOption.REPLACE_EXISTING); - //test output - StarTableWriter writer = new VOTableWriter(); - // Alternatives: - // StarTableWriter writer = new uk.ac.starlink.table.formats.CsvTableWriter(); - // StarTableWriter writer = new uk.ac.starlink.table.formats.TextTableWriter(); - - // 3. Pipe the stream into a ByteArrayOutputStream - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try (os) { - writer.writeStarTable(t, os); - } - - // 4. Convert the byte stream to a String - String tableString = os.toString(StandardCharsets.UTF_8); - - // Now you can print it or use it as needed - System.out.println(tableString); - } catch (IOException e) { - throw new RuntimeException(e); - } + return persistent.toUri(); } + return null; } } From 37287673dee5acc2ea5bd16a0fff7c7d63f784bc Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Tue, 16 Jun 2026 12:07:26 +0100 Subject: [PATCH 05/15] Slight update to move the single upload form-data (at present) into a Map for the expected params for the TapJobSpecification --- .../org/javastro/ivoa/tap/AsyncQueryResource.java | 6 +++++- .../java/org/javastro/ivoa/tap/QueryResource.java | 15 +++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java index ccf25ab..d0dec07 100644 --- a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java @@ -20,6 +20,9 @@ import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestResponse; +import java.net.URI; +import java.util.Map; + /** * Main Async TAP Query. * Created on 04/03/2026 by Paul Harrison (paul.harrison@manchester.ac.uk). @@ -56,7 +59,8 @@ protected Response redirectToJob(String jobid) { public Response async(@RestForm("QUERY") String query, @RestForm("LANG") String lang, @RestForm("RESPONSEFORMAT") String responseformat, @RestForm("MAXREC") Long maxrec, @RestForm("RUNID") String runid, @RestForm("UPLOAD") String upload, @Context UriInfo uriInfo) throws UWSException { - BaseUWSJob job = tapHelper.jobmanager.createJob(new TAPJobSpecification(query,lang,responseformat,maxrec,runid,upload)); + Map uploadMap = null; //TODO handle post upload as with sync + BaseUWSJob job = tapHelper.jobmanager.createJob(new TAPJobSpecification(query,lang,responseformat,maxrec,runid,uploadMap)); return Response.seeOther(tapHelper.asyncJobUri(job.getID())).build(); } diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 6c486c0..8592cfd 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -34,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.Duration; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.regex.Pattern; @@ -64,7 +65,8 @@ public class QueryResource { public Uni syncGet(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @RestQuery String upload, @Context UriInfo uriInfo) { - return handleJob(query, lang, responseformat, maxrec, runid, upload, uriInfo); + Map uploadMap = null; //TODO - handle remote URIs only as there's no form-data with GET + return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo); } @@ -81,8 +83,10 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo @Context UriInfo uriInfo) { // If there's a "~,param:~~" upload parameter supplied then upload the file to tmp and create a file: URI to it. + String tableName = null; if (isValidUploadParam(upload)) { String[] parts = upload.split(","); + tableName = parts[0]; String uploadParam = parts[1]; if (uploadParam.startsWith("param:")) { @@ -96,18 +100,21 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo } } } + Map uploadMap = new java.util.HashMap<>(); + uploadMap.put(tableName, URI.create(upload)); - return handleJob(query, lang, responseformat, maxrec, runid, upload, uriInfo); + //TODO - REMOVE the temp file: once job has completed + return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo); } - private Uni handleJob(String query, String lang, String responseformat, Long maxrec, String runid, String upload, UriInfo uriInfo) { + private Uni handleJob(String query, String lang, String responseformat, Long maxrec, String runid, Map uploads, UriInfo uriInfo) { final Duration SYNC_WAIT = Duration.ofSeconds(syncTimeoutSeconds); return Uni.createFrom().deferred(() -> { final TAPJob job; try { job = (TAPJob) tapHelper.jobmanager.createJob( - new TAPJobSpecification(query, lang, responseformat, maxrec, runid, upload) + new TAPJobSpecification(query, lang, responseformat, maxrec, runid, uploads) ); tapHelper.jobmanager.runJob(job.getID()); // automatically run the job From a0ba2ccd20d03e83a53b0f18e67b0744ee471553 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Wed, 17 Jun 2026 09:47:39 +0100 Subject: [PATCH 06/15] Cleans up any temp uploaded files after the query has executed. --- .../org/javastro/ivoa/tap/QueryResource.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 8592cfd..e9ee11e 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -26,6 +26,8 @@ import org.jboss.resteasy.reactive.server.multipart.FormValue; import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import uk.ac.starlink.table.*; @@ -58,7 +60,7 @@ public class QueryResource { TAPHelper tapHelper; private static final Pattern UPLOAD_PATTERN = Pattern.compile("^[^,:]+,[a-zA-Z][a-zA-Z0-9+.-]*:.+$"); - + private static final Logger log = LoggerFactory.getLogger(QueryResource.class); @GET @Produces("application/x-votable+xml") @@ -82,7 +84,7 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo MultipartFormDataInput input, @Context UriInfo uriInfo) { - // If there's a "~,param:~~" upload parameter supplied then upload the file to tmp and create a file: URI to it. + // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. String tableName = null; if (isValidUploadParam(upload)) { String[] parts = upload.split(","); @@ -103,8 +105,19 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo Map uploadMap = new java.util.HashMap<>(); uploadMap.put(tableName, URI.create(upload)); - //TODO - REMOVE the temp file: once job has completed - return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo); + return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo) + .onTermination() + .invoke(() -> { + for (URI uploadedFile : uploadMap.values()) { + if ("file".equalsIgnoreCase(uploadedFile.getScheme())) { + try { + Files.deleteIfExists(java.nio.file.Path.of(uploadedFile)); + } catch (IOException e) { + log.warn("Failed to delete upload file {}", uploadedFile, e); + } + } + } + }); } From bc2a0e5e368e2108e0a8d28c98d3b86b46e2ccff Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Thu, 18 Jun 2026 14:43:10 +0100 Subject: [PATCH 07/15] Refactored the upload param slightly so that remote URLs are parsed (as well as the previously tested param: ) --- .../org/javastro/ivoa/tap/QueryResource.java | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index e9ee11e..a6537d2 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -84,26 +84,33 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo MultipartFormDataInput input, @Context UriInfo uriInfo) { - // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. - String tableName = null; - if (isValidUploadParam(upload)) { - String[] parts = upload.split(","); - tableName = parts[0]; - String uploadParam = parts[1]; - - if (uploadParam.startsWith("param:")) { - try { - URI fileUri = storeVOTable(uploadParam, input); - if (fileUri != null){ - upload = fileUri.toString(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + Map uploadMap = new java.util.HashMap<>(); + + if (upload != null) { + String[] uploadSpecs = upload.split(";"); + for (String uploadSpec : uploadSpecs) { + // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. + String[] parts = uploadSpec.split(","); + String tableName = parts[0]; + String tableLoc = parts[1]; //either param: or a URL to a remote file (http, https, vos, etc) + if (isValidUploadParam(uploadSpec)) { + if (tableLoc.startsWith("param:")) { + try { + URI fileUri = storeVOTable(tableLoc, input); + if (fileUri != null) { + uploadSpec = fileUri.toString(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + else { + uploadSpec = tableLoc; + } + } + uploadMap.put(tableName, URI.create(uploadSpec)); } } - Map uploadMap = new java.util.HashMap<>(); - uploadMap.put(tableName, URI.create(upload)); return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo) .onTermination() From 228c236c04e616285bd8b274d5f51666f6ffca9f Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Fri, 19 Jun 2026 14:18:58 +0100 Subject: [PATCH 08/15] Upload unit test placeholder and an example VOTable for testing only added --- .../java/org/javastro/ivoa/tap/UploadTest.java | 13 +++++++++++++ src/test/resources/uploads/example-votable.vot | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/test/java/org/javastro/ivoa/tap/UploadTest.java create mode 100644 src/test/resources/uploads/example-votable.vot diff --git a/src/test/java/org/javastro/ivoa/tap/UploadTest.java b/src/test/java/org/javastro/ivoa/tap/UploadTest.java new file mode 100644 index 0000000..9d7bbc0 --- /dev/null +++ b/src/test/java/org/javastro/ivoa/tap/UploadTest.java @@ -0,0 +1,13 @@ +package org.javastro.ivoa.tap; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class UploadTest { + + @Test + public void testUpload() { + + } +} diff --git a/src/test/resources/uploads/example-votable.vot b/src/test/resources/uploads/example-votable.vot new file mode 100644 index 0000000..1446243 --- /dev/null +++ b/src/test/resources/uploads/example-votable.vot @@ -0,0 +1,18 @@ + + + + + Stars + Some bright stars + + + + + + + + + +
Procyon114.8272425.227506
Vega279.23410638.782992
+
+
\ No newline at end of file From f41edfd97f45643456631394570009baa5234e48 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Fri, 19 Jun 2026 15:07:19 +0100 Subject: [PATCH 09/15] Added a link to a "permanent" remote VOTable to use in testing Added the ability to pass an upload table to a GET request (sync) --- .../java/org/javastro/ivoa/tap/QueryResource.java | 11 ++++++++++- src/test/java/org/javastro/ivoa/tap/UploadTest.java | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index a6537d2..04d9a55 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -67,7 +67,16 @@ public class QueryResource { public Uni syncGet(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @RestQuery String upload, @Context UriInfo uriInfo) { - Map uploadMap = null; //TODO - handle remote URIs only as there's no form-data with GET + Map uploadMap = new java.util.HashMap<>(); + if (upload != null) { + String[] uploadSpecs = upload.split(";"); + for (String uploadSpec : uploadSpecs) { + String[] parts = uploadSpec.split(","); + String tableName = parts[0]; + String tableLoc = parts[1]; + uploadMap.put(tableName, URI.create(tableLoc)); + } + } return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo); } diff --git a/src/test/java/org/javastro/ivoa/tap/UploadTest.java b/src/test/java/org/javastro/ivoa/tap/UploadTest.java index 9d7bbc0..f6a09bf 100644 --- a/src/test/java/org/javastro/ivoa/tap/UploadTest.java +++ b/src/test/java/org/javastro/ivoa/tap/UploadTest.java @@ -6,6 +6,8 @@ @QuarkusTest public class UploadTest { + //https://gist.githubusercontent.com/slloyd-src/7300663888d7e78994eab792ac232253/raw/4eb09a722c9afb6e72e0898d545283941ddd5881/gistfile1.txt + @Test public void testUpload() { From 42e7616d7dced00045d063151261b5dcecb66357 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Mon, 22 Jun 2026 12:09:24 +0100 Subject: [PATCH 10/15] Removed duplicate functionality. --- .../org/javastro/ivoa/tap/QueryResource.java | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 04d9a55..4f30c09 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -20,6 +20,7 @@ import org.javastro.ivoacore.tap.TAPJob; import org.javastro.ivoacore.tap.TAPJobSpecification; import org.javastro.ivoacore.tap.TAPWriter; +import org.javastro.ivoacore.tap.upload.TapUploadService; import org.javastro.ivoacore.uws.UWSException; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; @@ -39,7 +40,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.regex.Pattern; /** * Main TAP Query. @@ -59,7 +59,6 @@ public class QueryResource { @Inject TAPHelper tapHelper; - private static final Pattern UPLOAD_PATTERN = Pattern.compile("^[^,:]+,[a-zA-Z][a-zA-Z0-9+.-]*:.+$"); private static final Logger log = LoggerFactory.getLogger(QueryResource.class); @GET @@ -67,18 +66,8 @@ public class QueryResource { public Uni syncGet(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @RestQuery String upload, @Context UriInfo uriInfo) { - Map uploadMap = new java.util.HashMap<>(); - if (upload != null) { - String[] uploadSpecs = upload.split(";"); - for (String uploadSpec : uploadSpecs) { - String[] parts = uploadSpec.split(","); - String tableName = parts[0]; - String tableLoc = parts[1]; - uploadMap.put(tableName, URI.create(tableLoc)); - } - } + Map uploadMap = parseUploadParams(upload, null); return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo); - } //UPLOAD param details - https://www.ivoa.net/documents/DALI/20170517/REC-DALI-1.1.html#tth_sEc3.4.5 @@ -93,33 +82,7 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo MultipartFormDataInput input, @Context UriInfo uriInfo) { - Map uploadMap = new java.util.HashMap<>(); - - if (upload != null) { - String[] uploadSpecs = upload.split(";"); - for (String uploadSpec : uploadSpecs) { - // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. - String[] parts = uploadSpec.split(","); - String tableName = parts[0]; - String tableLoc = parts[1]; //either param: or a URL to a remote file (http, https, vos, etc) - if (isValidUploadParam(uploadSpec)) { - if (tableLoc.startsWith("param:")) { - try { - URI fileUri = storeVOTable(tableLoc, input); - if (fileUri != null) { - uploadSpec = fileUri.toString(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - else { - uploadSpec = tableLoc; - } - } - uploadMap.put(tableName, URI.create(uploadSpec)); - } - } + Map uploadMap = parseUploadParams(upload, input); return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo) .onTermination() @@ -215,10 +178,54 @@ protected java.nio.file.Path buildErrorVOTable(TAPJob job, UWSException exceptio } } - private static boolean isValidUploadParam(String input) { - return input != null && UPLOAD_PATTERN.matcher(input).matches(); + /** + * Parses the UPLOAD parameter from a DALI-compliant query and processes any file uploads or URLs present in it. + * + * @param uploadParam The UPLOAD parameter containing mappings of table names to data locations. + * Each mapping is provided in the format: tableName,dataLocation, where dataLocation could be a URL or a + * parameter indicating an uploaded file (e.g., param:uploadFile). + * @param input The multipart form data input containing uploaded file data, if any. + * @return A map where the keys are table names and the values are URIs pointing to corresponding data sources (e.g., temporary file URIs, remote URLs). + */ + private Map parseUploadParams(String uploadParam, MultipartFormDataInput input) { + Map uploadMap = new java.util.HashMap<>(); + + if (uploadParam != null) { + String[] uploadSpecs = uploadParam.split(";"); + for (String uploadSpec : uploadSpecs) { + // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. + String[] parts = uploadSpec.split(","); + String tableName = parts[0]; + String tableLoc = parts[1]; //either param: or a URL to a remote file (http, https, vos, etc) + if (TapUploadService.isValidUploadParam(uploadSpec)) { + if (tableLoc.startsWith("param:")) { + try { + URI fileUri = storeVOTable(tableLoc, input); + if (fileUri != null) { + uploadSpec = fileUri.toString(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + else { + uploadSpec = tableLoc; + } + } + uploadMap.put(tableName, URI.create(uploadSpec)); + } + } + return uploadMap; } + /** + * Stores a VOTable in a temporary file and returns the URI of the file. + * @param uploadParam parameter of the DALI UPLOAD query parameter, e.g. "table1,http://example.com/t1.xml", + * "image1,vos://example.authority!tempSpace/foo.fits", or "table3,param:t3" + * @param input The multipart form data input. + * @return The URI of the uploaded file, or null if the file was not uploaded. + * @throws IOException If an I/O error occurs while storing the file. + */ private URI storeVOTable(@NonNull String uploadParam, @NonNull MultipartFormDataInput input) throws IOException { String paramName = uploadParam.split(":")[1]; From dabfdf6b8154cca70341d516d9f98e00d7976c8a Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Mon, 22 Jun 2026 15:32:31 +0100 Subject: [PATCH 11/15] Moved some upload functionality into a shared class for sync/async processes Made sure the async endpoint now processes uploads (tested manually so far) --- .../javastro/ivoa/tap/AsyncQueryResource.java | 15 ++- .../org/javastro/ivoa/tap/QueryResource.java | 98 ++++--------------- .../javastro/ivoa/tap/upload/Utilities.java | 94 ++++++++++++++++++ 3 files changed, 122 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/javastro/ivoa/tap/upload/Utilities.java diff --git a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java index d0dec07..a8f3e62 100644 --- a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java @@ -11,6 +11,7 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.*; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.javastro.ivoa.tap.upload.Utilities; import org.javastro.ivoacore.common.ServiceLocator; import org.javastro.ivoacore.tap.TAPJobSpecification; import org.javastro.ivoacore.uws.BaseUWSJob; @@ -19,6 +20,7 @@ import org.javastro.ivoacore.uws.webapi.BaseUWSResource; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; import java.net.URI; import java.util.Map; @@ -32,8 +34,7 @@ @Path("async") public class AsyncQueryResource extends BaseUWSResource { - - @Inject + @Inject TAPHelper tapHelper; @Override @@ -53,13 +54,13 @@ protected Response redirectToJob(String jobid) { .build()).build(); } - - //IMPL the two query endpoints are in different resources for routing purposes. + //IMPL the two query endpoints are in different resources for routing purposes @POST public Response async(@RestForm("QUERY") String query, @RestForm("LANG") String lang, @RestForm("RESPONSEFORMAT") String responseformat, @RestForm("MAXREC") Long maxrec, @RestForm("RUNID") String runid, - @RestForm("UPLOAD") String upload, @Context UriInfo uriInfo) throws UWSException { - Map uploadMap = null; //TODO handle post upload as with sync + @RestForm("UPLOAD") String upload, MultipartFormDataInput input, @Context UriInfo uriInfo) throws UWSException { + + Map uploadMap = Utilities.parseUploadParams(upload, input); BaseUWSJob job = tapHelper.jobmanager.createJob(new TAPJobSpecification(query,lang,responseformat,maxrec,runid,uploadMap)); return Response.seeOther(tapHelper.asyncJobUri(job.getID())).build(); } @@ -73,6 +74,4 @@ public RestResponse getVotable(@PathParam("jobid") String jo .header(HttpHeaders.CONTENT_DISPOSITION, "result.vot") .build(); } - - } diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 4f30c09..7cb935d 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -17,16 +17,14 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.javastro.ivoa.entities.uws.ExecutionPhase; +import org.javastro.ivoa.tap.upload.Utilities; import org.javastro.ivoacore.tap.TAPJob; import org.javastro.ivoacore.tap.TAPJobSpecification; import org.javastro.ivoacore.tap.TAPWriter; -import org.javastro.ivoacore.tap.upload.TapUploadService; import org.javastro.ivoacore.uws.UWSException; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; -import org.jboss.resteasy.reactive.server.multipart.FormValue; import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; -import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.starlink.table.*; @@ -35,11 +33,8 @@ import java.io.IOException; import java.net.URI; import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.time.Duration; import java.util.Map; -import java.util.Optional; -import java.util.UUID; /** * Main TAP Query. @@ -66,8 +61,8 @@ public class QueryResource { public Uni syncGet(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @RestQuery String upload, @Context UriInfo uriInfo) { - Map uploadMap = parseUploadParams(upload, null); - return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo); + Map uploadMap = Utilities.parseUploadParams(upload, null); + return runAndCleanupJob(query, lang, responseformat, maxrec, runid, uriInfo, uploadMap); } //UPLOAD param details - https://www.ivoa.net/documents/DALI/20170517/REC-DALI-1.1.html#tth_sEc3.4.5 @@ -82,8 +77,25 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo MultipartFormDataInput input, @Context UriInfo uriInfo) { - Map uploadMap = parseUploadParams(upload, input); + Map uploadMap = Utilities.parseUploadParams(upload, input); + return runAndCleanupJob(query, lang, responseformat, maxrec, runid, uriInfo, uploadMap); + } + + /** + * Executes a job based on the provided parameters and ensures that temporary upload files are cleaned up after completion. + * The method invokes a TAP job, listens for its termination, and performs cleanup of uploaded files stored locally. + * + * @param query The query string detailing the request to be processed. + * @param lang The language in which the query is written. + * @param responseformat Specifies the format in which the response should be returned. + * @param maxrec The maximum number of records to be returned by the query. + * @param runid A unique identifier for the job run. + * @param uriInfo The URI context of the request. + * @param uploadMap A map of upload identifiers to their respective URIs for uploaded resources. + * @return A Uni containing the path to the resulting file of the job execution. + */ + private Uni runAndCleanupJob(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @Context UriInfo uriInfo, Map uploadMap) { return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo) .onTermination() .invoke(() -> { @@ -99,7 +111,6 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo }); } - private Uni handleJob(String query, String lang, String responseformat, Long maxrec, String runid, Map uploads, UriInfo uriInfo) { final Duration SYNC_WAIT = Duration.ofSeconds(syncTimeoutSeconds); return Uni.createFrom().deferred(() -> { @@ -177,71 +188,4 @@ protected java.nio.file.Path buildErrorVOTable(TAPJob job, UWSException exceptio throw new RuntimeException("Failed to create error VOTable: " + e.getMessage(), e); } } - - /** - * Parses the UPLOAD parameter from a DALI-compliant query and processes any file uploads or URLs present in it. - * - * @param uploadParam The UPLOAD parameter containing mappings of table names to data locations. - * Each mapping is provided in the format: tableName,dataLocation, where dataLocation could be a URL or a - * parameter indicating an uploaded file (e.g., param:uploadFile). - * @param input The multipart form data input containing uploaded file data, if any. - * @return A map where the keys are table names and the values are URIs pointing to corresponding data sources (e.g., temporary file URIs, remote URLs). - */ - private Map parseUploadParams(String uploadParam, MultipartFormDataInput input) { - Map uploadMap = new java.util.HashMap<>(); - - if (uploadParam != null) { - String[] uploadSpecs = uploadParam.split(";"); - for (String uploadSpec : uploadSpecs) { - // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. - String[] parts = uploadSpec.split(","); - String tableName = parts[0]; - String tableLoc = parts[1]; //either param: or a URL to a remote file (http, https, vos, etc) - if (TapUploadService.isValidUploadParam(uploadSpec)) { - if (tableLoc.startsWith("param:")) { - try { - URI fileUri = storeVOTable(tableLoc, input); - if (fileUri != null) { - uploadSpec = fileUri.toString(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - else { - uploadSpec = tableLoc; - } - } - uploadMap.put(tableName, URI.create(uploadSpec)); - } - } - return uploadMap; - } - - /** - * Stores a VOTable in a temporary file and returns the URI of the file. - * @param uploadParam parameter of the DALI UPLOAD query parameter, e.g. "table1,http://example.com/t1.xml", - * "image1,vos://example.authority!tempSpace/foo.fits", or "table3,param:t3" - * @param input The multipart form data input. - * @return The URI of the uploaded file, or null if the file was not uploaded. - * @throws IOException If an I/O error occurs while storing the file. - */ - private URI storeVOTable(@NonNull String uploadParam, @NonNull MultipartFormDataInput input) throws IOException { - String paramName = uploadParam.split(":")[1]; - - Optional value = Optional.ofNullable(input.getValues().get(paramName)) - .flatMap(list -> list.stream().findFirst()); - - if (value.isPresent() && value.get().isFileItem()) { - java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); - - UUID uuid = UUID.randomUUID(); - java.nio.file.Path persistent = Files.createTempFile("tap-upload-" + uuid, ".vot"); - - Files.copy(uploadedFile, persistent, StandardCopyOption.REPLACE_EXISTING); - - return persistent.toUri(); - } - return null; - } } diff --git a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java new file mode 100644 index 0000000..dc19484 --- /dev/null +++ b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java @@ -0,0 +1,94 @@ +package org.javastro.ivoa.tap.upload; + +import org.javastro.ivoacore.tap.upload.TapUploadService; +import org.jboss.resteasy.reactive.server.multipart.FormValue; +import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; +import org.jspecify.annotations.NonNull; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * The Utilities class provides functionality for parsing and processing + * the DALI-compliant UPLOAD parameter, handling file uploads or URLs for + * input data. It includes methods for extracting upload specifications + * from the UPLOAD parameter, storing VOTables as temporary files, and + * generating appropriate URIs. + */ +public class Utilities { + + /** + * Parses the UPLOAD parameter from a DALI-compliant query and processes any file uploads or URLs present in it. + * + * @param uploadParam The UPLOAD parameter containing mappings of table names to data locations. + * Each mapping is provided in the format: tableName,dataLocation, where dataLocation could be a URL or a + * parameter indicating an uploaded file (e.g., param:uploadFile). + * @param input The multipart form data input containing uploaded file data, if any. + * @return A map where the keys are table names and the values are URIs pointing to corresponding data sources (e.g., temporary file URIs, remote URLs). + */ + public static Map parseUploadParams(String uploadParam, MultipartFormDataInput input) { + Map uploadMap = new java.util.HashMap<>(); + + if (uploadParam != null) { + String[] uploadSpecs = uploadParam.split(";"); + for (String uploadSpec : uploadSpecs) { + // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. + String[] parts = uploadSpec.split(","); + String tableName = parts[0]; + String tableLoc = parts[1]; //either param: or a URL to a remote file (http, https, vos, etc) + if (TapUploadService.isValidUploadParam(uploadSpec)) { + if (tableLoc.startsWith("param:")) { + try { + URI fileUri = storeVOTable(tableLoc, input); + if (fileUri != null) { + uploadSpec = fileUri.toString(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + else if (tableLoc.startsWith("vos")) { + //TODO: implement VOSpace upload handling, which will likely involve downloading the file to a temporary location and then creating a file: URI to it, similar to the param: case above. + } + else { + uploadSpec = tableLoc; + } + } + uploadMap.put(tableName, URI.create(uploadSpec)); + } + } + return uploadMap; + } + + /** + * Stores a VOTable in a temporary file and returns the URI of the file. + * @param uploadParam parameter of the DALI UPLOAD query parameter, e.g. "table1,http://example.com/t1.xml", + * "image1,vos://example.authority!tempSpace/foo.fits", or "table3,param:t3" + * @param input The multipart form data input. + * @return The URI of the uploaded file, or null if the file was not uploaded. + * @throws IOException If an I/O error occurs while storing the file. + */ + private static URI storeVOTable(@NonNull String uploadParam, @NonNull MultipartFormDataInput input) throws IOException { + String paramName = uploadParam.split(":")[1]; + + Optional value = Optional.ofNullable(input.getValues().get(paramName)) + .flatMap(list -> list.stream().findFirst()); + + if (value.isPresent() && value.get().isFileItem()) { + java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); + + UUID uuid = UUID.randomUUID(); + java.nio.file.Path persistent = Files.createTempFile("tap-upload-" + uuid, ".vot"); + + Files.copy(uploadedFile, persistent, StandardCopyOption.REPLACE_EXISTING); + + return persistent.toUri(); + } + return null; + } +} From 7c05801f44a133e1ec44b15f52352c294e43e1a9 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Mon, 22 Jun 2026 16:37:03 +0100 Subject: [PATCH 12/15] output exception for vos: params as they are not going to be implemented immediately. --- src/main/java/org/javastro/ivoa/tap/upload/Utilities.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java index dc19484..90a4c59 100644 --- a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java +++ b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java @@ -1,6 +1,7 @@ package org.javastro.ivoa.tap.upload; import org.javastro.ivoacore.tap.upload.TapUploadService; +import org.jboss.resteasy.reactive.common.NotImplementedYet; import org.jboss.resteasy.reactive.server.multipart.FormValue; import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; import org.jspecify.annotations.NonNull; @@ -53,7 +54,8 @@ public static Map parseUploadParams(String uploadParam, MultipartFo } } else if (tableLoc.startsWith("vos")) { - //TODO: implement VOSpace upload handling, which will likely involve downloading the file to a temporary location and then creating a file: URI to it, similar to the param: case above. + //Requires a VOSpace client to be configured for testing + throw new UnsupportedOperationException("VOSpace uploads are not currently supported"); } else { uploadSpec = tableLoc; From 5a0fcb32d700085b02f512233c20aa2a7fb6e287 Mon Sep 17 00:00:00 2001 From: Stephen Lloyd Date: Tue, 23 Jun 2026 11:01:56 +0100 Subject: [PATCH 13/15] Fixed the rejected calls due to the multipart (only incorrectly) blocking non-multipart requests Added some unit tests for the upload process --- .../javastro/ivoa/tap/AsyncQueryResource.java | 1 + .../org/javastro/ivoa/tap/QueryResource.java | 2 +- .../javastro/ivoa/tap/upload/Utilities.java | 1 - .../java/org/javastro/ivoa/tap/QueryTest.java | 4 +- .../org/javastro/ivoa/tap/UploadTest.java | 54 ++++++++++++++++++- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java index a8f3e62..1eb6579 100644 --- a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java @@ -56,6 +56,7 @@ protected Response redirectToJob(String jobid) { //IMPL the two query endpoints are in different resources for routing purposes @POST + @Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.MULTIPART_FORM_DATA}) public Response async(@RestForm("QUERY") String query, @RestForm("LANG") String lang, @RestForm("RESPONSEFORMAT") String responseformat, @RestForm("MAXREC") Long maxrec, @RestForm("RUNID") String runid, @RestForm("UPLOAD") String upload, MultipartFormDataInput input, @Context UriInfo uriInfo) throws UWSException { diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 7cb935d..64df61a 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -70,7 +70,7 @@ public Uni syncGet(@RestQuery String query, @RestQuery Strin //UPLOAD=image1,vos://example.authority!tempSpace/foo.fits //UPLOAD=table3,param:t3 @POST - @Consumes(MediaType.MULTIPART_FORM_DATA) + @Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.MULTIPART_FORM_DATA}) @Produces("application/x-votable+xml") public Uni syncPost(@RestForm("QUERY") String query, @RestForm("LANG") String lang, @RestForm("RESPONSEFORMAT") String responseformat, @RestForm("MAXREC") Long maxrec, @RestForm("RUNID") String runid, @RestForm("UPLOAD") String upload, diff --git a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java index 90a4c59..31181cc 100644 --- a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java +++ b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java @@ -1,7 +1,6 @@ package org.javastro.ivoa.tap.upload; import org.javastro.ivoacore.tap.upload.TapUploadService; -import org.jboss.resteasy.reactive.common.NotImplementedYet; import org.jboss.resteasy.reactive.server.multipart.FormValue; import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; import org.jspecify.annotations.NonNull; diff --git a/src/test/java/org/javastro/ivoa/tap/QueryTest.java b/src/test/java/org/javastro/ivoa/tap/QueryTest.java index 63beab3..cfe0c29 100644 --- a/src/test/java/org/javastro/ivoa/tap/QueryTest.java +++ b/src/test/java/org/javastro/ivoa/tap/QueryTest.java @@ -7,7 +7,6 @@ import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; import io.restassured.filter.log.LogDetail; import io.restassured.http.ContentType; import io.restassured.response.Response; @@ -185,6 +184,7 @@ public void testSyncQuery() { .log().body() .statusCode(200); //TODO validate the VOTable } + @Test public void testErrorQuery() { given() @@ -194,6 +194,4 @@ public void testErrorQuery() { .log().body() .statusCode(200); //TODO validate the VOTable } - - } diff --git a/src/test/java/org/javastro/ivoa/tap/UploadTest.java b/src/test/java/org/javastro/ivoa/tap/UploadTest.java index f6a09bf..e5ff2c6 100644 --- a/src/test/java/org/javastro/ivoa/tap/UploadTest.java +++ b/src/test/java/org/javastro/ivoa/tap/UploadTest.java @@ -3,13 +3,63 @@ import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; +import java.io.File; +import java.net.URISyntaxException; +import java.util.Objects; + +import static io.restassured.RestAssured.given; + @QuarkusTest public class UploadTest { - //https://gist.githubusercontent.com/slloyd-src/7300663888d7e78994eab792ac232253/raw/4eb09a722c9afb6e72e0898d545283941ddd5881/gistfile1.txt + //Location of a simple and valid VOTable file + static final String REMOTE_FILE_URL = "https://gist.githubusercontent.com/slloyd-src/7300663888d7e78994eab792ac232253/raw/4eb09a722c9afb6e72e0898d545283941ddd5881/gistfile1.txt"; + + //Test a local file upload + @Test + public void testResourceUpload() { + try { + File votable = getTestFile(); + + given() + .multiPart("QUERY", "SELECT * FROM mytable") + .multiPart("UPLOAD", "mytable,param:mytableFile") + .multiPart("mytableFile", votable, "application/x-votable+xml") + .when() + .post("/sync") + .then() + .statusCode(200); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + //Test a remote file upload using POST @Test - public void testUpload() { + public void testPostRemoteUpload() { + given() + .multiPart("QUERY", "SELECT * FROM mytable") + .multiPart("UPLOAD", "mytable," + REMOTE_FILE_URL) + .when() + .post("/sync") + .then() + .statusCode(200); + } + + //Test a remote file upload using GET + @Test + public void testGetRemoteUpload() { + given().get("/sync?QUERY=select%20*%20from%20table3&UPLOAD=table3," + REMOTE_FILE_URL) + .then() + .statusCode(200); + } + //Get the test file from the resources folder + private File getTestFile() throws URISyntaxException { + return new File( + Objects.requireNonNull(getClass() + .getResource("/uploads/example-votable.vot")) + .toURI() + ); } } From f1e48c42979f69d004e76ed6a83a237ca5733146 Mon Sep 17 00:00:00 2001 From: Paul Harrison Date: Tue, 23 Jun 2026 17:59:00 +0100 Subject: [PATCH 14/15] test the upload against async endpoint --- .../javastro/ivoa/tap/AbstractTAPTest.java | 85 +++++++++++++++++++ .../java/org/javastro/ivoa/tap/QueryTest.java | 69 +-------------- .../org/javastro/ivoa/tap/UploadTest.java | 40 ++++++++- 3 files changed, 126 insertions(+), 68 deletions(-) create mode 100644 src/test/java/org/javastro/ivoa/tap/AbstractTAPTest.java diff --git a/src/test/java/org/javastro/ivoa/tap/AbstractTAPTest.java b/src/test/java/org/javastro/ivoa/tap/AbstractTAPTest.java new file mode 100644 index 0000000..cbd0df4 --- /dev/null +++ b/src/test/java/org/javastro/ivoa/tap/AbstractTAPTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026. Paul Harrison, University of Manchester. + * + */ + +package org.javastro.ivoa.tap; + + +import io.restassured.filter.log.LogDetail; +import io.restassured.http.ContentType; + +import java.time.Duration; + +import static io.restassured.RestAssured.given; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.fail; + +public abstract class AbstractTAPTest { + protected static void startAndTestJob(String jobUrl) { + // Start the Job (POST to /phase with PHASE=RUN) + given() + .contentType(ContentType.URLENC) + .redirects().follow(false) + .formParam("PHASE", "RUN") + .when() + .post(jobUrl + "/phase") + .then() + .statusCode(303); + + // Poll until + await().atMost(Duration.ofSeconds(30)) + .pollInterval(Duration.ofMillis(500)) + .untilAsserted(() -> { + given() + .when().get(jobUrl + "/phase") + .then() + .statusCode(200) + .body(not(comparesEqualTo("RUNNING"))); + }) + ; + String status = given() + .when().get(jobUrl + "/phase") + .then() + .statusCode(200) + .extract().body().asString(); + if (status.equals("ERROR")) + { + + given() + .when().get(jobUrl + "/error") + .then() + .statusCode(200) + .log().body(); + fail("Job ended in error state"); + + } + else if (status.equals("COMPLETED")) { + + + // Retrieve Results + given() + .when().get(jobUrl + "/results") + .then() + .log().ifValidationFails(LogDetail.BODY) + .statusCode(200) + .body("results.result.size()", greaterThan(0)); + + //retrieve the actual result + given() + .when().get(jobUrl + "/results/result") + .then() + .statusCode(200) + .log().body(); + ; + + //TODO get the result into a file and verify that it is an OK VOTable. + + } + else + { + fail("Unexpected job status "+status); + } + } +} diff --git a/src/test/java/org/javastro/ivoa/tap/QueryTest.java b/src/test/java/org/javastro/ivoa/tap/QueryTest.java index cfe0c29..9f6c61a 100644 --- a/src/test/java/org/javastro/ivoa/tap/QueryTest.java +++ b/src/test/java/org/javastro/ivoa/tap/QueryTest.java @@ -7,13 +7,10 @@ import io.quarkus.test.junit.QuarkusTest; -import io.restassured.filter.log.LogDetail; import io.restassured.http.ContentType; import io.restassured.response.Response; import org.junit.jupiter.api.Test; -import java.time.Duration; - import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; import static org.awaitility.Awaitility.await; @@ -24,7 +21,7 @@ * Created on 05/03/2026 by Paul Harrison (paul.harrison@manchester.ac.uk). */ @QuarkusTest -public class QueryTest { +public class QueryTest extends AbstractTAPTest { private static final String ASYNC_ENDPOINT = "/async"; private static final String SYNC_ENDPOINT = "/sync"; @Test @@ -75,69 +72,7 @@ public void testAsyncQuery() { // .statusCode(200) // .log().body(); - // Start the Job (POST to /phase with PHASE=RUN) - given() - .contentType(ContentType.URLENC) - .redirects().follow(false) - .formParam("PHASE", "RUN") - .when() - .post(jobUrl + "/phase") - .then() - .statusCode(303); - - // Poll until - await().atMost(Duration.ofSeconds(30)) - .pollInterval(Duration.ofMillis(500)) - .untilAsserted(() -> { - given() - .when().get(jobUrl + "/phase") - .then() - .statusCode(200) - .body(not(comparesEqualTo("RUNNING"))); - }) - ; - String status = given() - .when().get(jobUrl + "/phase") - .then() - .statusCode(200) - .extract().body().asString(); - if (status.equals("ERROR")) - { - - given() - .when().get(jobUrl + "/error") - .then() - .statusCode(200) - .log().body(); - fail("Job ended in error state"); - - } - else if (status.equals("COMPLETED")) { - - - // Retrieve Results - given() - .when().get(jobUrl + "/results") - .then() - .log().ifValidationFails(LogDetail.BODY) - .statusCode(200) - .body("results.result.size()", greaterThan(0)); - - //retrieve the actual result - given() - .when().get(jobUrl + "/results/result") - .then() - .statusCode(200) - .log().body(); - ; - - //TODO get the result into a file and verify that it is an OK VOTable. - - } - else - { - fail("Unexpected job status "+status); - } + startAndTestJob(jobUrl); } diff --git a/src/test/java/org/javastro/ivoa/tap/UploadTest.java b/src/test/java/org/javastro/ivoa/tap/UploadTest.java index e5ff2c6..f88dec5 100644 --- a/src/test/java/org/javastro/ivoa/tap/UploadTest.java +++ b/src/test/java/org/javastro/ivoa/tap/UploadTest.java @@ -1,6 +1,7 @@ package org.javastro.ivoa.tap; import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.Response; import org.junit.jupiter.api.Test; import java.io.File; @@ -8,9 +9,11 @@ import java.util.Objects; import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertNotNull; @QuarkusTest -public class UploadTest { +public class UploadTest extends AbstractTAPTest { //Location of a simple and valid VOTable file static final String REMOTE_FILE_URL = "https://gist.githubusercontent.com/slloyd-src/7300663888d7e78994eab792ac232253/raw/4eb09a722c9afb6e72e0898d545283941ddd5881/gistfile1.txt"; @@ -34,6 +37,36 @@ public void testResourceUpload() { } } + @Test + public void testAsyncResourceUpload() { + try { + File votable = getTestFile(); + + Response createResponse = given() + .multiPart("QUERY", "SELECT * FROM mytable") + .multiPart("UPLOAD", "mytable,param:mytableFile") + .multiPart("mytableFile", votable, "application/x-votable+xml") + .redirects().follow(false) + .when() + .post("/async") + .then() + .statusCode(303) + .header("Location", notNullValue()) + .extract().response(); + String jobFullUrl = createResponse.getHeader("Location"); + String jobId = jobFullUrl.substring(jobFullUrl.lastIndexOf('/') + 1); + String jobUrl = "/async/"+jobId; + + System.out.println(jobFullUrl); + startAndTestJob(jobUrl); + + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + + + //Test a remote file upload using POST @Test public void testPostRemoteUpload() { @@ -54,6 +87,11 @@ public void testGetRemoteUpload() { .statusCode(200); } + @Test + public void TestFileThere () throws URISyntaxException { + File file = getTestFile(); + assertNotNull(file); + } //Get the test file from the resources folder private File getTestFile() throws URISyntaxException { return new File( From bbb4ae32200a757be4b6fff9858d60510d78f39d Mon Sep 17 00:00:00 2001 From: Paul Harrison Date: Wed, 24 Jun 2026 13:35:32 +0100 Subject: [PATCH 15/15] refactor the TAP upload to place files in the Job work directory --- .../javastro/ivoa/tap/AsyncQueryResource.java | 13 ++- .../org/javastro/ivoa/tap/QueryResource.java | 48 +++------- .../ivoa/tap/upload/QuarkusTapUploader.java | 64 +++++++++++++ .../javastro/ivoa/tap/upload/Utilities.java | 95 ------------------- 4 files changed, 84 insertions(+), 136 deletions(-) create mode 100644 src/main/java/org/javastro/ivoa/tap/upload/QuarkusTapUploader.java delete mode 100644 src/main/java/org/javastro/ivoa/tap/upload/Utilities.java diff --git a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java index 1eb6579..197bf5e 100644 --- a/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/AsyncQueryResource.java @@ -11,9 +11,10 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.*; import org.eclipse.microprofile.openapi.annotations.tags.Tag; -import org.javastro.ivoa.tap.upload.Utilities; -import org.javastro.ivoacore.common.ServiceLocator; +import org.javastro.ivoa.tap.upload.QuarkusTapUploader; import org.javastro.ivoacore.tap.TAPJobSpecification; +import org.javastro.ivoacore.tap.upload.NullUploader; +import org.javastro.ivoacore.tap.upload.TAPUploadCacher; import org.javastro.ivoacore.uws.BaseUWSJob; import org.javastro.ivoacore.uws.JobManager; import org.javastro.ivoacore.uws.UWSException; @@ -60,9 +61,11 @@ protected Response redirectToJob(String jobid) { public Response async(@RestForm("QUERY") String query, @RestForm("LANG") String lang, @RestForm("RESPONSEFORMAT") String responseformat, @RestForm("MAXREC") Long maxrec, @RestForm("RUNID") String runid, @RestForm("UPLOAD") String upload, MultipartFormDataInput input, @Context UriInfo uriInfo) throws UWSException { - - Map uploadMap = Utilities.parseUploadParams(upload, input); - BaseUWSJob job = tapHelper.jobmanager.createJob(new TAPJobSpecification(query,lang,responseformat,maxrec,runid,uploadMap)); + TAPUploadCacher tapUploader = new NullUploader(); + if(upload != null && !upload.isEmpty() ) { + tapUploader = new QuarkusTapUploader(upload, input); + } + BaseUWSJob job = tapHelper.jobmanager.createJob(new TAPJobSpecification(query,lang,responseformat,maxrec,runid,tapUploader)); return Response.seeOther(tapHelper.asyncJobUri(job.getID())).build(); } diff --git a/src/main/java/org/javastro/ivoa/tap/QueryResource.java b/src/main/java/org/javastro/ivoa/tap/QueryResource.java index 64df61a..dace59a 100644 --- a/src/main/java/org/javastro/ivoa/tap/QueryResource.java +++ b/src/main/java/org/javastro/ivoa/tap/QueryResource.java @@ -17,10 +17,12 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.javastro.ivoa.entities.uws.ExecutionPhase; -import org.javastro.ivoa.tap.upload.Utilities; +import org.javastro.ivoa.tap.upload.QuarkusTapUploader; import org.javastro.ivoacore.tap.TAPJob; import org.javastro.ivoacore.tap.TAPJobSpecification; import org.javastro.ivoacore.tap.TAPWriter; +import org.javastro.ivoacore.tap.upload.NullUploader; +import org.javastro.ivoacore.tap.upload.TAPUploadCacher; import org.javastro.ivoacore.uws.UWSException; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.RestQuery; @@ -61,8 +63,8 @@ public class QueryResource { public Uni syncGet(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @RestQuery String upload, @Context UriInfo uriInfo) { - Map uploadMap = Utilities.parseUploadParams(upload, null); - return runAndCleanupJob(query, lang, responseformat, maxrec, runid, uriInfo, uploadMap); + + return handleJob(query, lang, responseformat, maxrec, runid, upload, null, uriInfo); } //UPLOAD param details - https://www.ivoa.net/documents/DALI/20170517/REC-DALI-1.1.html#tth_sEc3.4.5 @@ -77,47 +79,21 @@ public Uni syncPost(@RestForm("QUERY") String query, @RestFo MultipartFormDataInput input, @Context UriInfo uriInfo) { - Map uploadMap = Utilities.parseUploadParams(upload, input); - - return runAndCleanupJob(query, lang, responseformat, maxrec, runid, uriInfo, uploadMap); + return handleJob(query, lang, responseformat, maxrec, runid, upload, input, uriInfo); } - /** - * Executes a job based on the provided parameters and ensures that temporary upload files are cleaned up after completion. - * The method invokes a TAP job, listens for its termination, and performs cleanup of uploaded files stored locally. - * - * @param query The query string detailing the request to be processed. - * @param lang The language in which the query is written. - * @param responseformat Specifies the format in which the response should be returned. - * @param maxrec The maximum number of records to be returned by the query. - * @param runid A unique identifier for the job run. - * @param uriInfo The URI context of the request. - * @param uploadMap A map of upload identifiers to their respective URIs for uploaded resources. - * @return A Uni containing the path to the resulting file of the job execution. - */ - private Uni runAndCleanupJob(@RestQuery String query, @RestQuery String lang, @RestQuery String responseformat, @RestQuery Long maxrec, @RestQuery String runid, @Context UriInfo uriInfo, Map uploadMap) { - return handleJob(query, lang, responseformat, maxrec, runid, uploadMap, uriInfo) - .onTermination() - .invoke(() -> { - for (URI uploadedFile : uploadMap.values()) { - if ("file".equalsIgnoreCase(uploadedFile.getScheme())) { - try { - Files.deleteIfExists(java.nio.file.Path.of(uploadedFile)); - } catch (IOException e) { - log.warn("Failed to delete upload file {}", uploadedFile, e); - } - } - } - }); - } - private Uni handleJob(String query, String lang, String responseformat, Long maxrec, String runid, Map uploads, UriInfo uriInfo) { + private Uni handleJob(String query, String lang, String responseformat, Long maxrec, String runid, String upload, MultipartFormDataInput input, UriInfo uriInfo) { final Duration SYNC_WAIT = Duration.ofSeconds(syncTimeoutSeconds); return Uni.createFrom().deferred(() -> { final TAPJob job; try { + TAPUploadCacher tapUploader = new NullUploader(); + if(upload != null && !upload.isEmpty() ) { + tapUploader = new QuarkusTapUploader(upload, input); + } job = (TAPJob) tapHelper.jobmanager.createJob( - new TAPJobSpecification(query, lang, responseformat, maxrec, runid, uploads) + new TAPJobSpecification(query, lang, responseformat, maxrec, runid, tapUploader) ); tapHelper.jobmanager.runJob(job.getID()); // automatically run the job diff --git a/src/main/java/org/javastro/ivoa/tap/upload/QuarkusTapUploader.java b/src/main/java/org/javastro/ivoa/tap/upload/QuarkusTapUploader.java new file mode 100644 index 0000000..951f0f7 --- /dev/null +++ b/src/main/java/org/javastro/ivoa/tap/upload/QuarkusTapUploader.java @@ -0,0 +1,64 @@ + package org.javastro.ivoa.tap.upload; + + import org.javastro.ivoacore.tap.upload.BaseTAPUploadCacher; + import org.javastro.ivoacore.tap.upload.TapUploadService; + import org.jboss.resteasy.reactive.server.multipart.FormValue; + import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; + import org.jspecify.annotations.NonNull; + + import java.io.IOException; + import java.net.URI; + import java.nio.file.Files; + import java.nio.file.Path; + import java.nio.file.StandardCopyOption; + import java.util.Map; + import java.util.Optional; + import java.util.UUID; + + /** + * The QuarkusTapUploader class provides functionality for parsing and processing + * the DALI-compliant UPLOAD parameter, handling file uploads or URLs for + * input data. It includes methods for extracting upload specifications + * from the UPLOAD parameter, storing VOTables as temporary files, and + * generating appropriate Paths. + */ + public class QuarkusTapUploader extends BaseTAPUploadCacher { + + private final MultipartFormDataInput input; + + public QuarkusTapUploader(String uploadParam, MultipartFormDataInput input) { + super(uploadParam); + this.input = input; + } + + /** + * Stores a VOTable in a temporary file and returns the URI of the file. + * @param theParam a param parameter of the DALI UPLOAD query parameter, e.g. "param:t3" + * @return The Path of the uploaded file, or null if the file was not uploaded. + * @throws IOException If an I/O error occurs while storing the file. + */ + protected Path storeParam(@NonNull String theParam, Path dir) throws IOException { + String paramName = theParam.split(":")[1]; + + Optional value = Optional.ofNullable(input.getValues().get(paramName)) + .flatMap(list -> list.stream().findFirst()); + + if (value.isPresent() && value.get().isFileItem()) { + java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); + Path persistent = generateFileName(dir, paramName); + Files.copy(uploadedFile, persistent, StandardCopyOption.REPLACE_EXISTING); + + return persistent; + } + return null; + } + + @Override + public boolean hasUpload() { + return true; + } + + + + + } diff --git a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java b/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java deleted file mode 100644 index 31181cc..0000000 --- a/src/main/java/org/javastro/ivoa/tap/upload/Utilities.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.javastro.ivoa.tap.upload; - -import org.javastro.ivoacore.tap.upload.TapUploadService; -import org.jboss.resteasy.reactive.server.multipart.FormValue; -import org.jboss.resteasy.reactive.server.multipart.MultipartFormDataInput; -import org.jspecify.annotations.NonNull; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -/** - * The Utilities class provides functionality for parsing and processing - * the DALI-compliant UPLOAD parameter, handling file uploads or URLs for - * input data. It includes methods for extracting upload specifications - * from the UPLOAD parameter, storing VOTables as temporary files, and - * generating appropriate URIs. - */ -public class Utilities { - - /** - * Parses the UPLOAD parameter from a DALI-compliant query and processes any file uploads or URLs present in it. - * - * @param uploadParam The UPLOAD parameter containing mappings of table names to data locations. - * Each mapping is provided in the format: tableName,dataLocation, where dataLocation could be a URL or a - * parameter indicating an uploaded file (e.g., param:uploadFile). - * @param input The multipart form data input containing uploaded file data, if any. - * @return A map where the keys are table names and the values are URIs pointing to corresponding data sources (e.g., temporary file URIs, remote URLs). - */ - public static Map parseUploadParams(String uploadParam, MultipartFormDataInput input) { - Map uploadMap = new java.util.HashMap<>(); - - if (uploadParam != null) { - String[] uploadSpecs = uploadParam.split(";"); - for (String uploadSpec : uploadSpecs) { - // If there's a "~,param:~~" upload parameter supplied, then upload the file to tmp and create a file: URI to it. - String[] parts = uploadSpec.split(","); - String tableName = parts[0]; - String tableLoc = parts[1]; //either param: or a URL to a remote file (http, https, vos, etc) - if (TapUploadService.isValidUploadParam(uploadSpec)) { - if (tableLoc.startsWith("param:")) { - try { - URI fileUri = storeVOTable(tableLoc, input); - if (fileUri != null) { - uploadSpec = fileUri.toString(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - else if (tableLoc.startsWith("vos")) { - //Requires a VOSpace client to be configured for testing - throw new UnsupportedOperationException("VOSpace uploads are not currently supported"); - } - else { - uploadSpec = tableLoc; - } - } - uploadMap.put(tableName, URI.create(uploadSpec)); - } - } - return uploadMap; - } - - /** - * Stores a VOTable in a temporary file and returns the URI of the file. - * @param uploadParam parameter of the DALI UPLOAD query parameter, e.g. "table1,http://example.com/t1.xml", - * "image1,vos://example.authority!tempSpace/foo.fits", or "table3,param:t3" - * @param input The multipart form data input. - * @return The URI of the uploaded file, or null if the file was not uploaded. - * @throws IOException If an I/O error occurs while storing the file. - */ - private static URI storeVOTable(@NonNull String uploadParam, @NonNull MultipartFormDataInput input) throws IOException { - String paramName = uploadParam.split(":")[1]; - - Optional value = Optional.ofNullable(input.getValues().get(paramName)) - .flatMap(list -> list.stream().findFirst()); - - if (value.isPresent() && value.get().isFileItem()) { - java.nio.file.Path uploadedFile = value.get().getFileItem().getFile(); - - UUID uuid = UUID.randomUUID(); - java.nio.file.Path persistent = Files.createTempFile("tap-upload-" + uuid, ".vot"); - - Files.copy(uploadedFile, persistent, StandardCopyOption.REPLACE_EXISTING); - - return persistent.toUri(); - } - return null; - } -}