001/*
002 * Copyright 2002-2020 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 *      https://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.core.io;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.HttpURLConnection;
023import java.net.MalformedURLException;
024import java.net.URI;
025import java.net.URISyntaxException;
026import java.net.URL;
027import java.net.URLConnection;
028
029import org.springframework.lang.Nullable;
030import org.springframework.util.Assert;
031import org.springframework.util.ResourceUtils;
032import org.springframework.util.StringUtils;
033
034/**
035 * {@link Resource} implementation for {@code java.net.URL} locators.
036 * Supports resolution as a {@code URL} and also as a {@code File} in
037 * case of the {@code "file:"} protocol.
038 *
039 * @author Juergen Hoeller
040 * @since 28.12.2003
041 * @see java.net.URL
042 */
043public class UrlResource extends AbstractFileResolvingResource {
044
045        /**
046         * Original URI, if available; used for URI and File access.
047         */
048        @Nullable
049        private final URI uri;
050
051        /**
052         * Original URL, used for actual access.
053         */
054        private final URL url;
055
056        /**
057         * Cleaned URL (with normalized path), used for comparisons.
058         */
059        @Nullable
060        private volatile URL cleanedUrl;
061
062
063        /**
064         * Create a new {@code UrlResource} based on the given URI object.
065         * @param uri a URI
066         * @throws MalformedURLException if the given URL path is not valid
067         * @since 2.5
068         */
069        public UrlResource(URI uri) throws MalformedURLException {
070                Assert.notNull(uri, "URI must not be null");
071                this.uri = uri;
072                this.url = uri.toURL();
073        }
074
075        /**
076         * Create a new {@code UrlResource} based on the given URL object.
077         * @param url a URL
078         */
079        public UrlResource(URL url) {
080                Assert.notNull(url, "URL must not be null");
081                this.uri = null;
082                this.url = url;
083        }
084
085        /**
086         * Create a new {@code UrlResource} based on a URL path.
087         * <p>Note: The given path needs to be pre-encoded if necessary.
088         * @param path a URL path
089         * @throws MalformedURLException if the given URL path is not valid
090         * @see java.net.URL#URL(String)
091         */
092        public UrlResource(String path) throws MalformedURLException {
093                Assert.notNull(path, "Path must not be null");
094                this.uri = null;
095                this.url = new URL(path);
096                this.cleanedUrl = getCleanedUrl(this.url, path);
097        }
098
099        /**
100         * Create a new {@code UrlResource} based on a URI specification.
101         * <p>The given parts will automatically get encoded if necessary.
102         * @param protocol the URL protocol to use (e.g. "jar" or "file" - without colon);
103         * also known as "scheme"
104         * @param location the location (e.g. the file path within that protocol);
105         * also known as "scheme-specific part"
106         * @throws MalformedURLException if the given URL specification is not valid
107         * @see java.net.URI#URI(String, String, String)
108         */
109        public UrlResource(String protocol, String location) throws MalformedURLException  {
110                this(protocol, location, null);
111        }
112
113        /**
114         * Create a new {@code UrlResource} based on a URI specification.
115         * <p>The given parts will automatically get encoded if necessary.
116         * @param protocol the URL protocol to use (e.g. "jar" or "file" - without colon);
117         * also known as "scheme"
118         * @param location the location (e.g. the file path within that protocol);
119         * also known as "scheme-specific part"
120         * @param fragment the fragment within that location (e.g. anchor on an HTML page,
121         * as following after a "#" separator)
122         * @throws MalformedURLException if the given URL specification is not valid
123         * @see java.net.URI#URI(String, String, String)
124         */
125        public UrlResource(String protocol, String location, @Nullable String fragment) throws MalformedURLException  {
126                try {
127                        this.uri = new URI(protocol, location, fragment);
128                        this.url = this.uri.toURL();
129                }
130                catch (URISyntaxException ex) {
131                        MalformedURLException exToThrow = new MalformedURLException(ex.getMessage());
132                        exToThrow.initCause(ex);
133                        throw exToThrow;
134                }
135        }
136
137
138        /**
139         * Determine a cleaned URL for the given original URL.
140         * @param originalUrl the original URL
141         * @param originalPath the original URL path
142         * @return the cleaned URL (possibly the original URL as-is)
143         * @see org.springframework.util.StringUtils#cleanPath
144         */
145        private static URL getCleanedUrl(URL originalUrl, String originalPath) {
146                String cleanedPath = StringUtils.cleanPath(originalPath);
147                if (!cleanedPath.equals(originalPath)) {
148                        try {
149                                return new URL(cleanedPath);
150                        }
151                        catch (MalformedURLException ex) {
152                                // Cleaned URL path cannot be converted to URL -> take original URL.
153                        }
154                }
155                return originalUrl;
156        }
157
158        /**
159         * Lazily determine a cleaned URL for the given original URL.
160         * @see #getCleanedUrl(URL, String)
161         */
162        private URL getCleanedUrl() {
163                URL cleanedUrl = this.cleanedUrl;
164                if (cleanedUrl != null) {
165                        return cleanedUrl;
166                }
167                cleanedUrl = getCleanedUrl(this.url, (this.uri != null ? this.uri : this.url).toString());
168                this.cleanedUrl = cleanedUrl;
169                return cleanedUrl;
170        }
171
172
173        /**
174         * This implementation opens an InputStream for the given URL.
175         * <p>It sets the {@code useCaches} flag to {@code false},
176         * mainly to avoid jar file locking on Windows.
177         * @see java.net.URL#openConnection()
178         * @see java.net.URLConnection#setUseCaches(boolean)
179         * @see java.net.URLConnection#getInputStream()
180         */
181        @Override
182        public InputStream getInputStream() throws IOException {
183                URLConnection con = this.url.openConnection();
184                ResourceUtils.useCachesIfNecessary(con);
185                try {
186                        return con.getInputStream();
187                }
188                catch (IOException ex) {
189                        // Close the HTTP connection (if applicable).
190                        if (con instanceof HttpURLConnection) {
191                                ((HttpURLConnection) con).disconnect();
192                        }
193                        throw ex;
194                }
195        }
196
197        /**
198         * This implementation returns the underlying URL reference.
199         */
200        @Override
201        public URL getURL() {
202                return this.url;
203        }
204
205        /**
206         * This implementation returns the underlying URI directly,
207         * if possible.
208         */
209        @Override
210        public URI getURI() throws IOException {
211                if (this.uri != null) {
212                        return this.uri;
213                }
214                else {
215                        return super.getURI();
216                }
217        }
218
219        @Override
220        public boolean isFile() {
221                if (this.uri != null) {
222                        return super.isFile(this.uri);
223                }
224                else {
225                        return super.isFile();
226                }
227        }
228
229        /**
230         * This implementation returns a File reference for the underlying URL/URI,
231         * provided that it refers to a file in the file system.
232         * @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String)
233         */
234        @Override
235        public File getFile() throws IOException {
236                if (this.uri != null) {
237                        return super.getFile(this.uri);
238                }
239                else {
240                        return super.getFile();
241                }
242        }
243
244        /**
245         * This implementation creates a {@code UrlResource}, delegating to
246         * {@link #createRelativeURL(String)} for adapting the relative path.
247         * @see #createRelativeURL(String)
248         */
249        @Override
250        public Resource createRelative(String relativePath) throws MalformedURLException {
251                return new UrlResource(createRelativeURL(relativePath));
252        }
253
254        /**
255         * This delegate creates a {@code java.net.URL}, applying the given path
256         * relative to the path of the underlying URL of this resource descriptor.
257         * A leading slash will get dropped; a "#" symbol will get encoded.
258         * @since 5.2
259         * @see #createRelative(String)
260         * @see java.net.URL#URL(java.net.URL, String)
261         */
262        protected URL createRelativeURL(String relativePath) throws MalformedURLException {
263                if (relativePath.startsWith("/")) {
264                        relativePath = relativePath.substring(1);
265                }
266                // # can appear in filenames, java.net.URL should not treat it as a fragment
267                relativePath = StringUtils.replace(relativePath, "#", "%23");
268                // Use the URL constructor for applying the relative path as a URL spec
269                return new URL(this.url, relativePath);
270        }
271
272        /**
273         * This implementation returns the name of the file that this URL refers to.
274         * @see java.net.URL#getPath()
275         */
276        @Override
277        public String getFilename() {
278                return StringUtils.getFilename(getCleanedUrl().getPath());
279        }
280
281        /**
282         * This implementation returns a description that includes the URL.
283         */
284        @Override
285        public String getDescription() {
286                return "URL [" + this.url + "]";
287        }
288
289
290        /**
291         * This implementation compares the underlying URL references.
292         */
293        @Override
294        public boolean equals(@Nullable Object other) {
295                return (this == other || (other instanceof UrlResource &&
296                                getCleanedUrl().equals(((UrlResource) other).getCleanedUrl())));
297        }
298
299        /**
300         * This implementation returns the hash code of the underlying URL reference.
301         */
302        @Override
303        public int hashCode() {
304                return getCleanedUrl().hashCode();
305        }
306
307}