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}