001package org.apache.commons.jcs3.auxiliary.disk.indexed;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.File;
023import java.io.IOException;
024import java.nio.ByteBuffer;
025import java.nio.channels.FileChannel;
026import java.nio.file.StandardOpenOption;
027
028import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
029import org.apache.commons.jcs3.log.Log;
030import org.apache.commons.jcs3.log.LogManager;
031
032/** Provides thread safe access to the underlying random access file. */
033public class IndexedDisk implements AutoCloseable
034{
035    /** The size of the header that indicates the amount of data stored in an occupied block. */
036    public static final byte HEADER_SIZE_BYTES = 4;
037
038    /** The serializer. */
039    private final IElementSerializer elementSerializer;
040
041    /** The logger */
042    private static final Log log = LogManager.getLog(IndexedDisk.class);
043
044    /** The path to the log directory. */
045    private final String filepath;
046
047    /** The data file. */
048    private final FileChannel fc;
049
050    /**
051     * Constructor for the Disk object
052     * <p>
053     * @param file
054     * @param elementSerializer
055     * @throws IOException
056     */
057    public IndexedDisk(final File file, final IElementSerializer elementSerializer)
058        throws IOException
059    {
060        this.filepath = file.getAbsolutePath();
061        this.elementSerializer = elementSerializer;
062        this.fc = FileChannel.open(file.toPath(),
063                StandardOpenOption.CREATE,
064                StandardOpenOption.READ,
065                StandardOpenOption.WRITE);
066    }
067
068    /**
069     * This reads an object from the given starting position on the file.
070     * <p>
071     * The first four bytes of the record should tell us how long it is. The data is read into a byte
072     * array and then an object is constructed from the byte array.
073     * <p>
074     * @return Serializable
075     * @param ded
076     * @throws IOException
077     * @throws ClassNotFoundException
078     */
079    protected <T> T readObject(final IndexedDiskElementDescriptor ded)
080        throws IOException, ClassNotFoundException
081    {
082        String message = null;
083        boolean corrupted = false;
084        final long fileLength = fc.size();
085        if (ded.pos > fileLength)
086        {
087            corrupted = true;
088            message = "Record " + ded + " starts past EOF.";
089        }
090        else
091        {
092            final ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES);
093            fc.read(datalength, ded.pos);
094            datalength.flip();
095            final int datalen = datalength.getInt();
096            if (ded.len != datalen)
097            {
098                corrupted = true;
099                message = "Record " + ded + " does not match data length on disk (" + datalen + ")";
100            }
101            else if (ded.pos + ded.len > fileLength)
102            {
103                corrupted = true;
104                message = "Record " + ded + " exceeds file length.";
105            }
106        }
107
108        if (corrupted)
109        {
110            log.warn("\n The file is corrupt: \n {0}", message);
111            throw new IOException("The File Is Corrupt, need to reset");
112        }
113
114        final ByteBuffer data = ByteBuffer.allocate(ded.len);
115        fc.read(data, ded.pos + HEADER_SIZE_BYTES);
116        data.flip();
117
118        return elementSerializer.deSerialize(data.array(), null);
119    }
120
121    /**
122     * Moves the data stored from one position to another. The descriptor's position is updated.
123     * <p>
124     * @param ded
125     * @param newPosition
126     * @throws IOException
127     */
128    protected void move(final IndexedDiskElementDescriptor ded, final long newPosition)
129        throws IOException
130    {
131        final ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES);
132        fc.read(datalength, ded.pos);
133        datalength.flip();
134        final int length = datalength.getInt();
135
136        if (length != ded.len)
137        {
138            throw new IOException("Mismatched memory and disk length (" + length + ") for " + ded);
139        }
140
141        // TODO: more checks?
142
143        long readPos = ded.pos;
144        long writePos = newPosition;
145
146        // header len + data len
147        int remaining = HEADER_SIZE_BYTES + length;
148        final ByteBuffer buffer = ByteBuffer.allocate(16384);
149
150        while (remaining > 0)
151        {
152            // chunk it
153            final int chunkSize = Math.min(remaining, buffer.capacity());
154            buffer.limit(chunkSize);
155            fc.read(buffer, readPos);
156            buffer.flip();
157            fc.write(buffer, writePos);
158            buffer.clear();
159
160            writePos += chunkSize;
161            readPos += chunkSize;
162            remaining -= chunkSize;
163        }
164
165        ded.pos = newPosition;
166    }
167
168    /**
169     * Writes the given byte array to the Disk at the specified position.
170     * <p>
171     * @param data
172     * @param ded
173     * @return true if we wrote successfully
174     * @throws IOException
175     */
176    protected boolean write(final IndexedDiskElementDescriptor ded, final byte[] data)
177        throws IOException
178    {
179        final long pos = ded.pos;
180        if (log.isTraceEnabled())
181        {
182            log.trace("write> pos={0}", pos);
183            log.trace("{0} -- data.length = {1}", fc, data.length);
184        }
185
186        if (data.length != ded.len)
187        {
188            throw new IOException("Mismatched descriptor and data lengths");
189        }
190
191        final ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_SIZE_BYTES);
192        headerBuffer.putInt(data.length);
193        // write the header
194        headerBuffer.flip();
195        int written = fc.write(headerBuffer, pos);
196        assert written == HEADER_SIZE_BYTES;
197
198        //write the data
199        final ByteBuffer dataBuffer = ByteBuffer.wrap(data);
200        written = fc.write(dataBuffer, pos + HEADER_SIZE_BYTES);
201
202        return written == data.length;
203    }
204
205    /**
206     * Serializes the object and write it out to the given position.
207     * <p>
208     * TODO: make this take a ded as well.
209     * @param obj
210     * @param pos
211     * @throws IOException
212     */
213    protected <T> void writeObject(final T obj, final long pos)
214        throws IOException
215    {
216        final byte[] data = elementSerializer.serialize(obj);
217        write(new IndexedDiskElementDescriptor(pos, data.length), data);
218    }
219
220    /**
221     * Returns the raf length.
222     *
223     * @return the length of the file.
224     * @throws IOException If an I/O error occurs.
225     */
226    protected long length()
227        throws IOException
228    {
229        return fc.size();
230    }
231
232    /**
233     * Closes the raf.
234     * <p>
235     * @throws IOException
236     */
237    @Override
238    public void close()
239        throws IOException
240    {
241        fc.close();
242    }
243
244    /**
245     * Sets the raf to empty.
246     * <p>
247     * @throws IOException
248     */
249    protected synchronized void reset()
250        throws IOException
251    {
252        log.debug("Resetting Indexed File [{0}]", filepath);
253        fc.truncate(0);
254        fc.force(true);
255    }
256
257    /**
258     * Truncates the file to a given length.
259     * <p>
260     * @param length the new length of the file
261     * @throws IOException
262     */
263    protected void truncate(final long length)
264        throws IOException
265    {
266        log.info("Truncating file [{0}] to {1}", filepath, length);
267        fc.truncate(length);
268    }
269
270    /**
271     * This is used for debugging.
272     * <p>
273     * @return the file path.
274     */
275    protected String getFilePath()
276    {
277        return filepath;
278    }
279
280    /**
281     * Tests if the length is 0.
282     * @return true if the if the length is 0.
283     * @throws IOException If an I/O error occurs.
284     * @since 3.1
285     */
286    protected boolean isEmpty() throws IOException
287    {
288        return length() == 0;
289    }
290}