001package co.codewizards.cloudstore.core.util; 002 003import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 004import static co.codewizards.cloudstore.core.util.StringUtil.*; 005import static java.util.Objects.*; 006 007import java.net.MalformedURLException; 008import java.net.URI; 009import java.net.URISyntaxException; 010import java.net.URL; 011import java.util.ArrayList; 012import java.util.Collections; 013import java.util.List; 014 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018import co.codewizards.cloudstore.core.oio.File; 019 020public final class UrlUtil { 021 022 private static final Logger logger = LoggerFactory.getLogger(UrlUtil.class); 023 024 public static final String PROTOCOL_FILE = "file"; 025 public static final String PROTOCOL_JAR = "jar"; 026 027 private UrlUtil() { } 028 029 /** 030 * Turns the given {@code url} into a canonical form. 031 * <p> 032 * 033 * @param url the URL to be canonicalized. May be <code>null</code>. 034 * @return the canonicalized URL. Never <code>null</code>, unless the given {@code url} 035 * is <code>null</code>. 036 */ 037 public static URL canonicalizeURL(final URL url) { 038 if (url == null) 039 return null; 040 041 URL result = url; 042 043 String query = url.getQuery(); 044 if (query != null && query.isEmpty()) { 045 query = null; 046 result = null; 047 } 048 049 String path = url.getPath(); 050 while (path.endsWith("/")) { 051 path = path.substring(0, path.length() - 1); 052 result = null; 053 } 054 055 int duplicateSlashIndex = path.indexOf("//"); 056 while (duplicateSlashIndex >= 0) { 057 path = path.substring(0, duplicateSlashIndex) + path.substring(duplicateSlashIndex + 1); 058 059 duplicateSlashIndex = path.indexOf("//"); 060 result = null; 061 } 062 063 if (result == null) { 064 String file = query == null ? path : path + '?' + query; 065 if (isEmpty(url.getHost()) && isEmpty(file)) 066 file = "/"; 067 068 try { 069 result = new URL(url.getProtocol(), url.getHost(), url.getPort(), file); 070 } catch (final MalformedURLException e) { 071 throw new RuntimeException(e); 072 } 073 } 074 return result; 075 } 076 077 public static File getFile(final URL url) { 078 requireNonNull(url, "url"); 079 if (!url.getProtocol().equalsIgnoreCase(PROTOCOL_FILE)) 080 throw new IllegalStateException("url does not reference a local file, i.e. it does not start with 'file:': " + url); 081 082 try { 083 return createFile(url.toURI()); 084 } catch (final URISyntaxException e) { 085 throw new RuntimeException(e); 086 } 087 } 088 089 /** 090 * Appends the URL-encoded {@code path} to the given base {@code url}. 091 * <p> 092 * This method does <i>not</i> use {@link java.net.URLEncoder URLEncoder}, because of 093 * <a href="https://java.net/jira/browse/JERSEY-417">JERSEY-417</a>. 094 * @param url the URL to be appended. Must not be <code>null</code>. 095 * @param path the path to append. May be <code>null</code>. It is assumed that this 096 * path is already encoded. It is therefore <b>not</b> modified at all and appended 097 * as-is. 098 * @return the URL composed of the prefix {@code url} and the suffix {@code path}. 099 * @see #appendNonEncodedPath(URL, String) 100 */ 101 public static URL appendEncodedPath(final URL url, final String path) { 102 requireNonNull(url, "url"); 103 if (path == null || path.isEmpty()) 104 return url; 105 106 return appendEncodedPath(url, Collections.singletonList(path)); 107 } 108 109 /** 110 * Appends the plain {@code path} to the given base {@code url}. 111 * <p> 112 * Each path segment (the text between '/') is separately {@linkplain UrlEncoder URL-encoded}. A 113 * '/' itself is therefore conserved and not encoded. 114 * @param url the URL to be appended. Must not be <code>null</code>. 115 * @param path the path to append. May be <code>null</code>. 116 * @return the URL composed of the prefix {@code url} and the suffix {@code path}. 117 * @see #appendEncodedPath(URL, String) 118 */ 119 public static URL appendNonEncodedPath(final URL url, final String path) { 120 requireNonNull(url, "url"); 121 if (path == null || path.isEmpty()) 122 return url; 123 124 final String[] pathSegments = path.split("/"); 125 final List<String> encodedPathSegments = new ArrayList<String>(pathSegments.length); 126 for (final String pathSegment : pathSegments) { 127 encodedPathSegments.add(UrlEncoder.encode(pathSegment)); 128 } 129 return appendEncodedPath(url, encodedPathSegments); 130 } 131 132 private static URL appendEncodedPath(final URL url, final List<String> pathSegments) { 133 requireNonNull(url, "url"); 134 135 if (pathSegments == null || pathSegments.isEmpty()) 136 return url; 137 138 try { 139 final StringBuilder urlString = new StringBuilder(url.toExternalForm()); 140 141 for (final String ps : pathSegments) { 142 if (ps == null || ps.isEmpty()) 143 continue; 144 145 if (ps.startsWith("/") && getLastChar(urlString) == '/') 146 urlString.append(ps.substring(1)); 147 else if (!ps.startsWith("/") && getLastChar(urlString) != '/') 148 urlString.append('/').append(ps); 149 else 150 urlString.append(ps); 151 } 152 153 return new URL(urlString.toString()); 154 } catch (final MalformedURLException e) { 155 throw new IllegalArgumentException(e); 156 } 157 } 158 159 private static char getLastChar(final StringBuilder stringBuilder) { 160 requireNonNull(stringBuilder, "stringBuilder"); 161 162 final int index = stringBuilder.length() - 1; 163 if (index < 0) 164 return 0; 165 166 return stringBuilder.charAt(index); 167 } 168 169 /** 170 * Convert an URL to an URI. 171 * @param url The URL to cenvert 172 * @return The URI 173 */ 174 public static final URI urlToUri(final URL url) { 175 if (url == null) 176 return null; 177 178 try { 179 return new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), url.getRef()); 180 } catch (final URISyntaxException e) { 181 // Since every URL is an URI, its transformation should never fail. But if it does, we rethrow. 182 throw new RuntimeException(e); 183 } 184 } 185 186 /** 187 * Gets the File referencing the JAR. 188 * 189 * @param url the url to be unwrapped. Must not be <code>null</code>. Must be a JAR-URL (i.e. protocol must be {@link #PROTOCOL_JAR})! 190 * @return the unwrapped URL, i.e. usually the 'file:'-URL pointing to the JAR-URL. 191 */ 192 public static File getFileFromJarUrl(final URL url) { 193 URL fileUrl = getFileUrlFromJarUrl(url); 194 return getFile(fileUrl); 195 } 196 197 /** 198 * Removes the 'jar:'-prefix and the '!...'-suffix in order to unwrap the 'file:'-URL pointing to the JAR. 199 * 200 * @param url the url to be unwrapped. Must not be <code>null</code>. Must be a JAR-URL (i.e. protocol must be {@link #PROTOCOL_JAR})! 201 * @return the unwrapped URL, i.e. usually the 'file:'-URL pointing to the JAR-URL. 202 */ 203 public static URL getFileUrlFromJarUrl(final URL url) { // TODO nested JARs not yet supported! 204 requireNonNull(url, "url"); 205 logger.debug("getFileUrlFromJarUrl: url={}", url); 206 if (!url.getProtocol().equalsIgnoreCase(PROTOCOL_JAR)) 207 throw new IllegalArgumentException("url is not starting with 'jar:': " + url); 208 209 String urlStrWithoutJarPrefix = url.getFile(); 210 final int exclamationMarkIndex = urlStrWithoutJarPrefix.indexOf('!'); 211 if (exclamationMarkIndex >= 0) { 212 urlStrWithoutJarPrefix = urlStrWithoutJarPrefix.substring(0, exclamationMarkIndex); 213 } 214 try { 215 final URL urlWithoutJarPrefixAndSuffix = new URL(urlStrWithoutJarPrefix); 216 logger.debug("getFileUrlFromJarUrl: urlWithoutJarPrefixAndSuffix={}", urlWithoutJarPrefixAndSuffix); 217 return urlWithoutJarPrefixAndSuffix; 218 } catch (final MalformedURLException e) { 219 throw new RuntimeException(e); 220 } 221 } 222}