/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.transport; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.Map; import java.util.Properties; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.fudgemsg.FudgeContext; import org.fudgemsg.FudgeField; import org.fudgemsg.FudgeMsg; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.util.ArgumentChecker; /** * * */ public class TaxonomyGatheringFudgeMessageSender implements FudgeMessageSender { private static final Logger s_logger = LoggerFactory.getLogger(TaxonomyGatheringFudgeMessageSender.class); private static final long DEFAULT_PERIOD = 60 * 1000L; private final FudgeMessageSender _underlying; private final FudgeContext _fudgeContext; private final File _outputFile; private final File _temporaryFile; private final long _rewritePeriod; private final ConcurrentMap<String, Integer> _taxonomyValues = new ConcurrentHashMap<String, Integer>(); private final AtomicInteger _nextOrdinal = new AtomicInteger(1); private final AtomicReference<CountDownLatch> _waitForNextWrite = new AtomicReference<CountDownLatch>(); /** * The next ordinal the last time that the file was written, to avoid writing when * there's been no changes. */ private final AtomicInteger _lastMaxOrdinalWritten = new AtomicInteger(1); private final Timer _timer; public TaxonomyGatheringFudgeMessageSender(FudgeMessageSender underlying, String outputFileName) { this(underlying, outputFileName, FudgeContext.GLOBAL_DEFAULT); } public TaxonomyGatheringFudgeMessageSender(FudgeMessageSender underlying, String outputFileName, FudgeContext fudgeContext) { this(underlying, outputFileName, FudgeContext.GLOBAL_DEFAULT, DEFAULT_PERIOD); } public TaxonomyGatheringFudgeMessageSender(FudgeMessageSender underlying, String outputFileName, FudgeContext fudgeContext, long fileWritePeriod) { ArgumentChecker.notNull(underlying, "underlying"); ArgumentChecker.notNull(fudgeContext, "fudgeContext"); ArgumentChecker.notEmpty(outputFileName, "outputFileName"); ArgumentChecker.isTrue(fileWritePeriod > 0, "File write period must be positive"); _underlying = underlying; _fudgeContext = fudgeContext; _rewritePeriod = fileWritePeriod; File outputFile = new File(outputFileName); if (outputFile.exists()) { ArgumentChecker.isTrue(outputFile.canRead(), "Must be able to read the output file"); ArgumentChecker.isTrue(outputFile.canWrite(), "Must be able to write the output file"); } _outputFile = outputFile; File temporaryFile = new File(outputFileName + ".tmp"); if (temporaryFile.exists()) { ArgumentChecker.isTrue(temporaryFile.canWrite(), "Must be able to write to a temporary output file"); } _temporaryFile = temporaryFile; bootstrapTaxonomy(); _timer = new Timer("TaxonomyGatheringFudgeMessageSender", true); _timer.schedule(new PropertyWritingTimerTask(), 0, _rewritePeriod); } /** * */ private void bootstrapTaxonomy() { if (!_outputFile.exists()) { s_logger.debug("Existing file doesn't exist, so not bootstrapping."); return; } s_logger.info("Bootstrapping taxonomy from {}", _outputFile); Properties propsFromFile = null; try (InputStream underlyingInputStream = new BufferedInputStream(new FileInputStream(_outputFile))) { propsFromFile = new Properties(); propsFromFile.load(underlyingInputStream); } catch (IOException ioe) { s_logger.warn("Unable to load existing properties from {}", ioe, new Object[]{_outputFile}); } int maxOrdinal = 0; if (propsFromFile != null) { for (Map.Entry<Object, Object> entry : propsFromFile.entrySet()) { String keyString = (String) entry.getKey(); String valueString = (String) entry.getValue(); int ordinal = Integer.parseInt(keyString); maxOrdinal = Math.max(maxOrdinal, ordinal); _taxonomyValues.put(valueString, ordinal); } } _nextOrdinal.set(maxOrdinal + 1); _lastMaxOrdinalWritten.set(_nextOrdinal.get()); } @Override public FudgeContext getFudgeContext() { return _fudgeContext; } protected ConcurrentMap<String, Integer> getCurrentTaxonomy() { return _taxonomyValues; } protected Timer getTimer() { return _timer; } @Override public void send(FudgeMsg message) { gatherFieldNames(message); _underlying.send(message); } /** * @param message */ private void gatherFieldNames(FudgeMsg message) { for (FudgeField field : message) { if (field.getName() == null) { continue; } if (field.getOrdinal() != null) { continue; } // Yes, we double-check here. That's fine, as we want to avoid collisions // that will result in gaps in the taxonomy ordinal. The chances // of a collision here is small enough that we're fine if we have a gap, // but it wouldn't be if we did .putIfAbsent() without the first containsKey() // check. if (_taxonomyValues.containsKey(field.getName())) { continue; } _taxonomyValues.putIfAbsent(field.getName(), _nextOrdinal.getAndIncrement()); if (field.getValue() instanceof FudgeMsg) { gatherFieldNames((FudgeMsg) field.getValue()); } } } /** * Wait for the next write to occur that will have flushed the last of the field names collected by * the previous call to send. */ protected void waitForNextWrite() throws InterruptedException { final int nextOrdinal = _nextOrdinal.get(); while (_lastMaxOrdinalWritten.get() < nextOrdinal) { CountDownLatch latch = new CountDownLatch(1); if (_waitForNextWrite.compareAndSet(null, latch)) { // Latch available for the writing thread if (_lastMaxOrdinalWritten.get() >= nextOrdinal) { // Writing thread updated the last max; it may or may not have taken // the latch at this point but that doesn't matter. At worse it will // waste a "countDown" operation on it. return; } } else { latch = _waitForNextWrite.get(); // If latch is now null then a write must have happened, so don't need to block if (latch == null) { return; } } latch.await(); } } private class PropertyWritingTimerTask extends TimerTask { @Override public void run() { final int nextOrdinal = _nextOrdinal.get(); if (_lastMaxOrdinalWritten.get() >= nextOrdinal) { s_logger.debug("No reason to write taxonomy as no changes since last persist."); } else { s_logger.info("Writing current taxonomy of {} values to {}", _taxonomyValues.size(), _outputFile); Properties props = new Properties(); for (Map.Entry<String, Integer> taxonomyEntry : _taxonomyValues.entrySet()) { props.setProperty(taxonomyEntry.getValue().toString(), taxonomyEntry.getKey()); } FileOutputStream fos = null; try { fos = new FileOutputStream(_temporaryFile); props.store(new BufferedOutputStream(fos), "Automatically generated, written " + new Date()); } catch (IOException ioe) { s_logger.warn("Unable to write taxonomy to file {}", ioe, new Object[] {_temporaryFile}); } finally { if (fos != null) { try { fos.close(); if (!_temporaryFile.renameTo(_outputFile)) { boolean ok = false; if (_outputFile.exists()) { _outputFile.delete(); ok = _temporaryFile.renameTo(_outputFile); } if (!ok) { s_logger.warn("Unable to rename temporary file {} to {}", _temporaryFile, _outputFile); } } } catch (IOException ioe) { s_logger.warn("Unable to close output file {}", ioe, new Object[] {_temporaryFile}); } } } _lastMaxOrdinalWritten.set(nextOrdinal); final CountDownLatch latch = _waitForNextWrite.getAndSet(null); if (latch != null) { latch.countDown(); } } } } }