This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 54f97965a8 Allow HTTP connection from an URI with cache in a temporary
file.
54f97965a8 is described below
commit 54f97965a83d6952c27e36cc696172973fae802d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Dec 23 15:55:16 2022 +0100
Allow HTTP connection from an URI with cache in a temporary file.
---
.../apache/sis/cloud/aws/s3/CachedByteChannel.java | 16 +--
.../apache/sis/internal/jaxb/referencing/Code.java | 2 +-
.../apache/sis/test/integration/MetadataTest.java | 5 +-
.../org/apache/sis/internal/util/Constants.java | 6 +-
.../apache/sis/internal/util/DefinitionURI.java | 6 +-
.../apache/sis/storage/netcdf/MetadataReader.java | 2 +-
.../org/apache/sis/internal/storage/CodeType.java | 9 +-
.../sis/internal/storage/io/ChannelFactory.java | 12 +-
.../internal/storage/io/FileCacheByteChannel.java | 73 ++++++++----
.../sis/internal/storage/io/HttpByteChannel.java | 130 +++++++++++++++++++++
.../sis/internal/storage/io/IOUtilities.java | 13 +++
11 files changed, 224 insertions(+), 50 deletions(-)
diff --git
a/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
b/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
index dc7800160a..4bb3ae9cf1 100644
---
a/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
+++
b/cloud/sis-cloud-aws/src/main/java/org/apache/sis/cloud/aws/s3/CachedByteChannel.java
@@ -70,19 +70,15 @@ final class CachedByteChannel extends FileCacheByteChannel {
* @return contains the input stream providing the bytes to read starting
at the given start position.
*/
@Override
- protected Connection openConnection(long start, long end) throws
IOException {
+ protected Connection openConnection(final long start, final long end)
throws IOException {
final ResponseInputStream<GetObjectResponse> stream;
final String contentRange, acceptRanges;
final Long contentLength;
try {
GetObjectRequest.Builder builder =
GetObjectRequest.builder().bucket(path.bucket).key(path.key);
- final boolean hasEnd = (end > start) && (end != Long.MAX_VALUE);
- if (start != 0 || hasEnd) {
- final StringBuilder range = new
StringBuilder(RANGES_UNIT).append('=').append(start);
- if (hasEnd) {
- range.append('-').append(end); // Inclusive.
- }
- builder = builder.range(range.toString());
+ final String range = Connection.formatRange(start, end);
+ if (range != null) {
+ builder = builder.range(range);
}
stream = path.fs.client().getObject(builder.build());
final GetObjectResponse response = stream.response();
@@ -94,9 +90,7 @@ final class CachedByteChannel extends FileCacheByteChannel {
}
final List<String> rangeUnits = (acceptRanges != null) ?
List.of(acceptRanges) : List.of();
final long length = (contentLength != null) ? contentLength : -1;
- if (contentRange == null) {
- return new Connection(stream, 0, (length < 0) ? Long.MAX_VALUE :
length, length, Connection.acceptRanges(rangeUnits));
- } else try {
+ try {
return new Connection(stream, contentRange, length, rangeUnits);
} catch (IllegalArgumentException e) {
throw new IOException(e);
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/Code.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/Code.java
index 48a5d53ad2..e096f6d20c 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/Code.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/jaxb/referencing/Code.java
@@ -176,7 +176,7 @@ public final class Code {
return new Code(identifier);
}
if (!isHTTP) {
- isHTTP = code.regionMatches(true, 0, Constants.HTTP, 0, 5);
+ isHTTP = code.regionMatches(true, 0, Constants.HTTP + ':',
0, 5);
if (isHTTP) {
fallback = identifier;
} else if (!isEPSG) {
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/MetadataTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/MetadataTest.java
index 5538a1dfb0..1d3950bedc 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/MetadataTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/MetadataTest.java
@@ -57,6 +57,7 @@ import
org.apache.sis.internal.jaxb.metadata.replace.ReferenceSystemMetadata;
import org.apache.sis.internal.xml.LegacyNamespaces;
import org.apache.sis.internal.jaxb.gcx.Anchor;
import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.util.Constants;
import org.apache.sis.util.SimpleInternationalString;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.xml.Namespaces;
@@ -136,7 +137,7 @@ public final strictfp class MetadataTest extends TestCase {
final Anchor country = new Anchor(URI.create("SDN:C320:2:FR"),
"France"); // Non-public SIS class.
{
final DefaultOnlineResource online = new
DefaultOnlineResource(URI.create("http://www.ifremer.fr/sismer/"));
- online.setProtocol("http");
+ online.setProtocol(Constants.HTTP);
final DefaultContact contact = new DefaultContact(online);
contact.getIdentifierMap().putSpecialized(IdentifierSpace.ID,
"IFREMER");
contact.setPhones(List.of(
@@ -166,7 +167,7 @@ public final strictfp class MetadataTest extends TestCase {
@SuppressWarnings("deprecation")
final DefaultResponsibility originator = new
DefaultResponsibleParty(Role.ORIGINATOR);
final DefaultOnlineResource online = new
DefaultOnlineResource(URI.create("http://www.com.univ-mrs.fr/LOB/"));
- online.setProtocol("http");
+ online.setProtocol(Constants.HTTP);
final DefaultContact contact = new DefaultContact(online);
contact.setPhones(List.of(
new DefaultTelephone("+33 (0)4 xx.xx.xx.x5",
TelephoneType.VOICE),
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
index 82a2c9e042..9a38995c0a 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java
@@ -31,7 +31,7 @@ import org.apache.sis.util.Static;
* creates itself the instance to be tested.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 0.5
* @module
*/
@@ -53,9 +53,9 @@ public final class Constants extends Static {
public static final byte DEFAULT_INDENTATION = 2;
/**
- * The {@value} protocol. Used in XML namespaces.
+ * The {@value} protocol.
*/
- public static final String HTTP = "http:";
+ public static final String HTTP = "http", HTTPS = "https";
/**
* The {@value} code space.
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
index 5129f2865e..54a23c4e3e 100644
---
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
+++
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java
@@ -329,7 +329,7 @@ public final class DefinitionURI {
* in addition to "ogc" in URN.
*/
case 0: {
- if (regionMatches("http", uri, lower, upper)) {
+ if (regionMatches(Constants.HTTP, uri, lower, upper)) {
result = new DefinitionURI();
result.isHTTP = true;
if (codeForGML(null, null, uri, ++upper, result) !=
null) {
@@ -651,7 +651,7 @@ public final class DefinitionURI {
if (isGML) {
final String path = PATHS.get(type);
if (path != null) {
- return Constants.HTTP + path + authority + ".xml#" + code;
+ return Constants.HTTP + ':' + path + authority + ".xml#" +
code;
}
}
final StringBuilder buffer = new StringBuilder(40);
@@ -671,7 +671,7 @@ public final class DefinitionURI {
*/
private void appendStringTo(final StringBuilder buffer, char separator) {
if (isHTTP) {
- buffer.append(Constants.HTTP + "//").append(DOMAIN).append("/def");
+ buffer.append(Constants.HTTP +
"://").append(DOMAIN).append("/def");
separator = '/';
}
int n = 4;
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index 55385907ce..4ee557155e 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -386,7 +386,7 @@ split: while ((start =
CharSequences.skipLeadingWhitespaces(value, start, lengt
final DefaultOnlineResource resource = new DefaultOnlineResource(uri);
final String protocol = uri.getScheme();
resource.setProtocol(protocol);
- if ("http".equalsIgnoreCase(protocol) ||
"https".equalsIgnoreCase(protocol)) {
+ if (IOUtilities.isHTTP(protocol)) {
resource.setApplicationProfile("web browser");
}
resource.setFunction(OnLineFunction.INFORMATION);
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/CodeType.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/CodeType.java
index 419d6fbf53..9e05c61d74 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/CodeType.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/CodeType.java
@@ -21,6 +21,7 @@ import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import org.apache.sis.util.CharSequences;
+import org.apache.sis.internal.util.Constants;
import org.apache.sis.internal.util.DefinitionURI;
@@ -94,10 +95,10 @@ public enum CodeType {
private static final Map<String,CodeType> FOR_PROTOCOL;
static {
FOR_PROTOCOL = new HashMap<>();
- FOR_PROTOCOL.put("urn", CodeType.URN);
- FOR_PROTOCOL.put("http", CodeType.HTTP_OGC); // Will actually need
verification.
- FOR_PROTOCOL.put("https", CodeType.HTTP_OGC); // Will actually need
verification.
- FOR_PROTOCOL.put("shttp", CodeType.HTTP_OGC); // Not widely used but
nevertheless exist.
+ FOR_PROTOCOL.put("urn", CodeType.URN);
+ FOR_PROTOCOL.put(Constants.HTTP, CodeType.HTTP_OGC); // Will
actually need verification.
+ FOR_PROTOCOL.put(Constants.HTTPS, CodeType.HTTP_OGC); // Will
actually need verification.
+ FOR_PROTOCOL.put("shttp", CodeType.HTTP_OGC); // Not widely
used but nevertheless exist.
for (final String p : new String[] {"cvs", "dav", "file", "ftp",
"git", "jar", "nfs", "sftp", "ssh", "svn"}) {
if (FOR_PROTOCOL.put(p, CodeType.URL) != null) {
throw new AssertionError(p);
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
index 943a8fe371..bec2f32eb7 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
@@ -69,7 +69,7 @@ import org.apache.sis.storage.event.StoreListeners;
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 0.8
* @module
*/
@@ -224,6 +224,16 @@ public abstract class ChannelFactory {
* so we are better to check now and provide a more
appropriate exception for this method.
*/
throw new
IOException(Resources.format(Resources.Keys.MissingSchemeInURI_1, uri));
+ }
+ if (IOUtilities.isHTTP(uri.getScheme())) {
+ return new ChannelFactory(false) {
+ @Override public ReadableByteChannel readable(String
filename, StoreListeners listeners) throws IOException {
+ return new HttpByteChannel(filename, uri);
+ }
+ @Override public WritableByteChannel writable(String
filename, StoreListeners listeners) throws IOException {
+ return
Channels.newChannel(uri.toURL().openConnection().getOutputStream());
+ }
+ };
} else try {
storage = Paths.get(uri);
} catch (IllegalArgumentException | FileSystemNotFoundException e)
{
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/FileCacheByteChannel.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/FileCacheByteChannel.java
index 6ca122e7bd..092fd52bc0 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/FileCacheByteChannel.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/FileCacheByteChannel.java
@@ -73,11 +73,6 @@ public abstract class FileCacheByteChannel implements
SeekableByteChannel {
*/
static final int SKIP_THRESHOLD = 64 * 1024;
- /**
- * The unit of ranges used in HTTP connections.
- */
- protected static final String RANGES_UNIT = "bytes";
-
/**
* Number of nanoseconds to wait before to close an inactive connection.
*/
@@ -88,6 +83,9 @@ public abstract class FileCacheByteChannel implements
SeekableByteChannel {
* This is the return value of {@link #openConnection(long, long)}.
*/
protected static final class Connection extends
org.apache.sis.internal.jdk17.Record {
+ /** The unit of ranges used in HTTP connections. */
+ private static final String RANGES_UNIT = "bytes";
+
/** The input stream for reading the bytes. */
final InputStream input;
@@ -127,29 +125,35 @@ public abstract class FileCacheByteChannel implements
SeekableByteChannel {
* Example: "Content-Range: bytes 25000-75000/100000".
*
* @param input the input stream for reading the bytes.
- * @param contentRange value of "Content-Range" in HTTP header.
+ * @param contentRange value of "Content-Range" in HTTP header, or
{@code null} if none.
* @param acceptRanges value of "Accept-Ranges" in HTTP header.
* @param contentLength total length of the stream, or -1 if unknown.
- * @throws IllegalArgumentException if the start, end of length cannot
be parsed.
+ * @throws IllegalArgumentException if the start, end or length cannot
be parsed.
*/
public Connection(final InputStream input, String contentRange, long
contentLength, final Iterable<String> acceptRanges) {
this.input = input;
- contentRange = contentRange.trim();
- int s = contentRange.indexOf(' ');
- if (s >= 0 && (s != RANGES_UNIT.length() ||
!contentRange.regionMatches(true, 0, RANGES_UNIT, 0, s))) {
- throw new
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedArgumentValue_1,
contentRange));
- }
- int rs = contentRange.indexOf('-', ++s); //
Index of range separator.
- int ls = contentRange.indexOf('/', Math.max(s, rs+1)); //
Index of length separator.
- if (contentLength < 0 && ls >= 0) {
- final String t = contentRange.substring(ls+1).trim();
- if (!t.equals("*")) contentLength = Long.parseLong(t);
+ if (contentRange == null) {
+ start = 0;
+ end = (contentLength > 0) ? contentLength - 1 :
Long.MAX_VALUE;
+ length = contentLength;
+ } else {
+ contentRange = contentRange.trim();
+ int s = contentRange.indexOf(' ');
+ if (s >= 0 && (s != RANGES_UNIT.length() ||
!contentRange.regionMatches(true, 0, RANGES_UNIT, 0, s))) {
+ throw new
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedArgumentValue_1,
contentRange));
+ }
+ int rs = contentRange.indexOf('-', ++s); //
Index of range separator.
+ int ls = contentRange.indexOf('/', Math.max(s, rs+1)); //
Index of length separator.
+ if (contentLength < 0 && ls >= 0) {
+ final String t = contentRange.substring(ls+1).trim();
+ if (!t.equals("*")) contentLength = Long.parseLong(t);
+ }
+ length = contentLength;
+ if (ls < 0) ls = contentRange.length();
+ if (rs < 0) rs = ls;
+ start = Long.parseLong(contentRange.substring(s, rs).trim());
+ end = (rs < ls) ? Long.parseLong(contentRange.substring(rs+1,
ls).trim()) : length;
}
- length = contentLength;
- if (ls < 0) ls = contentRange.length();
- if (rs < 0) rs = ls;
- start = Long.parseLong(contentRange.substring(s, rs).trim());
- end = (rs < ls) ? Long.parseLong(contentRange.substring(rs+1,
ls).trim()) : length;
this.acceptRanges = acceptRanges(acceptRanges);
}
@@ -159,7 +163,7 @@ public abstract class FileCacheByteChannel implements
SeekableByteChannel {
* @param values HTTP header value for "Accept-Ranges".
* @return whether the values contains at least one "bytes" string.
*/
- public static boolean acceptRanges(final Iterable<String> values) {
+ private static boolean acceptRanges(final Iterable<String> values) {
for (final String t : values) {
if (ArraysExt.containsIgnoreCase((String[])
CharSequences.split(t, ','), RANGES_UNIT)) {
return true;
@@ -168,12 +172,33 @@ public abstract class FileCacheByteChannel implements
SeekableByteChannel {
return false;
}
+ /**
+ * Formats the "Range" value to send in an HTTP header for the
specified range of bytes.
+ * This is a helper method for {@link #openConnection(long, long)}
implementations.
+ *
+ * @param start position of the first byte to read (inclusive).
+ * @param end position of the last byte to read with the returned
stream (inclusive),
+ * or {@link Long#MAX_VALUE} for end of stream.
+ * @return
+ */
+ public static String formatRange(final long start, final long end) {
+ final boolean hasEnd = (end > start) && (end != Long.MAX_VALUE);
+ if (start == 0 && !hasEnd) {
+ return null;
+ }
+ final StringBuilder range = new
StringBuilder(RANGES_UNIT).append('=').append(start).append('-');
+ if (hasEnd) {
+ range.append(end); // Inclusive.
+ }
+ return range.toString();
+ }
+
/**
* Returns a string representation for debugging purposes.
*/
@Override
public String toString() {
- return Strings.toString(getClass(), "start", start, "end", end);
+ return Strings.toString(getClass(), null, formatRange(start, end));
}
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HttpByteChannel.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HttpByteChannel.java
new file mode 100644
index 0000000000..fad086563a
--- /dev/null
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/HttpByteChannel.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.io;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+
+/**
+ * A seekable byte channel on a HTTP connection.
+ * This implementation use HTTP range for reading bytes at an arbitrary
position.
+ * A temporary file is used for caching the bytes that have been read.
+ *
+ * @author Alexis Manin (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ * @module
+ */
+final class HttpByteChannel extends FileCacheByteChannel {
+ /**
+ * Data store name to report in case of failure.
+ */
+ private final String filename;
+
+ /**
+ * The request to be sent to the client, without "Range" header.
+ * This builder contains the {@linkplain #path} to the file.
+ */
+ private final HttpRequest.Builder request;
+
+ /**
+ * The client where to send HTTP requests.
+ */
+ private final HttpClient client;
+
+ /**
+ * The singleton client used for HTTP connections.
+ */
+ private static WeakReference<HttpClient> sharedClient;
+
+ /**
+ * Gets or create the singleton client used for HTTP connections.
+ */
+ private static synchronized HttpClient sharedClient() {
+ if (sharedClient != null) {
+ HttpClient client = sharedClient.get();
+ if (client != null) return client;
+ }
+ HttpClient client = HttpClient.newHttpClient();
+ sharedClient = new WeakReference<>(client);
+ return client;
+ }
+
+ /**
+ * Creates a new channel for a file at the given URI.
+ *
+ * @param name data store name to report in case of failure.
+ * @param path URL to the file to read.
+ * @throws IOException if the temporary file can not be created.
+ */
+ public HttpByteChannel(final String name, final URI path) throws
IOException {
+ super("http-");
+ filename = name;
+ request = HttpRequest.newBuilder(path);
+ client = sharedClient();
+ }
+
+ /**
+ * Returns the data store name to report in case of failure.
+ */
+ @Override
+ protected String filename() {
+ return filename;
+ }
+
+ /**
+ * Creates an input stream which provides the bytes to read starting at
the specified position.
+ *
+ * @param start position of the first byte to read (inclusive).
+ * @param end position of the last byte to read with the returned
stream (inclusive),
+ * or {@link Long#MAX_VALUE} for end of stream.
+ * @return contains the input stream providing the bytes to read starting
at the given start position.
+ */
+ @Override
+ protected Connection openConnection(final long start, final long end)
throws IOException {
+ HttpRequest.Builder r = request;
+ String range = Connection.formatRange(start, end);
+ if (range != null) {
+ r = r.copy().setHeader("Range", range);
+ }
+ final HttpResponse<InputStream> response;
+ try {
+ response = client.send(r.build(),
HttpResponse.BodyHandlers.ofInputStream());
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+ final InputStream stream = response.body();
+ final HttpHeaders headers = response.headers();
+ range = headers.firstValue("Content-Range").orElse(null);
+ final List<String> rangeUnits = headers.allValues("Accept-Ranges");
+ try {
+ final long length =
headers.firstValueAsLong("Content-Length").orElse(-1);
+ return new Connection(stream, range, length, rangeUnits);
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/IOUtilities.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/IOUtilities.java
index 60616119ad..8f613a9d1e 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/IOUtilities.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/IOUtilities.java
@@ -45,6 +45,7 @@ import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.Static;
import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.util.Constants;
import org.apache.sis.internal.storage.Resources;
@@ -673,6 +674,18 @@ public final class IOUtilities extends Static {
return isWrite & (!isRead | truncate);
}
+ /**
+ * Returns {@code true} if the given protocol is "http" or "https".
+ * The comparison is case-insensitive.
+ *
+ * @param protocol the protocol to test.
+ * @return whether the given protocol is HTTP(S).
+ */
+ public static boolean isHTTP(final String protocol) {
+ return Constants.HTTP .equalsIgnoreCase(protocol)
+ || Constants.HTTPS.equalsIgnoreCase(protocol);
+ }
+
/**
* Reads the next character as an Unicode code point. Unless end-of-file
has been reached, the returned value is
* between {@value java.lang.Character#MIN_CODE_POINT} and {@value
java.lang.Character#MAX_CODE_POINT} inclusive.