From 1f32ff4b16123e8b98d2304c2cffb00ab75da26b Mon Sep 17 00:00:00 2001 From: Patrick Dowler Date: Wed, 10 Apr 2024 08:21:03 -0700 Subject: [PATCH] minoc: infer FITS content type from filename if Artifact.contentType is null --- minoc/VERSION | 2 +- .../opencadc/minoc/FitsOperationsTest.java | 32 +++++++++++++- .../java/org/opencadc/minoc/GetAction.java | 42 +++++++++++++------ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/minoc/VERSION b/minoc/VERSION index 0c7cac03..c5fd5316 100644 --- a/minoc/VERSION +++ b/minoc/VERSION @@ -4,6 +4,6 @@ # tags with and without build number so operators use the versioned # tag but we always keep a timestamped tag in case a semantic tag gets # replaced accidentally -VER=1.0.2 +VER=1.0.3 TAGS="${VER} ${VER}-$(date --utc +"%Y%m%dT%H%M%S")" unset VER diff --git a/minoc/src/intTest/java/org/opencadc/minoc/FitsOperationsTest.java b/minoc/src/intTest/java/org/opencadc/minoc/FitsOperationsTest.java index e19595f4..2efd1888 100644 --- a/minoc/src/intTest/java/org/opencadc/minoc/FitsOperationsTest.java +++ b/minoc/src/intTest/java/org/opencadc/minoc/FitsOperationsTest.java @@ -71,6 +71,7 @@ import ca.nrc.cadc.auth.AuthMethod; import ca.nrc.cadc.net.HttpDelete; import ca.nrc.cadc.net.HttpGet; +import ca.nrc.cadc.net.HttpPost; import ca.nrc.cadc.net.HttpTransfer; import ca.nrc.cadc.net.HttpUpload; import ca.nrc.cadc.net.NetUtil; @@ -99,6 +100,9 @@ import java.nio.file.Path; import java.security.PrivilegedExceptionAction; import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; /** * Integration test to pull existing test FITS files from VOSpace (Vault) into a local directory, then PUT them into @@ -117,6 +121,9 @@ public class FitsOperationsTest extends MinocTest { + "/.config/test-data").toPath(); protected URL filesVaultURL; + + // normally true except for one test + private boolean setContentType = true; static { Log4jInit.setLevel("org.opencadc.minoc", Level.INFO); @@ -190,6 +197,27 @@ public void testSimple() throws Exception { }; uploadAndCompareCutout(artifactURI, SodaParamValidator.SUB, cutoutSpecs, testFilePrefix); + + LOGGER.info("unset content-type and try again: rely on filename extension only..."); + final URI noclArtifactURI = URI.create("cadc:TEST/" + testFilePrefix + "-nocl." + testFileExtension); + final URL noclArtifactURL = new URL(filesURL + "/" + noclArtifactURI.toString()); + LOGGER.info("no content-length: " + noclArtifactURL); + + try { + setContentType = false; + uploadAndCompareCutout(noclArtifactURI, SodaParamValidator.SUB, cutoutSpecs, testFilePrefix); + } finally { + setContentType = true; + } + + Subject.doAs(userSubject, (PrivilegedExceptionAction) () -> { + HttpGet head = new HttpGet(noclArtifactURL, false); + head.setHeadOnly(true); + head.prepare(); + Assert.assertNull("no content type", head.getResponseHeader("content-type")); + return null; + }); + } @Test @@ -462,7 +490,9 @@ private void ensureFile(final URI artifactURI) throws Exception { final HttpUpload upload = new HttpUpload(fileInputStream, artifactURL); upload.setRequestProperty("X-Test-Method", fileName); upload.setRequestProperty(HttpTransfer.CONTENT_LENGTH, Long.toString(localFile.length())); - upload.setRequestProperty(HttpTransfer.CONTENT_TYPE, "application/fits"); + if (setContentType) { + upload.setRequestProperty(HttpTransfer.CONTENT_TYPE, "application/fits"); + } upload.run(); LOGGER.info("response code: " + upload.getResponseCode() + " " + upload.getThrowable()); Assert.assertNull("Upload contains error.", upload.getThrowable()); diff --git a/minoc/src/main/java/org/opencadc/minoc/GetAction.java b/minoc/src/main/java/org/opencadc/minoc/GetAction.java index e2df6428..f58b687f 100644 --- a/minoc/src/main/java/org/opencadc/minoc/GetAction.java +++ b/minoc/src/main/java/org/opencadc/minoc/GetAction.java @@ -115,11 +115,18 @@ public class GetAction extends ArtifactAction { private static final String CONTENT_DISPOSITION = "content-disposition"; private static final String CONTENT_RANGE = "content-range"; private static final String CONTENT_LENGTH = "content-length"; + + private static final String FITS_CONTENT_TYPE = "application/fits"; private static final String[] FITS_CONTENT_TYPES = new String[] { - "application/fits", "image/fits" + FITS_CONTENT_TYPE, + "image/fits" // alternative }; - - private static final SodaParamValidator SODA_PARAM_VALIDATOR = new SodaParamValidator(); + private static final String[] FITS_EXTENSIONS = new String[] { + ".fits", + ".fz" + }; + + private final SodaParamValidator sodaParamValidator = new SodaParamValidator(); // constructor for unit tests with no config/init GetAction(boolean init) { @@ -271,7 +278,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut log.debug("SUB supplied"); final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.SUB, sodaCutout.requestedSubs); - final List slices = SODA_PARAM_VALIDATOR.validateSUB(parameterMap); + final List slices = sodaParamValidator.validateSUB(parameterMap); final Cutout cutout = new Cutout(); cutout.pixelCutouts = slices; @@ -296,7 +303,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.CIRCLE, sodaCutout.requestedCircles); - final List validCircles = SODA_PARAM_VALIDATOR.validateCircle(parameterMap); + final List validCircles = sodaParamValidator.validateCircle(parameterMap); cutout.pos = assertSingleWCS(SodaParamValidator.CIRCLE, validCircles); } @@ -306,7 +313,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.POLYGON, sodaCutout.requestedPolygons); - final List validPolygons = SODA_PARAM_VALIDATOR.validatePolygon(parameterMap); + final List validPolygons = sodaParamValidator.validatePolygon(parameterMap); cutout.pos = assertSingleWCS(SodaParamValidator.POLYGON, validPolygons); } @@ -316,7 +323,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.POS, sodaCutout.requestedPOSs); - final List validShapes = SODA_PARAM_VALIDATOR.validatePOS(parameterMap); + final List validShapes = sodaParamValidator.validatePOS(parameterMap); cutout.pos = assertSingleWCS(SodaParamValidator.POS, validShapes); } @@ -326,7 +333,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.BAND, sodaCutout.requestedBands); - final List validBandIntervals = SODA_PARAM_VALIDATOR.validateBAND(parameterMap); + final List validBandIntervals = sodaParamValidator.validateBAND(parameterMap); cutout.band = assertSingleWCS(SodaParamValidator.BAND, validBandIntervals); } @@ -336,7 +343,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.TIME, sodaCutout.requestedTimes); - final List validTimeIntervals = SODA_PARAM_VALIDATOR.validateTIME(parameterMap); + final List validTimeIntervals = sodaParamValidator.validateTIME(parameterMap); cutout.time = assertSingleWCS(SodaParamValidator.TIME, validTimeIntervals); } @@ -346,7 +353,7 @@ private ByteCountOutputStream doOperation(FitsOperations fitsOperations, SodaCut final Map> parameterMap = new TreeMap<>(new CaseInsensitiveStringComparator()); parameterMap.put(SodaParamValidator.POL, sodaCutout.requestedPOLs); - cutout.pol = SODA_PARAM_VALIDATOR.validatePOL(parameterMap); + cutout.pol = sodaParamValidator.validatePOL(parameterMap); if (cutout.pol.size() != sodaCutout.requestedPOLs.size()) { log.debug("Accepted " + cutout.pol + " valid POL states but " + sodaCutout.requestedPOLs @@ -381,8 +388,19 @@ private T assertSingleWCS(final String key, final List wcsValues) { } private boolean isFITS(final Artifact artifact) { - return StringUtil.hasText(artifact.contentType) - && Arrays.stream(FITS_CONTENT_TYPES).anyMatch(s -> s.equals(artifact.contentType)); + final String contentType = (artifact.contentType != null + ? artifact.contentType : getContentTypeFromFilename(artifact.getURI().getSchemeSpecificPart())); + return StringUtil.hasText(contentType) + && Arrays.stream(FITS_CONTENT_TYPES).anyMatch(s -> s.equals(contentType)); + } + + private String getContentTypeFromFilename(String filename) { + for (String ext : FITS_EXTENSIONS) { + if (filename.endsWith(ext)) { + return FITS_CONTENT_TYPE; + } + } + return null; } /**