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