You are on page 1of 8

package suncertify.

db;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Wrapper class for the data file, an instance of this class is used by the <co
de>Data.java</code> to access
* the data in the database.
* @author ssafi
*
*/
class Dao {
/**
* The physical file on disk containing the records.
*/
private RandomAccessFile dataFile = null;
/**
* Contains record_id/offset as key/value pairs
*/
private static Map<Long, Long> recordIdOffset = new HashMap<Long, Long>(
);
/**
* Contains deleted_record_id/offset as key value pairs
*/
private static Map<Long, Long> deletedIdOffset = new HashMap<Long, Long>
();
/**
* Contains record_id/record as key/value pairs.
*/
private static Map<Long, Record> records = new LinkedHashMap<Long, Recor
d>();
/**
* Ensures that multiple threads can read the <code>recordIdOffset</code
>
* Map as long as nobody is updating it.
*/
private static ReadWriteLock recordWriteLock = new ReentrantReadWriteLoc
k();
/**
* The location of the data file.
*/
private static String dataFilePath = null;
/**
* Holds the latest record id value.
*/
private static long currentRecordId = -1;
/**
* Holds the offset of the first record
*/
private final static long firstRecordOffset = 70;
/**
* The magicCookie of the data file.
*/
private static final byte[] magicCookie = {0x00, 0x00, 0x02, 0x02};
/**
* Constructor that accept the database path as parameter.<br/>
*
* All instances of this class share the same data file.
* @param dbPath - String represents the path for the data file
* @throws FileNotFoundException if the specified database file does not
exist.
* @throws IOException - when failed to check if it is a data file
* - when failed to load the rec
ords from the file
*/
public Dao(String dbPath) throws FileNotFoundException, IOException {
if (dataFile == null) {
dataFile = new RandomAccessFile(dbPath, "rw");
dataFile.seek(0);
validateDataFile();
dataFilePath = dbPath;
loadAllRecords();
} else if (dataFilePath != dbPath) {
System.err.println("Only one data file location can be u
sed.");
}
}
/**
* Checks if the database file is valid data file using the magic cookie
in the file header.
* @throws IOException if the file is not a data file
*/
private void validateDataFile() throws IOException {
byte[] b = new byte[magicCookie.length];
dataFile.read(b);
final String message = "The data file is not a valid data file";
for (int i = 0; i < b.length; i++) {
if (b[i] != magicCookie[i]) {
throw new IOException(message);
}
}
}
/**
* Selects the record that has the specified id.
* @param recordId the record id.
* @return an instance of vacancy object that represent the record.
* @throws RecordNotFoundException if the record is not found
* or marked as deleted.
* @throws IOException if any exception thrown while reading
* the record from the file.
*/
public Record select(final long recordId)
throws RecordNotFoundException, IOException {
recordWriteLock.readLock().lock();
try {
Long id = Long.valueOf(recordId);
if (records.containsKey(id)) {
return ((Record) records.get(id));
}
Long offset = (Long) recordIdOffset.get(Long.valueOf(rec
ordId));
if (offset == null) {
throw new RecordNotFoundException(
"Record with id " + recordId + " does no
t exist");
}
final byte[] recordData = readRecordFromDataFile(offset.
longValue());
RecordReader recordReader = new RecordReader(recordId, r
ecordData);
return recordReader.getRecord();
} finally {
recordWriteLock.readLock().unlock();
}
}
/**
* Reads the record from the database that has the specified record numb
er.
* @param offset the start offset of the record in the file.
* @return record data stored in byte[].
* @throws IOException if any exception thrown while reading
* the record from the file.
*/
private byte[] readRecordFromDataFile(final long offset)
throws IOException {
final byte[] record = new byte[DBConstants.RECORD_LENGTH];
synchronized (dataFile) {
dataFile.seek(offset);
dataFile.readFully(record);
}
return record;
}
/**
* Inserts a new record to the database.
* @param record - the record to be written.
* @return the new record id.
* @throws IOException - when any exception thrown while writing the rec
ord to the database file.
*/
public long insert(Record record) throws IOException {
Map.Entry<Long, Long> deletedRecord = getRecordInfoFromDeleted()
;
Long recordId = null;
Long offset = null;
if (deletedRecord == null) {
recordId = Long.valueOf(getNewRecordNumber());
} else {
recordId = (Long) deletedRecord.getKey();
offset = (Long) deletedRecord.getValue();
}
recordWriteLock.writeLock().lock();
try {
if (offset == null) {
offset = Long.valueOf(dataFile.length());
}
} finally {
recordWriteLock.writeLock().unlock();
}
RecordWriter recordWriter = new RecordWriter(false, record);
writeRecord(recordWriter.getRecord(), offset.longValue());
//Update the record number and cache the record
record.setRecordNumber(recordId.longValue());
recordIdOffset.put(recordId, offset);
records.put(recordId, record);
return recordId.longValue();
}
/**
* Writes the specified record to the database.
* @param record record data.
* @param offset the start offset of the record in the file.
* @throws IOException if any exception thrown while writing
* the record to the file.
*/
private void writeRecord(final byte[] record, final long offset)
throws IOException {
synchronized (dataFile) {
dataFile.seek(offset);
dataFile.write(record);
}
}
/**
* Generates new record number.
* @return new record number.
*/
private long getNewRecordNumber() {
recordWriteLock.writeLock().lock();
try {
long newRecordNumber = currentRecordId + 1;
currentRecordId = newRecordNumber;
return newRecordNumber;
} finally {
recordWriteLock.writeLock().unlock();
}
}
/**
* Gets all the record numbers.
* @return <code>Array</code> contains all the record numbers.
*/
protected long[] getAllRecordNumbers() {
recordWriteLock.writeLock().lock();
try {
Iterator recsNo = recordIdOffset.keySet().iterator();
long[] recNos = new long[recordIdOffset.size()];
int count = 0;
while (recsNo.hasNext()) {
Long recNo = (Long) recsNo.next();
recNos[count] = recNo.longValue();
count++;
}
return recNos;
} finally {
recordWriteLock.writeLock().unlock();
}
}
/**
* Gets the first record information from the deleted list,
* to be reused
* @return first entry in the deleted records the entry contains record
* number and it's offset in the database file.<br/>
* null if the deleted records map is empty.
*/
private Map.Entry<Long, Long> getRecordInfoFromDeleted() {
recordWriteLock.writeLock().lock();
try {
if (deletedIdOffset.isEmpty()) {
return null;
}
Iterator<Map.Entry<Long, Long>> iter = deletedIdOffset.e
ntrySet().iterator();
Map.Entry<Long, Long> entry = null;
if (iter.hasNext()) {
entry = iter.next();
deletedIdOffset.remove(entry.getKey());
}
return entry;
} finally {
recordWriteLock.writeLock().unlock();
}
}
/**
* Loads all records from the database file and cach it
* @throws IOException if any exception thrown while accessing the file.
*/
private void loadAllRecords() throws IOException {
recordWriteLock.writeLock().lock();
try {
long recordNumber = 1;
for (long offset = firstRecordOffset;
offset < dataFile.length();
offset += DBConstants.RECORD_LENGTH) {
Long recNo = Long.valueOf(recordNumber);
Long fileOffset = Long.valueOf(offset);
try {
RecordReader recordReader =
new RecordReader(
recordNumber,
readRecordFromDataFile(o
ffset));
// System.out.println(recordReader.getRecor
d());
records.put(recNo, recordReader.getRecor
d());
} catch(RecordNotFoundException rnf) {
// Record marked as deleted
deletedIdOffset.put(recNo, fileOffset);
continue;
} finally {
recordNumber++;
}
recordIdOffset.put(recNo, fileOffset);
}
currentRecordId = recordNumber - 1;
} finally {
recordWriteLock.writeLock().unlock();
}
}
/**
* Deletes a record from the database file.
* @param recId the record number
* @throws IOException if failed to delete the record from the database.
* @throws RecordNotFoundException if the record does not exist.
*/
public void deleteRecord(final long recId)
throws IOException, RecordNotFoundException {
recordWriteLock.writeLock().lock();
Long offset = null;
try {
Long recordNumber = Long.valueOf(recId);
offset = (Long) recordIdOffset.get(recordNumber);
if (offset == null) {
throw new RecordNotFoundException(
"Failed to delete Record number "
+ recId
+ " ,the record does not exist")
;
}
recordIdOffset.remove(recordNumber);
records.remove(recordNumber);
// add the deleted record number to be reused.
deletedIdOffset.put(recordNumber, offset);
} finally {
recordWriteLock.writeLock().unlock();
}
RecordWriter recordWriter = new RecordWriter(true, null);
writeRecord(recordWriter.getRecord(), offset.longValue());
}
/**
* Modifies and update the record fields on the database.
* @param record - the record to be updated.
* @throws IOException if any exception thrown while writing
* the record to the database file.
* @throws RecordNotFoundException if the record does not exist.
* @throws IllegalArgumentException if the specified record is null.
*/
public void update(Record record)
throws IOException, RecordNotFoundException {
if (record == null) {
throw new IllegalArgumentException(
"Failed to update record, record cannot be null"
);
}
recordWriteLock.writeLock().lock();
Long offset = null;
long recNo = -1;
try {
recNo = record.getRecordNumber();
offset = (Long) recordIdOffset.get(Long.valueOf(recNo));
if (offset == null) {
throw new RecordNotFoundException(
"Failed to update Record number "
+ recNo
+ " ,the record does not exist")
;
}
} finally {
recordWriteLock.writeLock().unlock();
}
RecordWriter recordWriter = new RecordWriter(false, record);
writeRecord(recordWriter.getRecord(), offset.longValue());
records.put(record.getRecordNumber(), record);
}
/**
* Search the database for the records that matches the specified search
query.
* @param query search query
* @return <code>Array</code> contains the all the record ids
* numbers that match the query.
*/
public long[] search(final String query) {
if (query == null || query.equals(".")) {
return getAllRecordNumbers();
}
recordWriteLock.writeLock().lock();
try {
Iterator<Map.Entry<Long, Record>> recordsIter = records.
entrySet().iterator();
ArrayList<Long> recIds = new ArrayList<Long>();
while (recordsIter.hasNext()) {
Map.Entry<Long, Record> entry = recordsIter.next
();
Record record = entry.getValue();
if (isMatch(record.toString(), query)) {
recIds.add(entry.getKey());
}
}
long[] result = new long[recIds.size()];
for (int i = 0; i < result.length; i++) {
result[i] = recIds.get(i).longValue();
}
return result;
} finally {
recordWriteLock.writeLock().unlock();
}
}
/**
* Checks if the given value match the specified condition.
* @param value value to check
* @param condition The condition to check the value against it.
* @return true if the value match the condition
* otherwise it will return false.
*/
private boolean isMatch(final String value, final String condition) {
Pattern pattern = Pattern.compile(condition);
Matcher matcher = pattern.matcher(value);
return matcher.find();
}
/**
* Checks if a record exists or not.
* @param recId the record number.
* @return true if the record is already exist,
* otherwise it will return false.
*/
static boolean isRecordExist(long recId) {
recordWriteLock.writeLock().lock();
try {
return recordIdOffset.containsKey(Long.valueOf(recId));
} finally {
recordWriteLock.writeLock().unlock();
}
}
}