/**
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.opengamma.DataNotFoundException;
import com.opengamma.core.change.ChangeEvent;
import com.opengamma.core.change.ChangeListener;
import com.opengamma.id.ExternalBundleIdentifiable;
import com.opengamma.id.ExternalIdBundle;
import com.opengamma.id.UniqueId;
import com.opengamma.id.UniqueIdentifiable;
import com.opengamma.id.VersionCorrection;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.ehcache.EHCacheUtils;
import com.opengamma.util.tuple.Pair;
import com.opengamma.util.tuple.Pairs;
/**
* A cache decorating a {@link SourceWithExternalBundle}.
* <p>
* The cache is implemented using {@code EHCache}.
*
* @param <V> the type returned by the source
* @param <S> the source
*/
public abstract class AbstractEHCachingSourceWithExternalBundle<V extends UniqueIdentifiable & ExternalBundleIdentifiable, S extends SourceWithExternalBundle<V>>
extends AbstractEHCachingSource<V, S>
implements SourceWithExternalBundle<V> {
private static final Logger s_logger = LoggerFactory.getLogger(AbstractEHCachingSourceWithExternalBundle.class);
private static final String EID_TO_UID_CACHE = "-eid-to-uid";
/**
* The cache of external identifiers at a version/correction to matching unique identifiers.
*/
private final Cache _eidToUidCache;
/**
* Creates an instance over an underlying source specifying the cache manager.
*
* @param underlying the underlying security source, not null
* @param cacheManager the cache manager, not null
*/
public AbstractEHCachingSourceWithExternalBundle(final S underlying, final CacheManager cacheManager) {
super(underlying, cacheManager);
EHCacheUtils.addCache(cacheManager, this.getClass().getName() + EID_TO_UID_CACHE);
_eidToUidCache = EHCacheUtils.getCacheFromManager(cacheManager, this.getClass().getName() + EID_TO_UID_CACHE);
// this is crude but it allows caching of lookups that use VersionCorrection.LATEST.
// VersionCorrection.LATEST can refer to different versions of the same object, but by clearing the caches
// when anything in the underlying source changes we ensure we never see stale data.
// this won't work well if the data changes frequently.
getUnderlying().changeManager().addChangeListener(new ChangeListener() {
@Override
public void entityChanged(ChangeEvent event) {
_eidToUidCache.flush();
}
});
}
@Override
public Collection<V> get(final ExternalIdBundle bundle) {
final Collection<V> result = getUnderlying().get(bundle);
cacheItems(result);
return result;
}
@SuppressWarnings("unchecked")
@Override
public Collection<V> get(final ExternalIdBundle bundle, final VersionCorrection versionCorrection) {
ArgumentChecker.notNull(bundle, "bundle");
ArgumentChecker.notNull(versionCorrection, "versionCorrection");
if (versionCorrection.containsLatest()) {
Collection<V> results = getUnderlying().get(bundle, versionCorrection);
cacheItems(results);
return results;
}
final Pair<ExternalIdBundle, VersionCorrection> key = Pairs.of(bundle, versionCorrection);
final Element e = _eidToUidCache.get(key);
if (e != null) {
if (e.getObjectValue() instanceof Collection) {
final Collection<UniqueId> identifiers = (Collection<UniqueId>) e.getObjectValue();
if (identifiers.isEmpty()) {
return Collections.emptySet();
} else {
return get(identifiers).values();
}
}
}
final Collection<V> result = getUnderlying().get(bundle, versionCorrection);
if (result.isEmpty()) {
cacheIdentifiers(Collections.<UniqueId>emptyList(), key);
} else {
final List<UniqueId> uids = new ArrayList<UniqueId>(result.size());
for (final V item : result) {
uids.add(item.getUniqueId());
}
Collections.sort(uids);
cacheIdentifiers(uids, key);
cacheItems(result);
}
return result;
}
@SuppressWarnings("unchecked")
@Override
public Map<ExternalIdBundle, Collection<V>> getAll(final Collection<ExternalIdBundle> bundles, VersionCorrection versionCorrection) {
ArgumentChecker.notNull(bundles, "bundles");
ArgumentChecker.notNull(versionCorrection, "versionCorrection");
if (versionCorrection.containsLatest()) {
return getUnderlying().getAll(bundles, versionCorrection);
}
final Map<ExternalIdBundle, Collection<V>> results = Maps.newHashMapWithExpectedSize(bundles.size());
final Collection<ExternalIdBundle> misses = new ArrayList<ExternalIdBundle>(bundles.size());
final Map<ExternalIdBundle, Collection<UniqueId>> lookupBundles = Maps.newHashMapWithExpectedSize(bundles.size());
final Set<UniqueId> lookupIds = Sets.newHashSetWithExpectedSize(bundles.size());
for (ExternalIdBundle bundle : bundles) {
final Pair<ExternalIdBundle, VersionCorrection> key = Pairs.of(bundle, versionCorrection);
final Element e = _eidToUidCache.get(key);
if (e != null) {
if (e.getObjectValue() instanceof Collection) {
final Collection<UniqueId> identifiers = (Collection<UniqueId>) e.getObjectValue();
if (identifiers.isEmpty()) {
results.put(bundle, Collections.<V>emptySet());
} else {
lookupBundles.put(bundle, identifiers);
lookupIds.addAll(identifiers);
}
continue;
}
}
misses.add(bundle);
}
if (!lookupIds.isEmpty()) {
final Map<UniqueId, V> underlying = get(lookupIds);
for (Map.Entry<ExternalIdBundle, Collection<UniqueId>> lookupBundle : lookupBundles.entrySet()) {
final ArrayList<V> resultCollection = new ArrayList<V>(lookupBundle.getValue().size());
for (UniqueId uid : lookupBundle.getValue()) {
final V resultValue = underlying.get(uid);
if (resultValue != null) {
resultCollection.add(resultValue);
}
}
resultCollection.trimToSize();
results.put(lookupBundle.getKey(), resultCollection);
}
}
if (!misses.isEmpty()) {
final Map<ExternalIdBundle, Collection<V>> underlying = getUnderlying().getAll(misses, versionCorrection);
for (ExternalIdBundle miss : misses) {
final Pair<ExternalIdBundle, VersionCorrection> key = Pairs.of(miss, versionCorrection);
final Collection<V> result = underlying.get(miss);
if ((result == null) || result.isEmpty()) {
cacheIdentifiers(Collections.<UniqueId>emptyList(), key);
} else {
final List<UniqueId> uids = new ArrayList<>(result.size());
for (final V item : result) {
uids.add(item.getUniqueId());
}
Collections.sort(uids);
cacheIdentifiers(uids, key);
cacheItems(result);
results.put(miss, result);
}
}
}
return results;
}
@Override
public V getSingle(final ExternalIdBundle bundle) {
return getSingle(bundle, VersionCorrection.LATEST);
}
@SuppressWarnings("unchecked")
@Override
public V getSingle(final ExternalIdBundle bundle, final VersionCorrection versionCorrection) {
ArgumentChecker.notNull(bundle, "bundle");
ArgumentChecker.notNull(versionCorrection, "versionCorrection");
final Pair<ExternalIdBundle, VersionCorrection> key = Pairs.of(bundle, versionCorrection);
final Element e = _eidToUidCache.get(key);
if (e != null) {
if (e.getObjectValue() instanceof List) {
final List<UniqueId> identifiers = (List<UniqueId>) e.getObjectValue();
for (final UniqueId uid : identifiers) {
V result;
try {
result = get(uid);
} catch (DataNotFoundException dnfe) {
s_logger.warn("Cached {} for {} no longer available", uid, key);
result = null;
}
if (result != null) {
return result;
}
}
return null;
} else if (e.getObjectValue() instanceof UniqueId) {
// REVIEW 2013-11-06 Andrew -- Get will probably throw a DNFE instead of returning NULL
final UniqueId uid = (UniqueId) e.getObjectValue();
try {
return get(uid);
} catch (DataNotFoundException dnfe) {
s_logger.warn("Cached {} for {} no longer available", uid, key);
return null;
}
}
}
final V result = getUnderlying().getSingle(bundle, versionCorrection);
if (result == null) {
cacheIdentifiers(Collections.<UniqueId>emptyList(), key);
} else {
cacheIdentifiers(result.getUniqueId(), key);
cacheItem(result);
}
return result;
}
@Override
@SuppressWarnings("unchecked")
public Map<ExternalIdBundle, V> getSingle(final Collection<ExternalIdBundle> bundles, final VersionCorrection versionCorrection) {
ArgumentChecker.notNull(bundles, "bundles");
ArgumentChecker.notNull(versionCorrection, "versionCorrection");
if (versionCorrection.containsLatest()) {
return getUnderlying().getSingle(bundles, versionCorrection);
}
final Map<ExternalIdBundle, V> results = Maps.newHashMapWithExpectedSize(bundles.size());
final Collection<ExternalIdBundle> misses = new ArrayList<ExternalIdBundle>(bundles.size());
final Map<ExternalIdBundle, Collection<UniqueId>> hits = Maps.newHashMapWithExpectedSize(bundles.size());
final Set<UniqueId> lookup = Sets.newHashSetWithExpectedSize(bundles.size());
for (ExternalIdBundle bundle : bundles) {
final Pair<ExternalIdBundle, VersionCorrection> key = Pairs.of(bundle, versionCorrection);
final Element e = _eidToUidCache.get(key);
if (e != null) {
if (e.getObjectValue() instanceof List) {
final List<UniqueId> identifiers = (List<UniqueId>) e.getObjectValue();
lookup.addAll(identifiers);
hits.put(bundle, identifiers);
continue;
} else if (e.getObjectValue() instanceof UniqueId) {
final UniqueId identifier = (UniqueId) e.getObjectValue();
lookup.add(identifier);
hits.put(bundle, Collections.singleton(identifier));
continue;
}
}
misses.add(bundle);
}
if (!lookup.isEmpty()) {
final Map<UniqueId, V> underlying = getUnderlying().get(lookup);
for (Map.Entry<ExternalIdBundle, Collection<UniqueId>> hit : hits.entrySet()) {
final ExternalIdBundle bundle = hit.getKey();
for (UniqueId uid : hit.getValue()) {
final V result = underlying.get(uid);
if (result != null) {
results.put(bundle, result);
break;
}
}
}
}
if (!misses.isEmpty()) {
final Map<ExternalIdBundle, ? extends V> underlying = getUnderlying().getSingle(misses, versionCorrection);
for (ExternalIdBundle miss : misses) {
final Pair<ExternalIdBundle, VersionCorrection> key = Pairs.of(miss, versionCorrection);
final V result = underlying.get(miss);
if (result == null) {
cacheIdentifiers(Collections.<UniqueId>emptyList(), key);
} else {
cacheIdentifiers(result.getUniqueId(), key);
cacheItem(result);
results.put(miss, result);
}
}
}
return results;
}
protected void cacheIdentifiers(final UniqueId uniqueId, final Pair<ExternalIdBundle, VersionCorrection> key) {
synchronized (_eidToUidCache) {
final Element e = _eidToUidCache.get(key);
if (e == null) {
_eidToUidCache.put(new Element(key, uniqueId));
}
}
}
protected void cacheIdentifiers(final List<UniqueId> uniqueIds, final Pair<ExternalIdBundle, VersionCorrection> key) {
synchronized (_eidToUidCache) {
_eidToUidCache.put(new Element(key, uniqueIds));
}
}
@Override
public void shutdown() {
super.shutdown();
getCacheManager().removeCache(EID_TO_UID_CACHE);
}
}