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}