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.data; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.RandomAccessFile; 023import java.util.Queue; 024import java.util.concurrent.ConcurrentLinkedQueue; 025import java.util.concurrent.Semaphore; 026 027/** 028 * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. 029 * 030 * @author Phillip Webb 031 */ 032public class RandomAccessDataFile implements RandomAccessData { 033 034 private static final int DEFAULT_CONCURRENT_READS = 4; 035 036 private final File file; 037 038 private final FilePool filePool; 039 040 private final long offset; 041 042 private final long length; 043 044 /** 045 * Create a new {@link RandomAccessDataFile} backed by the specified file. 046 * @param file the underlying file 047 * @throws IllegalArgumentException if the file is null or does not exist 048 * @see #RandomAccessDataFile(File, int) 049 */ 050 public RandomAccessDataFile(File file) { 051 this(file, DEFAULT_CONCURRENT_READS); 052 } 053 054 /** 055 * Create a new {@link RandomAccessDataFile} backed by the specified file. 056 * @param file the underlying file 057 * @param concurrentReads the maximum number of concurrent reads allowed on the 058 * underlying file before blocking 059 * @throws IllegalArgumentException if the file is null or does not exist 060 * @see #RandomAccessDataFile(File) 061 */ 062 public RandomAccessDataFile(File file, int concurrentReads) { 063 if (file == null) { 064 throw new IllegalArgumentException("File must not be null"); 065 } 066 if (!file.exists()) { 067 throw new IllegalArgumentException("File must exist"); 068 } 069 this.file = file; 070 this.filePool = new FilePool(file, concurrentReads); 071 this.offset = 0L; 072 this.length = file.length(); 073 } 074 075 /** 076 * Private constructor used to create a {@link #getSubsection(long, long) subsection}. 077 * @param file the underlying file 078 * @param pool the underlying pool 079 * @param offset the offset of the section 080 * @param length the length of the section 081 */ 082 private RandomAccessDataFile(File file, FilePool pool, long offset, long length) { 083 this.file = file; 084 this.filePool = pool; 085 this.offset = offset; 086 this.length = length; 087 } 088 089 /** 090 * Returns the underlying File. 091 * @return the underlying file 092 */ 093 public File getFile() { 094 return this.file; 095 } 096 097 @Override 098 public InputStream getInputStream(ResourceAccess access) throws IOException { 099 return new DataInputStream(access); 100 } 101 102 @Override 103 public RandomAccessData getSubsection(long offset, long length) { 104 if (offset < 0 || length < 0 || offset + length > this.length) { 105 throw new IndexOutOfBoundsException(); 106 } 107 return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset, 108 length); 109 } 110 111 @Override 112 public long getSize() { 113 return this.length; 114 } 115 116 public void close() throws IOException { 117 this.filePool.close(); 118 } 119 120 /** 121 * {@link RandomAccessDataInputStream} implementation for the 122 * {@link RandomAccessDataFile}. 123 */ 124 private class DataInputStream extends InputStream { 125 126 private RandomAccessFile file; 127 128 private int position; 129 130 DataInputStream(ResourceAccess access) throws IOException { 131 if (access == ResourceAccess.ONCE) { 132 this.file = new RandomAccessFile(RandomAccessDataFile.this.file, "r"); 133 this.file.seek(RandomAccessDataFile.this.offset); 134 } 135 } 136 137 @Override 138 public int read() throws IOException { 139 return doRead(null, 0, 1); 140 } 141 142 @Override 143 public int read(byte[] b) throws IOException { 144 return read(b, 0, b == null ? 0 : b.length); 145 } 146 147 @Override 148 public int read(byte[] b, int off, int len) throws IOException { 149 if (b == null) { 150 throw new NullPointerException("Bytes must not be null"); 151 } 152 return doRead(b, off, len); 153 } 154 155 /** 156 * Perform the actual read. 157 * @param b the bytes to read or {@code null} when reading a single byte 158 * @param off the offset of the byte array 159 * @param len the length of data to read 160 * @return the number of bytes read into {@code b} or the actual read byte if 161 * {@code b} is {@code null}. Returns -1 when the end of the stream is reached 162 * @throws IOException in case of I/O errors 163 */ 164 public int doRead(byte[] b, int off, int len) throws IOException { 165 if (len == 0) { 166 return 0; 167 } 168 int cappedLen = cap(len); 169 if (cappedLen <= 0) { 170 return -1; 171 } 172 RandomAccessFile file = this.file; 173 try { 174 if (file == null) { 175 file = RandomAccessDataFile.this.filePool.acquire(); 176 file.seek(RandomAccessDataFile.this.offset + this.position); 177 } 178 if (b == null) { 179 int rtn = file.read(); 180 moveOn(rtn == -1 ? 0 : 1); 181 return rtn; 182 } 183 else { 184 return (int) moveOn(file.read(b, off, cappedLen)); 185 } 186 } 187 finally { 188 if (this.file == null && file != null) { 189 RandomAccessDataFile.this.filePool.release(file); 190 } 191 } 192 } 193 194 @Override 195 public long skip(long n) throws IOException { 196 return (n <= 0 ? 0 : moveOn(cap(n))); 197 } 198 199 @Override 200 public void close() throws IOException { 201 if (this.file != null) { 202 this.file.close(); 203 } 204 } 205 206 /** 207 * Cap the specified value such that it cannot exceed the number of bytes 208 * remaining. 209 * @param n the value to cap 210 * @return the capped value 211 */ 212 private int cap(long n) { 213 return (int) Math.min(RandomAccessDataFile.this.length - this.position, n); 214 } 215 216 /** 217 * Move the stream position forwards the specified amount. 218 * @param amount the amount to move 219 * @return the amount moved 220 */ 221 private long moveOn(int amount) { 222 this.position += amount; 223 return amount; 224 } 225 226 } 227 228 /** 229 * Manage a pool that can be used to perform concurrent reads on the underlying 230 * {@link RandomAccessFile}. 231 */ 232 static class FilePool { 233 234 private final File file; 235 236 private final int size; 237 238 private final Semaphore available; 239 240 private final Queue<RandomAccessFile> files; 241 242 FilePool(File file, int size) { 243 this.file = file; 244 this.size = size; 245 this.available = new Semaphore(size); 246 this.files = new ConcurrentLinkedQueue<RandomAccessFile>(); 247 } 248 249 public RandomAccessFile acquire() throws IOException { 250 this.available.acquireUninterruptibly(); 251 RandomAccessFile file = this.files.poll(); 252 if (file != null) { 253 return file; 254 } 255 return new RandomAccessFile(this.file, "r"); 256 } 257 258 public void release(RandomAccessFile file) { 259 this.files.add(file); 260 this.available.release(); 261 } 262 263 public void close() throws IOException { 264 this.available.acquireUninterruptibly(this.size); 265 try { 266 RandomAccessFile pooledFile = this.files.poll(); 267 while (pooledFile != null) { 268 pooledFile.close(); 269 pooledFile = this.files.poll(); 270 } 271 } 272 finally { 273 this.available.release(this.size); 274 } 275 } 276 277 } 278 279}