/******************************************************************************* * 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.lburgazzoli.quickfixj.core.IFIXContext; import quickfix.field.converter.UtcTimestampConverter; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; 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.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; 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.CachedFileStoreFactory */ public class CachedFileStore implements MessageStore { private final Logger log = LoggerFactory.getLogger(getClass()); 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 MemoryStore cache; private final String msgFileName; private final String headerFileName; private final String seqNumFileName; private final String sessionFileName; private RandomAccessFile messageFileReader; private RandomAccessFile messageFileWriter; private DataOutputStream headerDataOutputStream; private RandomAccessFile sequenceNumberFile; private final boolean syncWrites; private final CachedHashMap messageIndex = new CachedHashMap(100); private FileOutputStream headerFileOutputStream; private final String charsetEncoding = CharsetSupport.getCharset(); private final IFIXContext context; CachedFileStore(IFIXContext context,String path, SessionID sessionID, boolean syncWrites) throws IOException { this.syncWrites = syncWrites; this.context = context; this.cache = new MemoryStore(context); 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"; seqNumFileName = prefix + "seqnums"; 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); sequenceNumberFile = new RandomAccessFile(seqNumFileName, 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 { sequenceNumberFile.seek(0); if (sequenceNumberFile.length() > 0) { final String s = sequenceNumberFile.readUTF(); final int offset = s.indexOf(':'); if (offset < 0) { throw new IOException("Invalid sequenceNumbderFile '" + seqNumFileName + "' character ':' is missing"); } cache.setNextSenderMsgSeqNum(Integer.parseInt(s.substring(0, offset))); cache.setNextTargetMsgSeqNum(Integer.parseInt(s.substring(offset + 1))); } } private void initializeMessageIndex() throws IOException { 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(); messageIndex.put(Long.valueOf(sequenceNumber), new long[] { offset, size }); } } finally { headerDataInputStream.close(); } } headerFileOutputStream = new FileOutputStream(headerFileName, true); headerDataOutputStream = new DataOutputStream(new BufferedOutputStream( headerFileOutputStream)); } private String getRandomAccessFileOptions() { return READ_OPTION + WRITE_OPTION + (syncWrites ? SYNC_OPTION : NOSYNC_OPTION); } /** * Close the store's files. * * @throws IOException */ public void closeFiles() throws IOException { closeOutputStream(headerDataOutputStream); closeFile(messageFileWriter); closeFile(messageFileReader); closeFile(sequenceNumberFile); } 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 { closeFiles(); deleteFile(headerFileName); deleteFile(msgFileName); deleteFile(seqNumFileName); deleteFile(sessionFileName); } private void deleteFile(String fileName) throws IOException { final File file = new File(fileName); if (file.exists() && !file.delete()) { log.error("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); storeSequenceNumbers(); } /* * (non-Javadoc) * @see quickfix.MessageStore#setNextTargetMsgSeqNum(int) */ public void setNextTargetMsgSeqNum(int next) throws IOException { cache.setNextTargetMsgSeqNum(next); storeSequenceNumbers(); } /* * (non-Javadoc) * @see quickfix.MessageStore#incrNextSenderMsgSeqNum() */ public void incrNextSenderMsgSeqNum() throws IOException { cache.incrNextSenderMsgSeqNum(); storeSequenceNumbers(); } /* * (non-Javadoc) * @see quickfix.MessageStore#incrNextTargetMsgSeqNum() */ public void incrNextTargetMsgSeqNum() throws IOException { cache.incrNextTargetMsgSeqNum(); storeSequenceNumbers(); } /* * (non-Javadoc) * @see quickfix.MessageStore#get(int, int, java.util.Collection) */ public void get(int startSequence, int endSequence, Collection<String> messages) throws IOException { final Collection<String> readedMsg = getMessage(startSequence, endSequence); messages.addAll(readedMsg); } /** * 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) { throw new UnsupportedOperationException("not supported"); } private String read(long offset, long size) throws IOException { String message = null; final byte[] data = new byte[(int) size]; messageFileReader.seek(offset); if (messageFileReader.read(data) != size) { throw new IOException("Truncated input while reading message: " + new String(data, charsetEncoding)); } message = new String(data, charsetEncoding); return message; } private Collection<String> getMessage(long startSequence, long endSequence) throws IOException { final Collection<String> messages = new ArrayList<String>(); final List<long[]> offsetAndSizes = messageIndex.get(startSequence, endSequence); for (final long[] offsetAndSize : offsetAndSizes) { if (offsetAndSize != null) { final String message = read(offsetAndSize[0], offsetAndSize[1]); messages.add(message); } } messageFileReader.seek(messageFileReader.length()); return messages; } /* * (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(); messageIndex.put(Long.valueOf(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 storeSequenceNumbers() throws IOException { sequenceNumberFile.seek(0); // I changed this from explicitly using a StringBuffer because of // recommendations from Sun. The performance also appears higher // with this implementation. -- smb. // http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4259569 sequenceNumberFile.writeUTF("" + cache.getNextSenderMsgSeqNum() + ':' + cache.getNextTargetMsgSeqNum()); } String getHeaderFileName() { return headerFileName; } String getMsgFileName() { return msgFileName; } String getSeqNumFileName() { return seqNumFileName; } /* * (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); } /** * @author mratsimbazafy 29 august 2008 */ private class CachedHashMap implements Map<Long, long[]> { private final TreeMap<Long, long[]> cacheIndex = new TreeMap<Long, long[]>(); private int currentSize; private final int maxSize; public CachedHashMap(int _maxSize) { currentSize = 0; maxSize = _maxSize; } public void clear() { cacheIndex.clear(); currentSize = 0; } public boolean containsKey(Object key) { return cacheIndex.containsKey(key); } public boolean containsValue(Object value) { return cacheIndex.containsValue(value); } public Set<java.util.Map.Entry<Long, long[]>> entrySet() { return cacheIndex.entrySet(); } public long[] get(Object key) { final long[] v = cacheIndex.get(key); if (v != null) { return v; } return seekMessageIndex((Long) key); } public boolean isEmpty() { return cacheIndex.isEmpty(); } public Set<Long> keySet() { return cacheIndex.keySet(); } public long[] put(Long key, long[] value) { cacheIndex.put(key, value); currentSize++; if (currentSize > maxSize) { final Iterator<Entry<Long, long[]>> it = cacheIndex.entrySet().iterator(); it.next(); it.remove(); currentSize--; } return value; } public void putAll(Map<? extends Long, ? extends long[]> t) { throw new UnsupportedOperationException("not supported"); } public long[] remove(Object key) { throw new UnsupportedOperationException("not supported"); } public int size() { return cacheIndex.size(); } public Collection<long[]> values() { return cacheIndex.values(); } private long[] seekMessageIndex(final long index) { final File headerFile = new File(headerFileName); if (headerFile.exists()) { DataInputStream headerDataInputStream = null; try { headerDataInputStream = new DataInputStream(new BufferedInputStream( new FileInputStream(headerFile))); while (headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final long offset = headerDataInputStream.readLong(); final int size = headerDataInputStream.readInt(); if (index == sequenceNumber) { return new long[] { offset, size }; } } } catch (final IOException e) { return null; } finally { try { if (headerDataInputStream != null) { headerDataInputStream.close(); } } catch (final IOException e) { log.error("", e); } } } return null; } private List<long[]> seekMessageIndex(final long startSequence, final long endSequence) { final TreeMap<Integer, long[]> indexPerSequenceNumber = new TreeMap<Integer, long[]>(); final File headerFile = new File(headerFileName); if (headerFile.exists()) { DataInputStream headerDataInputStream = null; try { headerDataInputStream = new DataInputStream(new BufferedInputStream( new FileInputStream(headerFile))); while (headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final Integer sequenceNumberInteger = Integer.valueOf(sequenceNumber); final long offset = headerDataInputStream.readLong(); final int size = headerDataInputStream.readInt(); if (sequenceNumber >= startSequence && sequenceNumber <= endSequence) { indexPerSequenceNumber.put(sequenceNumberInteger, new long[] { offset, size }); } } } catch (final IOException e) { log.error("", e); return null; } finally { try { if (headerDataInputStream != null) { headerDataInputStream.close(); } } catch (final IOException e) { log.error("", e); } } } return new ArrayList<long[]>(indexPerSequenceNumber.values()); } public List<long[]> get(final long startSequence, final long endSequence) { return seekMessageIndex(startSequence, endSequence); } } }