001/* 002 * Copyright 2002-2018 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.FileNotFoundException; 021import java.io.IOException; 022import java.net.HttpURLConnection; 023import java.net.URI; 024import java.net.URL; 025import java.net.URLConnection; 026import java.nio.channels.FileChannel; 027import java.nio.channels.ReadableByteChannel; 028import java.nio.file.NoSuchFileException; 029import java.nio.file.StandardOpenOption; 030 031import org.springframework.util.ResourceUtils; 032 033/** 034 * Abstract base class for resources which resolve URLs into File references, 035 * such as {@link UrlResource} or {@link ClassPathResource}. 036 * 037 * <p>Detects the "file" protocol as well as the JBoss "vfs" protocol in URLs, 038 * resolving file system references accordingly. 039 * 040 * @author Juergen Hoeller 041 * @since 3.0 042 */ 043public abstract class AbstractFileResolvingResource extends AbstractResource { 044 045 @Override 046 public boolean exists() { 047 try { 048 URL url = getURL(); 049 if (ResourceUtils.isFileURL(url)) { 050 // Proceed with file system resolution 051 return getFile().exists(); 052 } 053 else { 054 // Try a URL connection content-length header 055 URLConnection con = url.openConnection(); 056 customizeConnection(con); 057 HttpURLConnection httpCon = 058 (con instanceof HttpURLConnection ? (HttpURLConnection) con : null); 059 if (httpCon != null) { 060 int code = httpCon.getResponseCode(); 061 if (code == HttpURLConnection.HTTP_OK) { 062 return true; 063 } 064 else if (code == HttpURLConnection.HTTP_NOT_FOUND) { 065 return false; 066 } 067 } 068 if (con.getContentLengthLong() > 0) { 069 return true; 070 } 071 if (httpCon != null) { 072 // No HTTP OK status, and no content-length header: give up 073 httpCon.disconnect(); 074 return false; 075 } 076 else { 077 // Fall back to stream existence: can we open the stream? 078 getInputStream().close(); 079 return true; 080 } 081 } 082 } 083 catch (IOException ex) { 084 return false; 085 } 086 } 087 088 @Override 089 public boolean isReadable() { 090 try { 091 URL url = getURL(); 092 if (ResourceUtils.isFileURL(url)) { 093 // Proceed with file system resolution 094 File file = getFile(); 095 return (file.canRead() && !file.isDirectory()); 096 } 097 else { 098 // Try InputStream resolution for jar resources 099 URLConnection con = url.openConnection(); 100 customizeConnection(con); 101 if (con instanceof HttpURLConnection) { 102 HttpURLConnection httpCon = (HttpURLConnection) con; 103 int code = httpCon.getResponseCode(); 104 if (code != HttpURLConnection.HTTP_OK) { 105 httpCon.disconnect(); 106 return false; 107 } 108 } 109 long contentLength = con.getContentLengthLong(); 110 if (contentLength > 0) { 111 return true; 112 } 113 else if (contentLength == 0) { 114 // Empty file or directory -> not considered readable... 115 return false; 116 } 117 else { 118 // Fall back to stream existence: can we open the stream? 119 getInputStream().close(); 120 return true; 121 } 122 } 123 } 124 catch (IOException ex) { 125 return false; 126 } 127 } 128 129 @Override 130 public boolean isFile() { 131 try { 132 URL url = getURL(); 133 if (url.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { 134 return VfsResourceDelegate.getResource(url).isFile(); 135 } 136 return ResourceUtils.URL_PROTOCOL_FILE.equals(url.getProtocol()); 137 } 138 catch (IOException ex) { 139 return false; 140 } 141 } 142 143 /** 144 * This implementation returns a File reference for the underlying class path 145 * resource, provided that it refers to a file in the file system. 146 * @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String) 147 */ 148 @Override 149 public File getFile() throws IOException { 150 URL url = getURL(); 151 if (url.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { 152 return VfsResourceDelegate.getResource(url).getFile(); 153 } 154 return ResourceUtils.getFile(url, getDescription()); 155 } 156 157 /** 158 * This implementation determines the underlying File 159 * (or jar file, in case of a resource in a jar/zip). 160 */ 161 @Override 162 protected File getFileForLastModifiedCheck() throws IOException { 163 URL url = getURL(); 164 if (ResourceUtils.isJarURL(url)) { 165 URL actualUrl = ResourceUtils.extractArchiveURL(url); 166 if (actualUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { 167 return VfsResourceDelegate.getResource(actualUrl).getFile(); 168 } 169 return ResourceUtils.getFile(actualUrl, "Jar URL"); 170 } 171 else { 172 return getFile(); 173 } 174 } 175 176 /** 177 * This implementation returns a File reference for the given URI-identified 178 * resource, provided that it refers to a file in the file system. 179 * @since 5.0 180 * @see #getFile(URI) 181 */ 182 protected boolean isFile(URI uri) { 183 try { 184 if (uri.getScheme().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { 185 return VfsResourceDelegate.getResource(uri).isFile(); 186 } 187 return ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()); 188 } 189 catch (IOException ex) { 190 return false; 191 } 192 } 193 194 /** 195 * This implementation returns a File reference for the given URI-identified 196 * resource, provided that it refers to a file in the file system. 197 * @see org.springframework.util.ResourceUtils#getFile(java.net.URI, String) 198 */ 199 protected File getFile(URI uri) throws IOException { 200 if (uri.getScheme().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { 201 return VfsResourceDelegate.getResource(uri).getFile(); 202 } 203 return ResourceUtils.getFile(uri, getDescription()); 204 } 205 206 /** 207 * This implementation returns a FileChannel for the given URI-identified 208 * resource, provided that it refers to a file in the file system. 209 * @since 5.0 210 * @see #getFile() 211 */ 212 @Override 213 public ReadableByteChannel readableChannel() throws IOException { 214 try { 215 // Try file system channel 216 return FileChannel.open(getFile().toPath(), StandardOpenOption.READ); 217 } 218 catch (FileNotFoundException | NoSuchFileException ex) { 219 // Fall back to InputStream adaptation in superclass 220 return super.readableChannel(); 221 } 222 } 223 224 @Override 225 public long contentLength() throws IOException { 226 URL url = getURL(); 227 if (ResourceUtils.isFileURL(url)) { 228 // Proceed with file system resolution 229 File file = getFile(); 230 long length = file.length(); 231 if (length == 0L && !file.exists()) { 232 throw new FileNotFoundException(getDescription() + 233 " cannot be resolved in the file system for checking its content length"); 234 } 235 return length; 236 } 237 else { 238 // Try a URL connection content-length header 239 URLConnection con = url.openConnection(); 240 customizeConnection(con); 241 return con.getContentLengthLong(); 242 } 243 } 244 245 @Override 246 public long lastModified() throws IOException { 247 URL url = getURL(); 248 boolean fileCheck = false; 249 if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) { 250 // Proceed with file system resolution 251 fileCheck = true; 252 try { 253 File fileToCheck = getFileForLastModifiedCheck(); 254 long lastModified = fileToCheck.lastModified(); 255 if (lastModified > 0L || fileToCheck.exists()) { 256 return lastModified; 257 } 258 } 259 catch (FileNotFoundException ex) { 260 // Defensively fall back to URL connection check instead 261 } 262 } 263 // Try a URL connection last-modified header 264 URLConnection con = url.openConnection(); 265 customizeConnection(con); 266 long lastModified = con.getLastModified(); 267 if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) { 268 throw new FileNotFoundException(getDescription() + 269 " cannot be resolved in the file system for checking its last-modified timestamp"); 270 } 271 return lastModified; 272 } 273 274 /** 275 * Customize the given {@link URLConnection}, obtained in the course of an 276 * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. 277 * <p>Calls {@link ResourceUtils#useCachesIfNecessary(URLConnection)} and 278 * delegates to {@link #customizeConnection(HttpURLConnection)} if possible. 279 * Can be overridden in subclasses. 280 * @param con the URLConnection to customize 281 * @throws IOException if thrown from URLConnection methods 282 */ 283 protected void customizeConnection(URLConnection con) throws IOException { 284 ResourceUtils.useCachesIfNecessary(con); 285 if (con instanceof HttpURLConnection) { 286 customizeConnection((HttpURLConnection) con); 287 } 288 } 289 290 /** 291 * Customize the given {@link HttpURLConnection}, obtained in the course of an 292 * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. 293 * <p>Sets request method "HEAD" by default. Can be overridden in subclasses. 294 * @param con the HttpURLConnection to customize 295 * @throws IOException if thrown from HttpURLConnection methods 296 */ 297 protected void customizeConnection(HttpURLConnection con) throws IOException { 298 con.setRequestMethod("HEAD"); 299 } 300 301 302 /** 303 * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. 304 */ 305 private static class VfsResourceDelegate { 306 307 public static Resource getResource(URL url) throws IOException { 308 return new VfsResource(VfsUtils.getRoot(url)); 309 } 310 311 public static Resource getResource(URI uri) throws IOException { 312 return new VfsResource(VfsUtils.getRoot(uri)); 313 } 314 } 315 316}