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}