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}