Java tutorial
/* * Copyright 2014-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.s3; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A URI wrapper that can parse out information about an S3 URI. */ public class AmazonS3URI { private static final Pattern ENDPOINT_PATTERN = Pattern.compile("^(.+\\.)?s3[.-]([a-z0-9-]+)\\."); private static final Pattern VERSION_ID_PATTERN = Pattern.compile("[&;]"); private final URI uri; private final boolean isPathStyle; private final String bucket; private final String key; private final String versionId; private final String region; /** * Creates a new AmazonS3URI by parsing the given string. * String will be URL encoded before generating the URI. * * @param str the URI to parse. */ public AmazonS3URI(final String str) { this(str, true); } /** * Creates a new AmazonS3URI by parsing the given string. * String will optionally be URL encoded before generating the URI. * URL encoding is recommended if you work with bucket names or object * keys with special characters. This can be disabled if you pre-encode * URI strings before passing them to this class. * * @param str the URI to parse. * @param urlEncode true if string should be URL encoded */ public AmazonS3URI(final String str, final boolean urlEncode) { this(URI.create(preprocessUrlStr(str, urlEncode)), urlEncode); } /** * Creates a new AmazonS3URI by wrapping the given {@code URI}. * * @param uri the URI to wrap */ public AmazonS3URI(final URI uri) { this(uri, false); } private AmazonS3URI(final URI uri, final boolean urlEncode) { if (uri == null) { throw new IllegalArgumentException("uri cannot be null"); } this.uri = uri; // s3://* if ("s3".equalsIgnoreCase(uri.getScheme())) { this.region = null; this.versionId = null; this.isPathStyle = false; this.bucket = uri.getAuthority(); if (bucket == null) { throw new IllegalArgumentException("Invalid S3 URI: no bucket: " + uri); } String path = uri.getPath(); if (path.length() <= 1) { // s3://bucket or s3://bucket/ this.key = null; } else { // s3://bucket/key // Remove the leading '/'. this.key = uri.getPath().substring(1); } return; } String host = uri.getHost(); if (host == null) { throw new IllegalArgumentException("Invalid S3 URI: no hostname: " + uri); } Matcher matcher = ENDPOINT_PATTERN.matcher(host); if (!matcher.find()) { throw new IllegalArgumentException( "Invalid S3 URI: hostname does not appear to be a valid S3 " + "endpoint: " + uri); } String prefix = matcher.group(1); if (prefix == null || prefix.isEmpty()) { // No bucket name in the authority; parse it from the path. this.isPathStyle = true; // Use the raw path to avoid running afoul of '/'s in the // bucket name if we have not performed full URL encoding String path = urlEncode ? uri.getPath() : uri.getRawPath(); if ("".equals(path) || "/".equals(path)) { this.bucket = null; this.key = null; } else { int index = path.indexOf('/', 1); if (index == -1) { // https://s3.amazonaws.com/bucket this.bucket = decode(path.substring(1)); this.key = null; } else if (index == (path.length() - 1)) { // https://s3.amazonaws.com/bucket/ this.bucket = decode(path.substring(1, index)); this.key = null; } else { // https://s3.amazonaws.com/bucket/key this.bucket = decode(path.substring(1, index)); this.key = decode(path.substring(index + 1)); } } } else { // Bucket name was found in the host; path is the key. this.isPathStyle = false; // Remove the trailing '.' from the prefix to get the bucket. this.bucket = prefix.substring(0, prefix.length() - 1); String path = uri.getPath(); if (path == null || path.isEmpty() || "/".equals(uri.getPath())) { this.key = null; } else { // Remove the leading '/'. this.key = uri.getPath().substring(1); } } this.versionId = parseVersionId(uri.getRawQuery()); if ("amazonaws".equals(matcher.group(2))) { // No region specified this.region = null; } else { this.region = matcher.group(2); } } /** * Attempts to parse a versionId parameter from the query * string. * * @param query the query string to parse (possibly null) * @return the versionId (possibly null) */ private static String parseVersionId(String query) { if (query != null) { String[] params = VERSION_ID_PATTERN.split(query); for (String param : params) { if (param.startsWith("versionId=")) { return decode(param.substring(10)); } } } return null; } /** * @return the S3 URI being parsed */ public URI getURI() { return uri; } /** * @return true if the URI contains the bucket in the path, false if it * contains the bucket in the authority */ public boolean isPathStyle() { return isPathStyle; } /** * @return the bucket name parsed from the URI (or null if no bucket * specified) */ public String getBucket() { return bucket; } /** * @return the key parsed from the URI (or null if no key specified) */ public String getKey() { return key; } /** * @return the version id parsed from the URI (or null if no version specified) */ public String getVersionId() { return versionId; } /** * @return the region parsed from the URI (or null if no region specified) */ public String getRegion() { return region; } @Override public String toString() { return uri.toString(); } /** * URL encodes the given string. This allows us to pass special characters * that would otherwise be rejected when building a URI instance. Because we * need to retain the URI's path structure we subsequently need to replace * percent encoded path delimiters back to their decoded counterparts. * * @param str the string to encode * @return the encoded string */ private static String preprocessUrlStr(final String str, final boolean encode) { if (encode) { try { return (URLEncoder.encode(str, "UTF-8").replace("%3A", ":").replace("%2F", "/").replace("+", "%20")); } catch (UnsupportedEncodingException e) { // This should never happen unless there is something // fundamentally broken with the running JVM. throw new RuntimeException(e); } } return str; } /** * Percent-decodes the given string, with a fast path for strings that * are not percent-encoded. * * @param str the string to decode * @return the decoded string */ private static String decode(final String str) { if (str == null) { return null; } for (int i = 0; i < str.length(); ++i) { if (str.charAt(i) == '%') { return decode(str, i); } } return str; } /** * Percent-decodes the given string. * * @param str the string to decode * @param firstPercent the index of the first '%' character in the string * @return the decoded string */ private static String decode(final String str, final int firstPercent) { StringBuilder builder = new StringBuilder(); builder.append(str.substring(0, firstPercent)); appendDecoded(builder, str, firstPercent); for (int i = firstPercent + 3; i < str.length(); ++i) { if (str.charAt(i) == '%') { appendDecoded(builder, str, i); i += 2; } else { builder.append(str.charAt(i)); } } return builder.toString(); } /** * Decodes the percent-encoded character at the given index in the string * and appends the decoded value to the given {@code StringBuilder}. * * @param builder the string builder to append to * @param str the string being decoded * @param index the index of the '%' character in the string */ private static void appendDecoded(final StringBuilder builder, final String str, final int index) { if (index > str.length() - 3) { throw new IllegalStateException("Invalid percent-encoded string:" + "\"" + str + "\"."); } char first = str.charAt(index + 1); char second = str.charAt(index + 2); char decoded = (char) ((fromHex(first) << 4) | fromHex(second)); builder.append(decoded); } /** * Converts a hex character (0-9A-Fa-f) into its corresponding quad value. * * @param c the hex character * @return the quad value */ private static int fromHex(final char c) { if (c < '0') { throw new IllegalStateException( "Invalid percent-encoded string: bad character '" + c + "' in " + "escape sequence."); } if (c <= '9') { return (c - '0'); } if (c < 'A') { throw new IllegalStateException( "Invalid percent-encoded string: bad character '" + c + "' in " + "escape sequence."); } if (c <= 'F') { return (c - 'A') + 10; } if (c < 'a') { throw new IllegalStateException( "Invalid percent-encoded string: bad character '" + c + "' in " + "escape sequence."); } if (c <= 'f') { return (c - 'a') + 10; } throw new IllegalStateException( "Invalid percent-encoded string: bad character '" + c + "' in " + "escape sequence."); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AmazonS3URI that = (AmazonS3URI) o; if (isPathStyle != that.isPathStyle) return false; if (!uri.equals(that.uri)) return false; if (bucket != null ? !bucket.equals(that.bucket) : that.bucket != null) return false; if (key != null ? !key.equals(that.key) : that.key != null) return false; if (versionId != null ? !versionId.equals(that.versionId) : that.versionId != null) return false; return region != null ? region.equals(that.region) : that.region == null; } @Override public int hashCode() { int result = uri.hashCode(); result = 31 * result + (isPathStyle ? 1 : 0); result = 31 * result + (bucket != null ? bucket.hashCode() : 0); result = 31 * result + (key != null ? key.hashCode() : 0); result = 31 * result + (versionId != null ? versionId.hashCode() : 0); result = 31 * result + (region != null ? region.hashCode() : 0); return result; } }