/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.master.historicaltimeseries.impl; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.commons.lang.StringUtils; import org.joda.beans.JodaBeanUtils; import org.threeten.bp.Instant; import org.threeten.bp.LocalDate; import com.google.common.base.Objects; import com.google.common.base.Supplier; import com.opengamma.DataNotFoundException; import com.opengamma.core.change.BasicChangeManager; import com.opengamma.core.change.ChangeManager; import com.opengamma.core.change.ChangeType; import com.opengamma.id.ObjectId; import com.opengamma.id.ObjectIdSupplier; import com.opengamma.id.ObjectIdentifiable; import com.opengamma.id.UniqueId; import com.opengamma.id.VersionCorrection; import com.opengamma.master.SimpleAbstractInMemoryMaster; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesGetFilter; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoDocument; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoHistoryRequest; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoHistoryResult; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoMetaDataRequest; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoMetaDataResult; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoSearchRequest; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesInfoSearchResult; import com.opengamma.master.historicaltimeseries.HistoricalTimeSeriesMaster; import com.opengamma.master.historicaltimeseries.ManageableHistoricalTimeSeries; import com.opengamma.master.historicaltimeseries.ManageableHistoricalTimeSeriesInfo; import com.opengamma.timeseries.DoubleTimeSeriesOperators; import com.opengamma.timeseries.date.localdate.ImmutableLocalDateDoubleTimeSeries; import com.opengamma.timeseries.date.localdate.LocalDateDoubleEntryIterator; import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeries; import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeriesBuilder; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.paging.Paging; /** * An in-memory implementation of a historical time-series master. */ public class InMemoryHistoricalTimeSeriesMaster extends SimpleAbstractInMemoryMaster<HistoricalTimeSeriesInfoDocument> implements HistoricalTimeSeriesMaster { /** * The default scheme used for each {@link UniqueId}. */ public static final String DEFAULT_OID_SCHEME = "MemHts"; /** * A cache of time-series points by identifier. */ private final ConcurrentMap<ObjectId, LocalDateDoubleTimeSeries> _storePoints = new ConcurrentHashMap<ObjectId, LocalDateDoubleTimeSeries>(); /** * Creates an instance. */ public InMemoryHistoricalTimeSeriesMaster() { this(new ObjectIdSupplier(DEFAULT_OID_SCHEME)); } /** * Creates an instance specifying the change manager. * * @param changeManager the change manager, not null */ public InMemoryHistoricalTimeSeriesMaster(final ChangeManager changeManager) { this(new ObjectIdSupplier(DEFAULT_OID_SCHEME), changeManager); } /** * Creates an instance specifying the supplier of object identifiers. * * @param objectIdSupplier the supplier of object identifiers, not null */ public InMemoryHistoricalTimeSeriesMaster(final Supplier<ObjectId> objectIdSupplier) { this(objectIdSupplier, new BasicChangeManager()); } /** * Creates an instance specifying the supplier of object identifiers and change manager. * * @param objectIdSupplier the supplier of object identifiers, not null * @param changeManager the change manager, not null */ public InMemoryHistoricalTimeSeriesMaster(final Supplier<ObjectId> objectIdSupplier, final ChangeManager changeManager) { super(objectIdSupplier, changeManager); } //------------------------------------------------------------------------- @Override protected void validateDocument(HistoricalTimeSeriesInfoDocument document) { ArgumentChecker.notNull(document, "document"); if (document.getUniqueId() != null) { validateId(document.getUniqueId()); } ArgumentChecker.notNull(document.getInfo(), "document.series"); ArgumentChecker.notNull(document.getInfo().getExternalIdBundle(), "document.series.identifiers"); ArgumentChecker.isTrue(document.getInfo().getExternalIdBundle().toBundle().getExternalIds().size() > 0, "document.series.identifiers must not be empty"); ArgumentChecker.isTrue(StringUtils.isNotBlank(document.getInfo().getDataSource()), "document.series.dataSource must not be blank"); ArgumentChecker.isTrue(StringUtils.isNotBlank(document.getInfo().getDataProvider()), "document.series.dataProvider must not be blank"); ArgumentChecker.isTrue(StringUtils.isNotBlank(document.getInfo().getDataField()), "document.series.dataField must not be blank"); ArgumentChecker.isTrue(StringUtils.isNotBlank(document.getInfo().getObservationTime()), "document.series.observationTime must not be blank"); } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoMetaDataResult metaData(HistoricalTimeSeriesInfoMetaDataRequest request) { ArgumentChecker.notNull(request, "request"); HistoricalTimeSeriesInfoMetaDataResult result = new HistoricalTimeSeriesInfoMetaDataResult(); if (request.isDataFields()) { Set<String> types = new HashSet<String>(); for (HistoricalTimeSeriesInfoDocument doc : _store.values()) { types.add(doc.getInfo().getDataField()); } result.getDataFields().addAll(types); } if (request.isDataSources()) { Set<String> types = new HashSet<String>(); for (HistoricalTimeSeriesInfoDocument doc : _store.values()) { types.add(doc.getInfo().getDataSource()); } result.getDataSources().addAll(types); } if (request.isDataProviders()) { Set<String> types = new HashSet<String>(); for (HistoricalTimeSeriesInfoDocument doc : _store.values()) { types.add(doc.getInfo().getDataProvider()); } result.getDataProviders().addAll(types); } if (request.isObservationTimes()) { Set<String> types = new HashSet<String>(); for (HistoricalTimeSeriesInfoDocument doc : _store.values()) { types.add(doc.getInfo().getObservationTime()); } result.getObservationTimes().addAll(types); } return result; } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoSearchResult search(HistoricalTimeSeriesInfoSearchRequest request) { ArgumentChecker.notNull(request, "request"); final List<HistoricalTimeSeriesInfoDocument> list = new ArrayList<HistoricalTimeSeriesInfoDocument>(); for (HistoricalTimeSeriesInfoDocument doc : _store.values()) { if (request.matches(doc)) { list.add(doc); } } HistoricalTimeSeriesInfoSearchResult result = new HistoricalTimeSeriesInfoSearchResult(); result.setPaging(Paging.of(request.getPagingRequest(), list)); result.getDocuments().addAll(request.getPagingRequest().select(list)); return result; } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoDocument get(final UniqueId uniqueId) { return get(uniqueId, VersionCorrection.LATEST); } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoDocument get(final ObjectIdentifiable objectKey, VersionCorrection versionCorrection) { validateId(objectKey); ArgumentChecker.notNull(versionCorrection, "versionCorrection"); final ObjectId objectId = objectKey.getObjectId(); final HistoricalTimeSeriesInfoDocument document = _store.get(objectId); if (document == null) { throw new DataNotFoundException("Historical time-series not found: " + objectId); } return document; } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoDocument add(final HistoricalTimeSeriesInfoDocument document) { validateDocument(document); final ObjectId objectId = _objectIdSupplier.get(); final UniqueId uniqueId = objectId.atVersion(""); final HistoricalTimeSeriesInfoDocument cloned = JodaBeanUtils.clone(document); final ManageableHistoricalTimeSeriesInfo info = cloned.getInfo(); info.setUniqueId(uniqueId); final Instant now = Instant.now(); cloned.setVersionFromInstant(now); cloned.setCorrectionFromInstant(now); cloned.getInfo().setTimeSeriesObjectId(objectId); _store.put(objectId, cloned); _changeManager.entityChanged(ChangeType.ADDED, objectId, cloned.getVersionFromInstant(), cloned.getVersionToInstant(), now); return cloned; } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoDocument update(final HistoricalTimeSeriesInfoDocument document) { validateDocument(document); ArgumentChecker.notNull(document.getUniqueId(), "document.uniqueId"); final UniqueId uniqueId = document.getUniqueId(); final Instant now = Instant.now(); final HistoricalTimeSeriesInfoDocument storedDocument = _store.get(uniqueId.getObjectId()); if (storedDocument == null) { throw new DataNotFoundException("Historical time-series not found: " + uniqueId); } document.setVersionFromInstant(now); document.setVersionToInstant(null); document.setCorrectionFromInstant(now); document.setCorrectionToInstant(null); if (_store.replace(uniqueId.getObjectId(), storedDocument, document) == false) { throw new IllegalArgumentException("Concurrent modification"); } _changeManager.entityChanged(ChangeType.CHANGED, document.getObjectId(), document.getVersionFromInstant(), document.getVersionToInstant(), now); return document; } //------------------------------------------------------------------------- @Override public void remove(final ObjectIdentifiable objectIdentifiable) { validateId(objectIdentifiable); if (_store.remove(objectIdentifiable.getObjectId()) == null) { throw new DataNotFoundException("Historical time-series not found: " + objectIdentifiable); } _changeManager.entityChanged(ChangeType.REMOVED, objectIdentifiable.getObjectId(), null, null, Instant.now()); } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoDocument correct(final HistoricalTimeSeriesInfoDocument document) { return update(document); } //------------------------------------------------------------------------- @Override public HistoricalTimeSeriesInfoHistoryResult history(HistoricalTimeSeriesInfoHistoryRequest request) { ArgumentChecker.notNull(request, "request"); ArgumentChecker.notNull(request.getObjectId(), "request.objectId"); final HistoricalTimeSeriesInfoHistoryResult result = new HistoricalTimeSeriesInfoHistoryResult(); final HistoricalTimeSeriesInfoDocument doc = get(request.getObjectId(), VersionCorrection.LATEST); if (doc != null) { result.getDocuments().add(doc); } result.setPaging(Paging.ofAll(result.getDocuments())); return result; } //------------------------------------------------------------------------- public ManageableHistoricalTimeSeries getTimeSeries(UniqueId uniqueId) { return getTimeSeries(uniqueId.getObjectId(), VersionCorrection.LATEST); } public ManageableHistoricalTimeSeries getTimeSeries(UniqueId uniqueId, HistoricalTimeSeriesGetFilter filter) { return getTimeSeries(uniqueId.getObjectId(), VersionCorrection.LATEST, filter); } public ManageableHistoricalTimeSeries getTimeSeries(ObjectIdentifiable objectId, VersionCorrection versionCorrection) { return getTimeSeries(objectId, versionCorrection, HistoricalTimeSeriesGetFilter.ofRange(null, null)); } public ManageableHistoricalTimeSeries getTimeSeries(ObjectIdentifiable objectKey, VersionCorrection versionCorrection, HistoricalTimeSeriesGetFilter filter) { validateId(objectKey); LocalDate fromDateInclusive = Objects.firstNonNull(filter.getEarliestDate(), LocalDate.of(1000, 1, 1)); // TODO: JSR-310 min/max date LocalDate toDateInclusive = Objects.firstNonNull(filter.getLatestDate(), LocalDate.of(9999, 1, 1)); ArgumentChecker.inOrderOrEqual(fromDateInclusive, toDateInclusive, "fromDateInclusive", "toDateInclusive"); final ObjectId objectId = objectKey.getObjectId(); final Instant now = Instant.now(); LocalDateDoubleTimeSeries existingSeries = _storePoints.get(objectId); if (existingSeries == null) { if (_store.get(objectId) == null) { throw new DataNotFoundException("Historical time-series not found: " + objectId); } existingSeries = ImmutableLocalDateDoubleTimeSeries.EMPTY_SERIES; } // Filter points by date range and max points to return // Heeds LocalDateDoubleTimeSeries convention: inclusive start, exclusive end LocalDateDoubleTimeSeries subSeries = existingSeries.subSeries(fromDateInclusive, toDateInclusive.plusDays(1)); Integer maxPoints = filter.getMaxPoints(); if (((maxPoints != null) && (Math.abs(maxPoints) < subSeries.size()))) { subSeries = maxPoints >= 0 ? subSeries.head(maxPoints) : subSeries.tail(-maxPoints); } final ManageableHistoricalTimeSeries result = new ManageableHistoricalTimeSeries(); result.setUniqueId(objectId.atLatestVersion()); result.setTimeSeries(subSeries); result.setVersionInstant(now); result.setCorrectionInstant(now); return result; } //------------------------------------------------------------------------- @Override public UniqueId updateTimeSeriesDataPoints(ObjectIdentifiable objectKey, LocalDateDoubleTimeSeries series) { ArgumentChecker.notNull(objectKey, "objectKey"); ArgumentChecker.notNull(series, "series"); final ObjectId objectId = objectKey.getObjectId(); final LocalDateDoubleTimeSeries existingSeries = _storePoints.get(objectId); if (existingSeries != null) { if (existingSeries.size() > 0 && series.getEarliestTime().isBefore(existingSeries.getLatestTime())) { throw new IllegalArgumentException("Unable to add time-series as dates overlap"); } LocalDateDoubleTimeSeries newSeries = existingSeries.noIntersectionOperation(series); if (_storePoints.replace(objectId, existingSeries, newSeries) == false) { throw new IllegalArgumentException("Concurrent modification"); } } else { if (_storePoints.putIfAbsent(objectId, series) != null) { throw new IllegalArgumentException("Concurrent modification"); } } final Instant now = Instant.now(); final UniqueId uniqueId = objectId.atLatestVersion(); changeManager().entityChanged(ChangeType.CHANGED, objectId, null, null, now); return uniqueId; } //------------------------------------------------------------------------- @Override public UniqueId correctTimeSeriesDataPoints(ObjectIdentifiable objectKey, LocalDateDoubleTimeSeries series) { ArgumentChecker.notNull(objectKey, "objectKey"); ArgumentChecker.notNull(series, "series"); final ObjectId objectId = objectKey.getObjectId(); LocalDateDoubleTimeSeries existingSeries = _storePoints.get(objectId); if (existingSeries != null) { LocalDateDoubleTimeSeries newSeries = existingSeries.unionOperate(series, DoubleTimeSeriesOperators.SECOND_OPERATOR); if (_storePoints.replace(objectId, existingSeries, newSeries) == false) { throw new IllegalArgumentException("Concurrent modification"); } } else { if (_storePoints.putIfAbsent(objectId, series) != null) { throw new IllegalArgumentException("Concurrent modification"); } } final Instant now = Instant.now(); final UniqueId uniqueId = objectId.atLatestVersion(); changeManager().entityChanged(ChangeType.CHANGED, objectId, null, null, now); return uniqueId; } //------------------------------------------------------------------------- @Override public UniqueId removeTimeSeriesDataPoints(ObjectIdentifiable objectKey, LocalDate fromDateInclusive, LocalDate toDateInclusive) { ArgumentChecker.notNull(objectKey, "objectKey"); fromDateInclusive = Objects.firstNonNull(fromDateInclusive, LocalDate.of(1000, 1, 1)); // TODO: JSR-310 min/max date toDateInclusive = Objects.firstNonNull(toDateInclusive, LocalDate.of(9999, 1, 1)); ArgumentChecker.inOrderOrEqual(fromDateInclusive, toDateInclusive, "fromDateInclusive", "toDateInclusive"); final ObjectId objectId = objectKey.getObjectId(); LocalDateDoubleTimeSeries existingSeries = _storePoints.get(objectId); if (existingSeries == null) { return objectId.atLatestVersion(); } LocalDateDoubleTimeSeriesBuilder bld = existingSeries.toBuilder(); for (LocalDateDoubleEntryIterator it = bld.iterator(); it.hasNext(); ) { LocalDate date = it.nextTime(); if (date.isBefore(fromDateInclusive) == false && date.isAfter(toDateInclusive) == false) { it.remove(); } } if (_storePoints.replace(objectId, existingSeries, bld.build()) == false) { throw new IllegalArgumentException("Concurrent modification"); } return objectId.atLatestVersion(); } //------------------------------------------------------------------------- private long validateId(ObjectIdentifiable objectId) { ArgumentChecker.notNull(objectId, "objectId"); try { return Long.parseLong(objectId.getObjectId().getValue()); } catch (NumberFormatException ex) { throw new IllegalArgumentException("Invalid objectId " + objectId); } } }