/******************************************************************************* * Copyright (c) quickfixengine.org All rights reserved. * * This file is part of the QuickFIX FIX Engine * * This file may be distributed under the terms of the quickfixengine.org * license as defined by quickfixengine.org and appearing in the file * LICENSE included in the packaging of this file. * * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE. * * See http://www.quickfixengine.org/LICENSE for licensing information. * * Contact ask@quickfixengine.org if any conditions of this licensing * are not clear to you. ******************************************************************************/ package quickfix; import org.quickfixj.CharsetSupport; import com.github.lburgazzoli.quickfixj.core.IFIXContext; import quickfix.field.converter.UtcTimestampConverter; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.Set; import java.util.TreeMap; /** * File store implementation. THIS CLASS IS PUBLIC ONLY TO MAINTAIN * COMPATIBILITY WITH THE QUICKFIX JNI. IT SHOULD ONLY BE CREATED USING A * FACTORY. * * @see quickfix.FileStoreFactory */ public class FileStore implements MessageStore, Closeable { private static final String READ_OPTION = "r"; private static final String WRITE_OPTION = "w"; private static final String SYNC_OPTION = "d"; private static final String NOSYNC_OPTION = ""; private final TreeMap<Long, long[]> messageIndex; private final MemoryStore cache; private final IFIXContext context; private final String msgFileName; private final String headerFileName; private final String senderSeqNumFileName; private final String targetSeqNumFileName; private final String sessionFileName; private final boolean syncWrites; private final int maxCachedMsgs; private final String charsetEncoding = CharsetSupport.getCharset(); private RandomAccessFile messageFileReader; private RandomAccessFile messageFileWriter; private DataOutputStream headerDataOutputStream; private FileOutputStream headerFileOutputStream; private RandomAccessFile senderSequenceNumberFile; private RandomAccessFile targetSequenceNumberFile; FileStore(IFIXContext context,String path, SessionID sessionID, boolean syncWrites, int maxCachedMsgs) throws IOException { this.syncWrites = syncWrites; this.maxCachedMsgs = maxCachedMsgs; this.context = context; this.cache = new MemoryStore(context); if (maxCachedMsgs > 0) { messageIndex = new TreeMap<Long, long[]>(); } else { messageIndex = null; } final String fullPath = new File(path == null ? "." : path).getAbsolutePath(); final String sessionName = FileUtil.sessionIdFileName(sessionID); final String prefix = FileUtil.fileAppendPath(fullPath, sessionName + "."); msgFileName = prefix + "body"; headerFileName = prefix + "header"; senderSeqNumFileName = prefix + "senderseqnums"; targetSeqNumFileName = prefix + "targetseqnums"; sessionFileName = prefix + "session"; final File directory = new File(msgFileName).getParentFile(); if (!directory.exists()) { directory.mkdirs(); } initialize(false); } void initialize(boolean deleteFiles) throws IOException { closeFiles(); if (deleteFiles) { deleteFiles(); } messageFileWriter = new RandomAccessFile(msgFileName, getRandomAccessFileOptions()); messageFileReader = new RandomAccessFile(msgFileName, READ_OPTION); senderSequenceNumberFile = new RandomAccessFile(senderSeqNumFileName, getRandomAccessFileOptions()); targetSequenceNumberFile = new RandomAccessFile(targetSeqNumFileName, getRandomAccessFileOptions()); initializeCache(); } private void initializeCache() throws IOException { cache.reset(); initializeMessageIndex(); initializeSequenceNumbers(); initializeSessionCreateTime(); messageFileWriter.seek(messageFileWriter.length()); } private void initializeSessionCreateTime() throws IOException { final File sessionTimeFile = new File(sessionFileName); if (sessionTimeFile.exists() && sessionTimeFile.length() > 0) { final DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream( new FileInputStream(sessionTimeFile))); try { final Calendar c = SystemTime.getUtcCalendar(UtcTimestampConverter .convert(sessionTimeInput.readUTF())); cache.setCreationTime(c); } catch (final Exception e) { throw new IOException(e.getMessage()); } finally { sessionTimeInput.close(); } } else { storeSessionTimeStamp(); } } private void storeSessionTimeStamp() throws IOException { final DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream( new FileOutputStream(sessionFileName, false))); try { final Date date = SystemTime.getDate(); cache.setCreationTime(SystemTime.getUtcCalendar(date)); sessionTimeOutput.writeUTF(UtcTimestampConverter.convert(date, true)); } finally { sessionTimeOutput.close(); } } /* (non-Javadoc) * @see quickfix.MessageStore#getCreationTime() */ public Date getCreationTime() throws IOException { return cache.getCreationTime(); } private void initializeSequenceNumbers() throws IOException { senderSequenceNumberFile.seek(0); targetSequenceNumberFile.seek(0); if (senderSequenceNumberFile.length() > 0) { final String s = senderSequenceNumberFile.readUTF(); cache.setNextSenderMsgSeqNum(Integer.parseInt(s)); } if (targetSequenceNumberFile.length() > 0) { final String s = targetSequenceNumberFile.readUTF(); cache.setNextTargetMsgSeqNum(Integer.parseInt(s)); } } private void initializeMessageIndex() throws IOException { // this part is unnecessary if no offsets are being stored in memory if (messageIndex != null) { messageIndex.clear(); final File headerFile = new File(headerFileName); if (headerFile.exists()) { final DataInputStream headerDataInputStream = new DataInputStream( new BufferedInputStream(new FileInputStream(headerFile))); try { while (headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final long offset = headerDataInputStream.readLong(); final int size = headerDataInputStream.readInt(); updateMessageIndex((long) sequenceNumber, new long[] { offset, size }); } } finally { headerDataInputStream.close(); } } } headerFileOutputStream = new FileOutputStream(headerFileName, true); headerDataOutputStream = new DataOutputStream(new BufferedOutputStream( headerFileOutputStream)); } private void updateMessageIndex(Long sequenceNum, long[] offsetAndSize) { // Remove the lowest indexed sequence number if this addition // would result the index growing to larger than maxCachedMsgs. if (messageIndex.size() >= maxCachedMsgs && messageIndex.get(sequenceNum) == null) { // Line below requires Java 6, using Java 5 approximation //messageIndex.pollFirstEntry(); messageIndex.remove(messageIndex.firstKey()); } messageIndex.put(sequenceNum, offsetAndSize); } private String getRandomAccessFileOptions() { return READ_OPTION + WRITE_OPTION + (syncWrites ? SYNC_OPTION : NOSYNC_OPTION); } /** * Close the store's files. * @deprecated use close instead * @throws IOException */ @Deprecated public void closeFiles() throws IOException { close(); } /** * Close the store's files. * @throws IOException */ public void close() throws IOException { closeOutputStream(headerDataOutputStream); closeFile(messageFileWriter); closeFile(messageFileReader); closeFile(senderSequenceNumberFile); closeFile(targetSequenceNumberFile); } private void closeFile(RandomAccessFile file) throws IOException { if (file != null) { file.close(); } } private void closeOutputStream(OutputStream stream) throws IOException { if (stream != null) { stream.close(); } } public void deleteFiles() throws IOException { close(); deleteFile(headerFileName); deleteFile(msgFileName); deleteFile(senderSeqNumFileName); deleteFile(targetSeqNumFileName); deleteFile(sessionFileName); } private void deleteFile(String fileName) throws IOException { final File file = new File(fileName); if (file.exists() && !file.delete()) { System.err.println("File delete failed: " + fileName); } } /* (non-Javadoc) * @see quickfix.MessageStore#getNextSenderMsgSeqNum() */ public int getNextSenderMsgSeqNum() throws IOException { return cache.getNextSenderMsgSeqNum(); } /* (non-Javadoc) * @see quickfix.MessageStore#getNextTargetMsgSeqNum() */ public int getNextTargetMsgSeqNum() throws IOException { return cache.getNextTargetMsgSeqNum(); } /* (non-Javadoc) * @see quickfix.MessageStore#setNextSenderMsgSeqNum(int) */ public void setNextSenderMsgSeqNum(int next) throws IOException { cache.setNextSenderMsgSeqNum(next); storeSenderSequenceNumber(); } /* (non-Javadoc) * @see quickfix.MessageStore#setNextTargetMsgSeqNum(int) */ public void setNextTargetMsgSeqNum(int next) throws IOException { cache.setNextTargetMsgSeqNum(next); storeTargetSequenceNumber(); } /* (non-Javadoc) * @see quickfix.MessageStore#incrNextSenderMsgSeqNum() */ public void incrNextSenderMsgSeqNum() throws IOException { cache.incrNextSenderMsgSeqNum(); storeSenderSequenceNumber(); } /* (non-Javadoc) * @see quickfix.MessageStore#incrNextTargetMsgSeqNum() */ public void incrNextTargetMsgSeqNum() throws IOException { cache.incrNextTargetMsgSeqNum(); storeTargetSequenceNumber(); } /* (non-Javadoc) * @see quickfix.MessageStore#get(int, int, java.util.Collection) */ public void get(int startSequence, int endSequence, Collection<String> messages) throws IOException { final Set<Integer> uncachedOffsetMsgIds = new HashSet<Integer>(); // Use a treemap to make sure the messages are sorted by sequence num final TreeMap<Integer, String> messagesFound = new TreeMap<Integer, String>(); for (int i = startSequence; i <= endSequence; i++) { final String message = getMessage(i); if (message != null) { messagesFound.put(i, message); } else { uncachedOffsetMsgIds.add(i); } } if (!uncachedOffsetMsgIds.isEmpty()) { // parse the header file to find missing messages final File headerFile = new File(headerFileName); final DataInputStream headerDataInputStream = new DataInputStream( new BufferedInputStream(new FileInputStream(headerFile))); try { while (headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final long offset = headerDataInputStream.readLong(); final int size = headerDataInputStream.readInt(); if (uncachedOffsetMsgIds.remove(sequenceNumber)) { final String message = getMessage(new long[] { offset, size }, sequenceNumber); if (message != null) { messagesFound.put(sequenceNumber, message); } } if (uncachedOffsetMsgIds.isEmpty()) { break; } } } finally { headerDataInputStream.close(); } } messages.addAll(messagesFound.values()); } /** * This method is here for JNI API consistency but it's not * implemented. Use get(int, int, Collection) with the same * start and end sequence. * */ public boolean get(int sequence, String message) throws IOException { throw new UnsupportedOperationException("not supported"); } private String getMessage(int i) throws IOException { String message = null; if (messageIndex != null) { final long[] offsetAndSize = messageIndex.get((long) i); if (offsetAndSize != null) { message = getMessage(offsetAndSize, i); } } return message; } private String getMessage(long[] offsetAndSize, int i) throws IOException { final long offset = offsetAndSize[0]; messageFileReader.seek(offset); final int size = (int) offsetAndSize[1]; final byte[] data = new byte[size]; final int sizeRead = messageFileReader.read(data); if (sizeRead != size) { throw new IOException("Truncated input while reading message: messageIndex=" + i + ", offset=" + offset + ", expected size=" + size + ", size read from file=" + sizeRead); } final String message = new String(data, charsetEncoding); messageFileReader.seek(messageFileReader.length()); return message; } /* (non-Javadoc) * @see quickfix.MessageStore#set(int, java.lang.String) */ public boolean set(int sequence, String message) throws IOException { final long offset = messageFileWriter.getFilePointer(); final int size = message.length(); if (messageIndex != null) { updateMessageIndex((long) sequence, new long[] { offset, size }); } headerDataOutputStream.writeInt(sequence); headerDataOutputStream.writeLong(offset); headerDataOutputStream.writeInt(size); headerDataOutputStream.flush(); if (syncWrites) { headerFileOutputStream.getFD().sync(); } messageFileWriter.write(message.getBytes(CharsetSupport.getCharset())); return true; } private void storeSenderSequenceNumber() throws IOException { senderSequenceNumberFile.seek(0); senderSequenceNumberFile.writeUTF("" + cache.getNextSenderMsgSeqNum()); } private void storeTargetSequenceNumber() throws IOException { targetSequenceNumberFile.seek(0); targetSequenceNumberFile.writeUTF("" + cache.getNextTargetMsgSeqNum()); } String getHeaderFileName() { return headerFileName; } String getMsgFileName() { return msgFileName; } String getSeqNumSenderFileName() { return senderSeqNumFileName; } String getSeqNumTargetFileName() { return targetSeqNumFileName; } /* * (non-Javadoc) * @see quickfix.RefreshableMessageStore#refresh() */ public void refresh() throws IOException { initialize(false); } /* (non-Javadoc) * @see quickfix.MessageStore#reset() */ public void reset() throws IOException { initialize(true); } }