001/*
002 * Copyright 2012-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.loader.jar;
018
019import java.io.File;
020import java.io.IOException;
021import java.lang.ref.SoftReference;
022import java.lang.reflect.Method;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.net.URLConnection;
026import java.net.URLDecoder;
027import java.net.URLStreamHandler;
028import java.util.Map;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032
033/**
034 * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s.
035 *
036 * @author Phillip Webb
037 * @author Andy Wilkinson
038 * @see JarFile#registerUrlProtocolHandler()
039 */
040public class Handler extends URLStreamHandler {
041
042        // NOTE: in order to be found as a URL protocol handler, this class must be public,
043        // must be named Handler and must be in a package ending '.jar'
044
045        private static final String JAR_PROTOCOL = "jar:";
046
047        private static final String FILE_PROTOCOL = "file:";
048
049        private static final String SEPARATOR = "!/";
050
051        private static final String[] FALLBACK_HANDLERS = {
052                        "sun.net.www.protocol.jar.Handler" };
053
054        private static final Method OPEN_CONNECTION_METHOD;
055
056        static {
057                Method method = null;
058                try {
059                        method = URLStreamHandler.class.getDeclaredMethod("openConnection",
060                                        URL.class);
061                }
062                catch (Exception ex) {
063                        // Swallow and ignore
064                }
065                OPEN_CONNECTION_METHOD = method;
066        }
067
068        private static SoftReference<Map<File, JarFile>> rootFileCache;
069
070        static {
071                rootFileCache = new SoftReference<Map<File, JarFile>>(null);
072        }
073
074        private final JarFile jarFile;
075
076        private URLStreamHandler fallbackHandler;
077
078        public Handler() {
079                this(null);
080        }
081
082        public Handler(JarFile jarFile) {
083                this.jarFile = jarFile;
084        }
085
086        @Override
087        protected URLConnection openConnection(URL url) throws IOException {
088                if (this.jarFile != null) {
089                        return JarURLConnection.get(url, this.jarFile);
090                }
091                try {
092                        return JarURLConnection.get(url, getRootJarFileFromUrl(url));
093                }
094                catch (Exception ex) {
095                        return openFallbackConnection(url, ex);
096                }
097        }
098
099        private URLConnection openFallbackConnection(URL url, Exception reason)
100                        throws IOException {
101                try {
102                        return openConnection(getFallbackHandler(), url);
103                }
104                catch (Exception ex) {
105                        if (reason instanceof IOException) {
106                                log(false, "Unable to open fallback handler", ex);
107                                throw (IOException) reason;
108                        }
109                        log(true, "Unable to open fallback handler", ex);
110                        if (reason instanceof RuntimeException) {
111                                throw (RuntimeException) reason;
112                        }
113                        throw new IllegalStateException(reason);
114                }
115        }
116
117        private void log(boolean warning, String message, Exception cause) {
118                try {
119                        Logger.getLogger(getClass().getName())
120                                        .log((warning ? Level.WARNING : Level.FINEST), message, cause);
121                }
122                catch (Exception ex) {
123                        if (warning) {
124                                System.err.println("WARNING: " + message);
125                        }
126                }
127        }
128
129        private URLStreamHandler getFallbackHandler() {
130                if (this.fallbackHandler != null) {
131                        return this.fallbackHandler;
132                }
133                for (String handlerClassName : FALLBACK_HANDLERS) {
134                        try {
135                                Class<?> handlerClass = Class.forName(handlerClassName);
136                                this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance();
137                                return this.fallbackHandler;
138                        }
139                        catch (Exception ex) {
140                                // Ignore
141                        }
142                }
143                throw new IllegalStateException("Unable to find fallback handler");
144        }
145
146        private URLConnection openConnection(URLStreamHandler handler, URL url)
147                        throws Exception {
148                if (OPEN_CONNECTION_METHOD == null) {
149                        throw new IllegalStateException(
150                                        "Unable to invoke fallback open connection method");
151                }
152                OPEN_CONNECTION_METHOD.setAccessible(true);
153                return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url);
154        }
155
156        @Override
157        protected void parseURL(URL context, String spec, int start, int limit) {
158                if (spec.toLowerCase().startsWith(JAR_PROTOCOL)) {
159                        setFile(context, getFileFromSpec(spec.substring(start, limit)));
160                }
161                else {
162                        setFile(context, getFileFromContext(context, spec.substring(start, limit)));
163                }
164        }
165
166        private String getFileFromSpec(String spec) {
167                int separatorIndex = spec.lastIndexOf("!/");
168                if (separatorIndex == -1) {
169                        throw new IllegalArgumentException("No !/ in spec '" + spec + "'");
170                }
171                try {
172                        new URL(spec.substring(0, separatorIndex));
173                        return spec;
174                }
175                catch (MalformedURLException ex) {
176                        throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex);
177                }
178        }
179
180        private String getFileFromContext(URL context, String spec) {
181                String file = context.getFile();
182                if (spec.startsWith("/")) {
183                        return trimToJarRoot(file) + SEPARATOR + spec.substring(1);
184                }
185                if (file.endsWith("/")) {
186                        return file + spec;
187                }
188                int lastSlashIndex = file.lastIndexOf('/');
189                if (lastSlashIndex == -1) {
190                        throw new IllegalArgumentException(
191                                        "No / found in context URL's file '" + file + "'");
192                }
193                return file.substring(0, lastSlashIndex + 1) + spec;
194        }
195
196        private String trimToJarRoot(String file) {
197                int lastSeparatorIndex = file.lastIndexOf(SEPARATOR);
198                if (lastSeparatorIndex == -1) {
199                        throw new IllegalArgumentException(
200                                        "No !/ found in context URL's file '" + file + "'");
201                }
202                return file.substring(0, lastSeparatorIndex);
203        }
204
205        private void setFile(URL context, String file) {
206                setURL(context, JAR_PROTOCOL, null, -1, null, null, normalize(file), null, null);
207        }
208
209        private String normalize(String file) {
210                int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length();
211                String afterSeparator = file.substring(afterLastSeparatorIndex);
212                afterSeparator = replaceParentDir(afterSeparator);
213                afterSeparator = replaceCurrentDir(afterSeparator);
214                return file.substring(0, afterLastSeparatorIndex) + afterSeparator;
215        }
216
217        private String replaceParentDir(String file) {
218                int parentDirIndex;
219                while ((parentDirIndex = file.indexOf("/../")) >= 0) {
220                        int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1);
221                        if (precedingSlashIndex >= 0) {
222                                file = file.substring(0, precedingSlashIndex)
223                                                + file.substring(parentDirIndex + 3);
224                        }
225                        else {
226                                file = file.substring(parentDirIndex + 4);
227                        }
228                }
229                return file;
230        }
231
232        private String replaceCurrentDir(String file) {
233                return file.replace("/./", "/");
234        }
235
236        @Override
237        protected int hashCode(URL u) {
238                return hashCode(u.getProtocol(), u.getFile());
239        }
240
241        private int hashCode(String protocol, String file) {
242                int result = (protocol == null ? 0 : protocol.hashCode());
243                int separatorIndex = file.indexOf(SEPARATOR);
244                if (separatorIndex == -1) {
245                        return result + file.hashCode();
246                }
247                String source = file.substring(0, separatorIndex);
248                String entry = canonicalize(file.substring(separatorIndex + 2));
249                try {
250                        result += new URL(source).hashCode();
251                }
252                catch (MalformedURLException ex) {
253                        result += source.hashCode();
254                }
255                result += entry.hashCode();
256                return result;
257        }
258
259        @Override
260        protected boolean sameFile(URL u1, URL u2) {
261                if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) {
262                        return false;
263                }
264                int separator1 = u1.getFile().indexOf(SEPARATOR);
265                int separator2 = u2.getFile().indexOf(SEPARATOR);
266                if (separator1 == -1 || separator2 == -1) {
267                        return super.sameFile(u1, u2);
268                }
269                String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length());
270                String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length());
271                if (!nested1.equals(nested2)) {
272                        String canonical1 = canonicalize(nested1);
273                        String canonical2 = canonicalize(nested2);
274                        if (!canonical1.equals(canonical2)) {
275                                return false;
276                        }
277                }
278                String root1 = u1.getFile().substring(0, separator1);
279                String root2 = u2.getFile().substring(0, separator2);
280                try {
281                        return super.sameFile(new URL(root1), new URL(root2));
282                }
283                catch (MalformedURLException ex) {
284                        // Continue
285                }
286                return super.sameFile(u1, u2);
287        }
288
289        private String canonicalize(String path) {
290                return path.replace(SEPARATOR, "/");
291        }
292
293        public JarFile getRootJarFileFromUrl(URL url) throws IOException {
294                String spec = url.getFile();
295                int separatorIndex = spec.indexOf(SEPARATOR);
296                if (separatorIndex == -1) {
297                        throw new MalformedURLException("Jar URL does not contain !/ separator");
298                }
299                String name = spec.substring(0, separatorIndex);
300                return getRootJarFile(name);
301        }
302
303        private JarFile getRootJarFile(String name) throws IOException {
304                try {
305                        if (!name.startsWith(FILE_PROTOCOL)) {
306                                throw new IllegalStateException("Not a file URL");
307                        }
308                        String path = name.substring(FILE_PROTOCOL.length());
309                        File file = new File(URLDecoder.decode(path, "UTF-8"));
310                        Map<File, JarFile> cache = rootFileCache.get();
311                        JarFile result = (cache == null ? null : cache.get(file));
312                        if (result == null) {
313                                result = new JarFile(file);
314                                addToRootFileCache(file, result);
315                        }
316                        return result;
317                }
318                catch (Exception ex) {
319                        throw new IOException("Unable to open root Jar file '" + name + "'", ex);
320                }
321        }
322
323        /**
324         * Add the given {@link JarFile} to the root file cache.
325         * @param sourceFile the source file to add
326         * @param jarFile the jar file.
327         */
328        static void addToRootFileCache(File sourceFile, JarFile jarFile) {
329                Map<File, JarFile> cache = rootFileCache.get();
330                if (cache == null) {
331                        cache = new ConcurrentHashMap<File, JarFile>();
332                        rootFileCache = new SoftReference<Map<File, JarFile>>(cache);
333                }
334                cache.put(sourceFile, jarFile);
335        }
336
337        /**
338         * Set if a generic static exception can be thrown when a URL cannot be connected.
339         * This optimization is used during class loading to save creating lots of exceptions
340         * which are then swallowed.
341         * @param useFastConnectionExceptions if fast connection exceptions can be used.
342         */
343        public static void setUseFastConnectionExceptions(
344                        boolean useFastConnectionExceptions) {
345                JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
346        }
347
348}