diff --git a/modules/dcache-webdav/src/main/java/org/dcache/webdav/DcacheResourceFactory.java b/modules/dcache-webdav/src/main/java/org/dcache/webdav/DcacheResourceFactory.java index ad2136fb2be..d5a5ae99ba4 100644 --- a/modules/dcache-webdav/src/main/java/org/dcache/webdav/DcacheResourceFactory.java +++ b/modules/dcache-webdav/src/main/java/org/dcache/webdav/DcacheResourceFactory.java @@ -110,6 +110,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.PostConstruct; import javax.security.auth.Subject; import javax.servlet.http.HttpServletRequest; @@ -1595,8 +1597,9 @@ private static boolean isDigestRequested() { case HEAD: case GET: return wantDigest() - .flatMap(Checksums::parseWantDigest) - .isPresent(); + .map(Checksums::parseWantDigest) + .map(c -> !c.isEmpty()) + .orElse(false); default: return false; } @@ -1674,7 +1677,7 @@ void removeExtendedAttribute(FsPath path, String name) throws CacheException { private class HttpTransfer extends RedirectedTransfer { private URI _location; - private ChecksumType _wantedChecksum; + private Set _wantedChecksums = EnumSet.noneOf(ChecksumType.class); private InetSocketAddress _clientAddressForPool; protected HttpProtocolInfo.Disposition _disposition; private boolean _isSSL; @@ -1696,9 +1699,19 @@ public HttpTransfer(PnfsHandler pnfs, Subject subject, } protected ProtocolInfo createProtocolInfo(InetSocketAddress address) { - List wantedChecksums = _wantedChecksum == null - ? Collections.emptyList() - : List.of(_wantedChecksum); + List wantedChecksums; + if (_wantedChecksums.isEmpty()) { + wantedChecksums = Collections.emptyList(); + } else { + ChecksumType preferred = _wantedChecksums.stream() + .sorted(Checksums.PREFERRED_CHECKSUM_TYPE_ORDERING) + .findFirst() + .orElseThrow(() -> new RuntimeException("Failed to identified preferred checksum in " + _wantedChecksums)); + wantedChecksums = Stream.concat( + Stream.of(preferred), + _wantedChecksums.stream().filter(c -> c != preferred)) + .collect(Collectors.toList()); + } HttpProtocolInfo protocolInfo = new HttpProtocolInfo( _isSSL ? PROTOCOL_INFO_SSL_NAME : PROTOCOL_INFO_NAME, @@ -1728,8 +1741,8 @@ public void setLocation(URI location) { _location = location; } - public void setWantedChecksum(ChecksumType type) { - _wantedChecksum = type; + public void setWantedChecksums(Set checksums) { + _wantedChecksums = requireNonNull(checksums); } public void setProxyTransfer(boolean isProxyTransfer) { @@ -1841,8 +1854,9 @@ public WriteTransfer(PnfsHandler pnfs, Subject subject, _mtime = OwncloudClients.parseMTime(request); wantDigest() - .flatMap(Checksums::parseWantDigest) - .ifPresent(this::setWantedChecksum); + .map(Checksums::parseWantDigest) + .filter(v -> !v.isEmpty()) + .ifPresent(this::setWantedChecksums); try { _contentMd5 = Optional.ofNullable( diff --git a/modules/dcache-webdav/src/main/java/org/dcache/webdav/transfer/RemoteTransferHandler.java b/modules/dcache-webdav/src/main/java/org/dcache/webdav/transfer/RemoteTransferHandler.java index 3e75f221c2c..5409ebf4f92 100644 --- a/modules/dcache-webdav/src/main/java/org/dcache/webdav/transfer/RemoteTransferHandler.java +++ b/modules/dcache-webdav/src/main/java/org/dcache/webdav/transfer/RemoteTransferHandler.java @@ -115,6 +115,7 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; import javax.annotation.PreDestroy; import javax.security.auth.Subject; @@ -763,7 +764,7 @@ private class RemoteTransfer { private final ImmutableMap _transferHeaders; private final Direction _direction; private final boolean _overwriteAllowed; - private final Optional _wantDigest; + private final Set _wantedDigests; private final PnfsHandler _pnfs; private final Instant _whenSubmitted = Instant.now(); private final SettableFuture> _transferResult = SettableFuture.create(); @@ -818,7 +819,9 @@ public RemoteTransfer(Subject subject, Restriction restriction, _transferHeaders = transferHeaders; _direction = direction; _overwriteAllowed = overwriteAllowed; - _wantDigest = wantDigest; + _wantedDigests = wantDigest + .map(Checksums::parseWantDigest) + .orElse(EnumSet.noneOf(ChecksumType.class)); } private IoDoorEntry describe() { @@ -850,9 +853,9 @@ private FileAttributes resolvePath() throws ErrorResponseException { try { switch (_direction) { case PUSH: - EnumSet desired = _wantDigest.isPresent() - ? EnumSet.of(PNFSID, SIZE, TYPE, CHECKSUM) - : EnumSet.of(PNFSID, SIZE, TYPE); + EnumSet desired = _wantedDigests.isEmpty() + ? EnumSet.of(PNFSID, SIZE, TYPE) + : EnumSet.of(PNFSID, SIZE, TYPE, CHECKSUM); desired.addAll(TransferManagerHandler.ATTRIBUTES_FOR_PUSH); try { FileAttributes attributes = _pnfs.getFileAttributes(_path.toString(), @@ -975,7 +978,7 @@ public synchronized ListenableFuture> start() _async = servletRequest.startAsync(); _async.setTimeout(0); // Disable timeout as we don't know how long we'll take. - if (_direction == Direction.PULL && _wantDigest.isPresent()) { + if (_direction == Direction.PULL && !_wantedDigests.isEmpty()) { // Ensure this is called before any perf-marker data is sent. addTrailerCallback(); } @@ -1029,14 +1032,25 @@ private IpProtocolInfo buildProtocolInfo() throws ErrorResponseException { "Unknown " + target + " hostname"); } - Optional desiredChecksum = _wantDigest.flatMap( - Checksums::parseWantDigest); - var desiredChecksums = desiredChecksum - .map(List::of) - .orElseGet(Collections::emptyList); + List desiredChecksums; + if (_wantedDigests.isEmpty()) { + desiredChecksums = Collections.emptyList(); + } else { + ChecksumType preferred = _wantedDigests.stream() + .sorted(Checksums.PREFERRED_CHECKSUM_TYPE_ORDERING) + .findFirst() + .orElseThrow(() -> new RuntimeException("Failed to identified preferred checksum in " + _wantedDigests)); + desiredChecksums = Stream.concat( + Stream.of(preferred), + _wantedDigests.stream().filter(c -> c != preferred)) + .collect(Collectors.toList()); + } switch (_type) { case GSIFTP: + Optional desiredChecksum = desiredChecksums.isEmpty() + ? Optional.empty() + : Optional.of(desiredChecksums.get(0)); return new RemoteGsiftpTransferProtocolInfo("RemoteGsiftpTransfer", 1, 1, address, _destination.toASCIIString(), null, null, buffer, MiB.toBytes(1), _privateKey, _certificateChain, @@ -1080,19 +1094,18 @@ private HttpFields getTrailers() { } private void fetchChecksums() { - if (_direction == Direction.PULL && _wantDigest.isPresent()) { - Optional empty = Optional.empty(); - _digestValue = _wantDigest.map(h -> { - try { - FileAttributes attributes = _pnfs.getFileAttributes(_path, - EnumSet.of(CHECKSUM)); - return Checksums.digestHeader(h, attributes); - } catch (CacheException e) { - LOGGER.warn("Failed to acquire checksum of fetched file: {}", - e.getMessage()); - return empty; - } - }).orElse(empty); + if (_direction == Direction.PULL && !_wantedDigests.isEmpty()) { + try { + FileAttributes attributes = _pnfs.getFileAttributes(_path, + EnumSet.of(CHECKSUM)); + _digestValue = Checksums.digestHeader(_wantedDigests, attributes); + } catch (CacheException e) { + LOGGER.warn("Failed to acquire checksum of fetched file: {}", + e.getMessage()); + _digestValue = Optional.empty(); + } + } else { + _digestValue = Optional.empty(); } } @@ -1127,13 +1140,13 @@ private void addDigestResponseHeader(FileAttributes attributes) { switch (_direction) { case PULL: - if (_wantDigest.isPresent()) { + if (!_wantedDigests.isEmpty()) { response.setHeader("Trailer", "Digest"); } break; case PUSH: - _wantDigest.flatMap(h -> Checksums.digestHeader(h, attributes)) + Checksums.digestHeader(_wantedDigests, attributes) .ifPresent(v -> response.setHeader("Digest", v)); break; } diff --git a/modules/dcache/pom.xml b/modules/dcache/pom.xml index b5c5885f9ea..c020f7b08ef 100644 --- a/modules/dcache/pom.xml +++ b/modules/dcache/pom.xml @@ -390,6 +390,11 @@ everit-json-schema test + + com.github.npathai + hamcrest-optional + test + diff --git a/modules/dcache/src/main/java/org/dcache/http/HttpPoolRequestHandler.java b/modules/dcache/src/main/java/org/dcache/http/HttpPoolRequestHandler.java index 726d3c2ec2d..6f04c73edb9 100644 --- a/modules/dcache/src/main/java/org/dcache/http/HttpPoolRequestHandler.java +++ b/modules/dcache/src/main/java/org/dcache/http/HttpPoolRequestHandler.java @@ -66,12 +66,15 @@ import java.nio.file.StandardOpenOption; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.dcache.namespace.FileAttribute; import org.dcache.pool.movers.NettyTransferService; import org.dcache.pool.movers.RepositoryFileRegion; @@ -132,7 +135,7 @@ public class HttpPoolRequestHandler extends HttpRequestHandler { */ private NettyTransferService.NettyMoverChannel _writeChannel; - private Optional _wantedDigest; + private Set _wantedDigests = EnumSet.noneOf(ChecksumType.class); /** * A simple data class to encapsulate the errors to return by the mover to the pool for file @@ -522,8 +525,10 @@ protected ChannelFuture doOnPut(ChannelHandlerContext context, HttpRequest reque } file.getProtocolInfo().getWantedChecksums().forEach(file::addChecksumType); - _wantedDigest = wantDigest(request).flatMap(Checksums::parseWantDigest); - _wantedDigest.ifPresent(file::addChecksumType); + _wantedDigests = wantDigest(request) + .map(Checksums::parseWantDigest) + .orElseGet(() -> EnumSet.noneOf(ChecksumType.class)); + _wantedDigests.forEach(file::addChecksumType); if (is100ContinueExpected(request)) { context.writeAndFlush(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)) @@ -589,10 +594,11 @@ protected ChannelFuture doOnContent(ChannelHandlerContext context, HttpContent c @Override public void onSuccess(Void result) { try { - Optional digest = _wantedDigest - .flatMap(t -> Checksums.digestHeader(t, - writeChannel.getFileAttributes())); - context.writeAndFlush(new HttpPutResponse(size, location, digest), + Optional digestResponseHeader = + Checksums.digestHeader(_wantedDigests, + writeChannel.getFileAttributes()); + context.writeAndFlush(new HttpPutResponse(size, + location, digestResponseHeader), promise); } catch (IOException e) { context.writeAndFlush( diff --git a/modules/dcache/src/main/java/org/dcache/util/Checksums.java b/modules/dcache/src/main/java/org/dcache/util/Checksums.java index da3a63defbd..45008a704cd 100644 --- a/modules/dcache/src/main/java/org/dcache/util/Checksums.java +++ b/modules/dcache/src/main/java/org/dcache/util/Checksums.java @@ -24,14 +24,19 @@ import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalDouble; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; +import org.dcache.namespace.FileAttribute; import org.dcache.vehicles.FileAttributes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +52,7 @@ public class Checksums { Splitter.on(',').omitEmptyStrings().trimResults(). withKeyValueSeparator(Splitter.on('=').limit(2)); + // String values must be lower case private static final Map CHECKSUMTYPE_TO_RFC3230_NAME = ImmutableMap.builder() .put(ADLER32, "adler32") .put(MD5_TYPE, "md5") @@ -55,6 +61,15 @@ public class Checksums { .put(SHA512, "sha-512") .build(); + // Note: keys are lower-case. + private static final Map RFC3230_NAME_TO_CHECKSUMTYPE; + + static { + var builder = ImmutableMap.builder(); + CHECKSUMTYPE_TO_RFC3230_NAME.forEach((ct,name) -> builder.put(name, ct)); + RFC3230_NAME_TO_CHECKSUMTYPE = builder.build(); + } + public static final boolean isValidRFC3230Name(String s) { return CHECKSUMTYPE_TO_RFC3230_NAME.values().stream() .anyMatch(s::equalsIgnoreCase); @@ -103,7 +118,7 @@ public static final ChecksumType getChecksumTypeForRFC3230Name(String name) { } }; - private static final Ordering PREFERRED_CHECKSUM_TYPE_ORDERING = + public static final Ordering PREFERRED_CHECKSUM_TYPE_ORDERING = Ordering.explicit(SHA512, SHA256, SHA1, MD5_TYPE, ADLER32, MD4_TYPE); private static final Ordering PREFERRED_CHECKSUM_ORDERING = PREFERRED_CHECKSUM_TYPE_ORDERING.onResultOf(Checksum::getType); @@ -166,10 +181,9 @@ public static String buildGenericWantDigest() { } /** - * Choose the best checksum algorithm based on the client's stated preferences and what - * checksums are available. Ties (e.g., client wants either ADLER32 or MD5 with no preference - * with both checksums are available) are resolved by a hard-coded ordering of checksum - * algorithms. The returned value is encoded as a header value for an RFC 3230 Digest header. + * Return RFC 3230 compliant Digest header value, based on requested + * digest algorithm and the available checksum values. All requested + * checksums are returned if available. * * @param wantDigest The client-supplied Want-Digest header * @param attributes The FileAttributes of the targeted file @@ -177,13 +191,30 @@ public static String buildGenericWantDigest() { */ public static Optional digestHeader(@Nullable String wantDigest, FileAttributes attributes) { - return attributes.getChecksumsIfPresent() - .filter(s -> !s.isEmpty()) - .map(s -> s.stream() - .map(Checksum::getType) - .collect(Collectors.toCollection(() -> EnumSet.noneOf(ChecksumType.class)))) - .flatMap(t -> Checksums.parseWantDigest(wantDigest, t)) - .flatMap(t -> digestHeader(t, attributes)); + if (!attributes.isDefined(FileAttribute.CHECKSUM)) { + return Optional.empty(); + } + + Stream checksumTypes = Checksums.parseWantDigestToStream(wantDigest) + .map(QualityValue::value); + + return buildResponseDigestHeader(checksumTypes, attributes); + } + + private static Optional buildResponseDigestHeader(Stream wantedDigests, + FileAttributes attributes) { + + Map knownChecksums = attributes.getChecksums().stream() + .collect(Collectors.toMap(Checksum::getType, c -> c)); + + String digestHeaderValue = wantedDigests + .filter(knownChecksums::containsKey) + .map(knownChecksums::get) + .map(TO_RFC3230_FRAGMENT::apply) + .collect(Collectors.joining(",")); + return digestHeaderValue.isEmpty() + ? Optional.empty() + : Optional.of(digestHeaderValue); } /** @@ -193,12 +224,9 @@ public static Optional digestHeader(@Nullable String wantDigest, * @param attributes The FileAttributes that may contain the directed checksum * @return If checksum is preset then the desired RFC3230-encoded checksum value. */ - public static Optional digestHeader(ChecksumType type, FileAttributes attributes) { - return attributes.getChecksumsIfPresent() - .flatMap(s -> s.stream() - .filter(c -> c.getType() == type) - .findFirst()) - .map(c -> TO_RFC3230_FRAGMENT.apply(c)); + public static Optional digestHeader(Collection checksumTypes, + FileAttributes attributes) { + return buildResponseDigestHeader(checksumTypes.stream(), attributes); } /** @@ -223,15 +251,15 @@ public static Set decodeRfc3230(String digest) { } /** - * Choose the best checksum algorithm based on the client's stated preferences. Ties (e.g., - * client wants either ADLER32 or MD5 with no preference) are resolved by a hard-coded ordering - * of checksum algorithms. - * - * @param wantDigest The value of the RFC 3230 Want-Digest HTTP header. - * @return The best algorithm, if any match. + * Convert a list of Want-Digest checksums to a set of ChecksumType. The + * order is based on the priority (q-value) of the checksums, selecting + * the first algorithms with the same (highest) quality. */ - public static Optional parseWantDigest(String wantDigest) { - return parseWantDigest(wantDigest, EnumSet.allOf(ChecksumType.class)); + public static Set parseWantDigest(String wantDigest) { + return parseWantDigestToStream(wantDigest) + .takeWhile(onlyOneQualitySeen()) + .map(QualityValue::value) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(ChecksumType.class))); } public static Checksum parseContentMd5(String value) { @@ -240,25 +268,41 @@ public static Checksum parseContentMd5(String value) { } /** - * Choose the best checksum algorithm based on the client's stated preferences and what - * checksums are available. + * Parse an RFC 3230 Want-Digest header. Only checksums supported by dCache + * are selected. The supplied checksums are sorted by client preferred + * order. + * @param wantDigest the Want-Digest header. + * @return */ - private static Optional parseWantDigest(@Nullable String wantDigest, - EnumSet allowedTypes) { - return Optional.ofNullable(wantDigest).flatMap(v -> - Splitter.on(',').omitEmptyStrings().trimResults().splitToList(v).stream() + private static Stream> parseWantDigestToStream(@Nullable String wantDigest) { + if (wantDigest == null) { + return Stream.empty(); + } + + List items = Splitter.on(',').omitEmptyStrings().trimResults().splitToList(wantDigest); + return items.stream() .map(QualityValue::of) .filter(q -> q.quality() != 0) - .filter(q -> isValidRFC3230Name(q.value())) - .map(q -> q.mapWith(Checksums::getChecksumTypeForRFC3230Name)) - .filter(q -> allowedTypes.contains(q.value())) + .flatMap(q -> q.flatMap(n -> { + var lowercaseName = n.toLowerCase(); + var type = RFC3230_NAME_TO_CHECKSUMTYPE.get(lowercaseName); + return Optional.ofNullable(type); + }).stream()) .sorted(Comparator.>comparingDouble(q -> q.quality()) .reversed() - .thenComparing(q -> q.value(), PREFERRED_CHECKSUM_TYPE_ORDERING)) - .map(QualityValue::value) - .findFirst()); + .thenComparing(q -> q.value(), PREFERRED_CHECKSUM_TYPE_ORDERING)); } + private static Predicate> onlyOneQualitySeen() { + Set seen = new HashSet<>(); + + return q -> { + double quality = q.quality(); + seen.add(quality); + return seen.size() == 1; + }; +} + public static Ordering preferredOrder() { return PREFERRED_CHECKSUM_ORDERING; } diff --git a/modules/dcache/src/main/java/org/dcache/util/QualityValue.java b/modules/dcache/src/main/java/org/dcache/util/QualityValue.java index 1d116e60645..5ad2a33f7ae 100644 --- a/modules/dcache/src/main/java/org/dcache/util/QualityValue.java +++ b/modules/dcache/src/main/java/org/dcache/util/QualityValue.java @@ -17,6 +17,7 @@ */ package org.dcache.util; +import java.util.Optional; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -82,6 +83,11 @@ public QualityValue mapWith(Function conversion) { return new QualityValue(rawValue, conversion.apply(rawValue), quality); } + public Optional> flatMap(Function> conversion) { + var maybeValue = conversion.apply(rawValue); + return maybeValue.map(v -> new QualityValue(rawValue, v, quality)); + } + /** * The desirability (or quality) of this qvalue. * diff --git a/modules/dcache/src/test/java/org/dcache/util/ChecksumsTests.java b/modules/dcache/src/test/java/org/dcache/util/ChecksumsTests.java index 8b3468a99f5..90f2365e9e0 100644 --- a/modules/dcache/src/test/java/org/dcache/util/ChecksumsTests.java +++ b/modules/dcache/src/test/java/org/dcache/util/ChecksumsTests.java @@ -29,6 +29,8 @@ import java.util.Set; import org.dcache.vehicles.FileAttributes; import org.hamcrest.Description; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import org.hamcrest.TypeSafeMatcher; import org.junit.Test; @@ -409,85 +411,116 @@ public void shouldReturnBothForAdler32AndMd5AndUnknown() { @Test public void shouldFindAdler32AsSingleEntry() { - Optional type = Checksums.parseWantDigest("adler32"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.ADLER32))); + Set type = Checksums.parseWantDigest("adler32"); + assertThat(type, contains(ChecksumType.ADLER32)); + } + + @Test + public void shouldFindUppercaseAdler32AsSingleEntry() { + Set type = Checksums.parseWantDigest("ADLER32"); + assertThat(type, contains(ChecksumType.ADLER32)); + } + + @Test + public void shouldFindMixedcaseAdler32AsSingleEntry() { + Set type = Checksums.parseWantDigest("Adler32"); + assertThat(type, contains(ChecksumType.ADLER32)); } @Test public void shouldFindMd5AsSingleEntry() { - Optional type = Checksums.parseWantDigest("md5"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.MD5_TYPE))); + Set type = Checksums.parseWantDigest("md5"); + assertThat(type, contains(ChecksumType.MD5_TYPE)); + } + + @Test + public void shouldFindUppercaseMd5AsSingleEntry() { + Set type = Checksums.parseWantDigest("MD5"); + assertThat(type, contains(ChecksumType.MD5_TYPE)); } @Test public void shouldFindSha1AsSingleEntry() { - Optional type = Checksums.parseWantDigest("sha"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(SHA1))); + Set type = Checksums.parseWantDigest("sha"); + assertThat(type, contains(SHA1)); + } + + @Test + public void shouldFindUppercaseSha1AsSingleEntry() { + Set type = Checksums.parseWantDigest("SHA"); + assertThat(type, contains(SHA1)); } @Test public void shouldNotFindSha1Explicitly() { - Optional type = Checksums.parseWantDigest("sha-1"); - assertFalse(type.isPresent()); + Set type = Checksums.parseWantDigest("sha-1"); + assertTrue(type.isEmpty()); + } + + @Test + public void shouldNotFindUppercaseSha1Explicitly() { + Set type = Checksums.parseWantDigest("SHA-1"); + assertTrue(type.isEmpty()); } @Test public void shouldFindSha256AsSingleEntry() { - Optional type = Checksums.parseWantDigest("sha-256"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(SHA256))); + Set type = Checksums.parseWantDigest("sha-256"); + assertThat(type, contains(SHA256)); + } + + @Test + public void shouldFindUppercaseSha256AsSingleEntry() { + Set type = Checksums.parseWantDigest("SHA-256"); + assertThat(type, contains(SHA256)); } @Test public void shouldFindSha512AsSingleEntry() { - Optional type = Checksums.parseWantDigest("sha-512"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(SHA512))); + Set type = Checksums.parseWantDigest("sha-512"); + assertThat(type, contains(SHA512)); + } + + @Test + public void shouldFindUppercaseSha512AsSingleEntry() { + Set type = Checksums.parseWantDigest("SHA-512"); + assertThat(type, contains(SHA512)); } @Test public void shouldFindSingleGoodEntryWithQ() { - Optional type = Checksums.parseWantDigest("adler32;q=0.5"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.ADLER32))); + Set type = Checksums.parseWantDigest("adler32;q=0.5"); + assertThat(type, contains(ChecksumType.ADLER32)); } @Test public void shouldSelectSecondAsBestByInternalPreference() { - Optional type = Checksums.parseWantDigest("adler32,md5"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.MD5_TYPE))); + Set type = Checksums.parseWantDigest("adler32,md5"); + assertThat(type, containsInAnyOrder(ChecksumType.ADLER32, ChecksumType.MD5_TYPE)); } @Test public void shouldSelectFirstAsBestByInternalPreference() { - Optional type = Checksums.parseWantDigest("md5,adler32"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.MD5_TYPE))); + Set type = Checksums.parseWantDigest("md5,adler32"); + assertThat(type, containsInAnyOrder(ChecksumType.ADLER32, ChecksumType.MD5_TYPE)); } @Test public void shouldSelectBestByExplicitQ() { - Optional type = Checksums.parseWantDigest("adler32;q=0.5,md5;q=1"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.MD5_TYPE))); + Set type = Checksums.parseWantDigest("adler32;q=0.5,md5;q=1"); + assertThat(type, contains(ChecksumType.MD5_TYPE)); } @Test public void shouldSelectBestByImplicitQ() { - Optional type = Checksums.parseWantDigest("adler32;q=0.5,md5"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.MD5_TYPE))); + Set type = Checksums.parseWantDigest("adler32;q=0.5,md5"); + assertThat(type, contains(ChecksumType.MD5_TYPE)); } @Test public void shouldIgnoreUnknownAlgorithm() { - Optional type = Checksums.parseWantDigest("adler32;q=0.5,UNKNOWN;q=1"); - assertThat(type.isPresent(), is(equalTo(true))); - assertThat(type.get(), is(equalTo(ChecksumType.ADLER32))); + Set type = Checksums.parseWantDigest("adler32;q=0.5,UNKNOWN;q=1"); + assertThat(type, contains(ChecksumType.ADLER32)); } @Test diff --git a/modules/dcache/src/test/java/org/dcache/util/QualityValueTest.java b/modules/dcache/src/test/java/org/dcache/util/QualityValueTest.java index 1b6021586da..f063cb90c47 100644 --- a/modules/dcache/src/test/java/org/dcache/util/QualityValueTest.java +++ b/modules/dcache/src/test/java/org/dcache/util/QualityValueTest.java @@ -17,6 +17,8 @@ */ package org.dcache.util; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty; +import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -24,6 +26,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.Optional; import org.junit.Test; public class QualityValueTest { @@ -97,4 +100,33 @@ public void shouldOrderByQualityWhenReversed() { actual.sort(Comparator.naturalOrder()); assertThat(actual, is(equalTo(expected))); } + + @Test + public void shouldFlatMapOptionalToOptional() { + QualityValue qvalue = QualityValue.of("value"); + + Optional> result = qvalue.flatMap(v -> Optional.empty()); + + assertThat(result, isEmpty()); + } + + @Test + public void shouldFlatMapValueToValue() { + QualityValue qvalue = QualityValue.of("value"); + + Optional> result = qvalue.flatMap(v -> Optional.of(42)); + + assertThat(result, isPresent()); + assertThat(result.get().value(), equalTo(42)); + } + + @Test + public void shouldFlatMapToSameQvalue() { + QualityValue qvalue = QualityValue.of("value;q=0.5"); + + Optional> result = qvalue.flatMap(v -> Optional.of(42)); + + assertThat(result, isPresent()); + assertThat(result.get().quality(), equalTo(0.5)); + } }