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}