/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.engine.view.client;
import java.util.HashSet;
import java.util.Set;
import java.util.Timer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.threeten.bp.Instant;
import com.opengamma.engine.marketdata.MarketDataInjector;
import com.opengamma.engine.resource.EngineResourceReference;
import com.opengamma.engine.resource.EngineResourceRetainer;
import com.opengamma.engine.value.ValueSpecification;
import com.opengamma.engine.view.ExecutionLogMode;
import com.opengamma.engine.view.ViewComputationResultModel;
import com.opengamma.engine.view.ViewDefinition;
import com.opengamma.engine.view.ViewDeltaResultModel;
import com.opengamma.engine.view.client.merging.RateLimitingMergingViewProcessListener;
import com.opengamma.engine.view.compilation.CompiledViewDefinition;
import com.opengamma.engine.view.compilation.CompiledViewDefinitionImpl;
import com.opengamma.engine.view.cycle.ViewCycle;
import com.opengamma.engine.view.cycle.ViewCycleMetadata;
import com.opengamma.engine.view.execution.ViewCycleExecutionOptions;
import com.opengamma.engine.view.execution.ViewExecutionOptions;
import com.opengamma.engine.view.impl.ViewProcessorImpl;
import com.opengamma.engine.view.listener.ViewResultListener;
import com.opengamma.engine.view.permission.ViewPermissionContext;
import com.opengamma.engine.view.permission.ViewPermissionProvider;
import com.opengamma.engine.view.permission.ViewPortfolioPermissionProvider;
import com.opengamma.id.UniqueId;
import com.opengamma.livedata.UserPrincipal;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.tuple.Pair;
/**
* Default implementation of {@link ViewClient}.
*/
public class ViewClientImpl implements ViewClient {
private static final Logger s_logger = LoggerFactory.getLogger(ViewClientImpl.class);
private final ReentrantLock _clientLock = new ReentrantLock();
private final UniqueId _id;
private final ViewProcessorImpl _viewProcessor;
private final UserPrincipal _user;
private final EngineResourceRetainer _latestCycleRetainer;
private final AtomicReference<ViewResultMode> _resultMode = new AtomicReference<>(ViewResultMode.FULL_ONLY);
private final AtomicReference<ViewResultMode> _fragmentResultMode = new AtomicReference<>(ViewResultMode.NONE);
private final AtomicBoolean _isViewCycleAccessSupported = new AtomicBoolean(false);
private final AtomicBoolean _isAttached = new AtomicBoolean(false);
private ViewClientState _state = ViewClientState.STARTED;
// Per-process state
private volatile ViewPermissionProvider _permissionProvider;
private volatile ViewPortfolioPermissionProvider _portfolioPermissionProvider;
@SuppressWarnings("unused")
private volatile boolean _canAccessCompiledViewDefinition;
@SuppressWarnings("unused")
private volatile boolean _canAccessComputationResults;
private volatile CountDownLatch _completionLatch = new CountDownLatch(0);
private final AtomicReference<ViewComputationResultModel> _latestResult = new AtomicReference<>();
private final AtomicReference<CompiledViewDefinition> _latestCompiledViewDefinition = new AtomicReference<>();
private final RateLimitingMergingViewProcessListener _mergingViewProcessListener;
private final AtomicReference<ViewResultListener> _userResultListener = new AtomicReference<>();
private final Set<Pair<String, ValueSpecification>> _elevatedLogSpecs = new HashSet<>();
/**
* Constructs an instance.
*
* @param id the unique identifier assigned to this view client
* @param viewProcessor the parent view processor to which this client belongs
* @param user the user who owns this client
* @param timer the timer to use for scheduled tasks
*/
public ViewClientImpl(UniqueId id, ViewProcessorImpl viewProcessor, UserPrincipal user, Timer timer) {
ArgumentChecker.notNull(id, "id");
ArgumentChecker.notNull(viewProcessor, "viewProcessor");
ArgumentChecker.notNull(user, "user");
ArgumentChecker.notNull(timer, "timer");
_id = id;
_viewProcessor = viewProcessor;
_user = user;
_latestCycleRetainer = new EngineResourceRetainer(viewProcessor.getViewCycleManager());
ViewResultListener mergedViewProcessListener = new ViewResultListener() {
@Override
public UserPrincipal getUser() {
return ViewClientImpl.this.getUser();
}
@Override
public void viewDefinitionCompiled(CompiledViewDefinition compiledViewDefinition,
boolean hasMarketDataPermissions) {
updateLatestCompiledViewDefinition(compiledViewDefinition);
_canAccessCompiledViewDefinition = _permissionProvider.canAccessCompiledViewDefinition(getUser(),
compiledViewDefinition);
_canAccessComputationResults = _permissionProvider.canAccessComputationResults(getUser(),
compiledViewDefinition,
hasMarketDataPermissions);
PortfolioFilter filter = _portfolioPermissionProvider.createPortfolioFilter(getUser());
// TODO [PLAT-1144] -- so we know whether or not the user is permissioned for various things, but what do we
// pass to downstream listeners? Some special perm denied message in place of results on each computation
// cycle?
// Would be better if there was a builder for this!
CompiledViewDefinition replacementViewDef = new CompiledViewDefinitionImpl(
compiledViewDefinition.getResolverVersionCorrection(),
compiledViewDefinition.getCompilationIdentifier(),
compiledViewDefinition.getViewDefinition(),
filter.generateRestrictedPortfolio(compiledViewDefinition.getPortfolio()),
compiledViewDefinition.getCompiledCalculationConfigurations(),
compiledViewDefinition.getValidFrom(),
compiledViewDefinition.getValidTo());
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
listener.viewDefinitionCompiled(replacementViewDef, hasMarketDataPermissions);
}
}
@Override
public void cycleStarted(ViewCycleMetadata cycleMetadata) {
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
// Only send cycle started results in conjunction with fragments
if (!getFragmentResultMode().equals(ViewResultMode.NONE)) {
listener.cycleStarted(cycleMetadata);
}
}
}
@Override
public void cycleCompleted(ViewComputationResultModel fullResult, ViewDeltaResultModel deltaResult) {
boolean isFirstResult = updateLatestResult(fullResult);
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
ViewResultMode resultMode = getResultMode();
if (!resultMode.equals(ViewResultMode.NONE)) {
ViewComputationResultModel userFullResult = isFullResultRequired(resultMode,
isFirstResult) ? fullResult : null;
ViewDeltaResultModel userDeltaResult = isDeltaResultRequired(resultMode,
isFirstResult) ? deltaResult : null;
if (userFullResult != null || userDeltaResult != null) {
listener.cycleCompleted(userFullResult, userDeltaResult);
} else if (!isFirstResult || resultMode != ViewResultMode.DELTA_ONLY) {
// Would expect this if it's the first result and we're in delta only mode, otherwise log a warning
s_logger.warn("Ignored CycleCompleted call with no useful results to propagate");
}
}
}
}
@Override
public void cycleFragmentCompleted(ViewComputationResultModel fullFragment, ViewDeltaResultModel deltaFragment) {
ViewComputationResultModel prevResult = _latestResult.get();
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
ViewResultMode resultMode = getFragmentResultMode();
if (!resultMode.equals(ViewResultMode.NONE)) {
ViewComputationResultModel userFullResult = isFullResultRequired(resultMode,
prevResult == null) ? fullFragment : null;
ViewDeltaResultModel userDeltaResult = isDeltaResultRequired(resultMode,
prevResult == null) ? deltaFragment : null;
if (userFullResult != null || userDeltaResult != null) {
listener.cycleFragmentCompleted(userFullResult, userDeltaResult);
} else if (prevResult == null || resultMode != ViewResultMode.DELTA_ONLY) {
// Would expect this if it's the first result and we're in delta only mode, otherwise log a warning
s_logger.warn("Ignored CycleFragmentCompleted call with no useful results to propagate");
}
}
}
}
@Override
public void cycleExecutionFailed(ViewCycleExecutionOptions executionOptions, Exception exception) {
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
listener.cycleExecutionFailed(executionOptions, exception);
}
}
@Override
public void viewDefinitionCompilationFailed(Instant valuationTime, Exception exception) {
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
listener.viewDefinitionCompilationFailed(valuationTime, exception);
}
}
@Override
public void processCompleted() {
ViewClientImpl.this.processCompleted();
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
listener.processCompleted();
}
}
@Override
public void processTerminated(boolean executionInterrupted) {
ViewClientImpl.this.detachFromViewProcess();
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
listener.processTerminated(executionInterrupted);
}
}
@Override
public void clientShutdown(Exception e) {
throw new UnsupportedOperationException(
"Shutdown notification unexpectedly received from merging result listener");
}
};
_mergingViewProcessListener = new RateLimitingMergingViewProcessListener(mergedViewProcessListener, getViewProcessor().getViewCycleManager(), timer);
_mergingViewProcessListener.setPaused(true);
}
@Override
public UniqueId getUniqueId() {
return _id;
}
@Override
public UserPrincipal getUser() {
return _user;
}
@Override
public ViewProcessorImpl getViewProcessor() {
return _viewProcessor;
}
@Override
public ViewClientState getState() {
return _state;
}
//-------------------------------------------------------------------------
@Override
public boolean isAttached() {
return _isAttached.get();
}
@Override
public void attachToViewProcess(UniqueId viewDefinitionId, ViewExecutionOptions executionOptions) {
attachToViewProcess(viewDefinitionId, executionOptions, false);
}
@Override
public void attachToViewProcess(UniqueId viewDefinitionId, ViewExecutionOptions executionOptions, boolean privateProcess) {
_clientLock.lock();
try {
checkNotTerminated();
// The client is detached right now so the merging update listener is paused. Although the following calls may
// cause initial updates to be pushed through, they will not be seen until the merging update listener is
// resumed, at which point the new permission provider will be in place.
ViewProcessorImpl viewProcessor = getViewProcessor();
ViewPermissionContext context = privateProcess ?
viewProcessor.attachClientToPrivateViewProcess(getUniqueId(), _mergingViewProcessListener, viewDefinitionId, executionOptions) :
viewProcessor.attachClientToSharedViewProcess(getUniqueId(), _mergingViewProcessListener, viewDefinitionId, executionOptions);
attachToViewProcessCore(context);
} finally {
_clientLock.unlock();
}
}
@Override
public void attachToViewProcess(UniqueId processId) {
_clientLock.lock();
try {
checkNotTerminated();
ViewPermissionContext context =
getViewProcessor().attachClientToViewProcess(getUniqueId(), _mergingViewProcessListener, processId);
attachToViewProcessCore(context);
} finally {
_clientLock.unlock();
}
}
private void attachToViewProcessCore(ViewPermissionContext context) {
_permissionProvider = context.getViewPermissionProvider();
_portfolioPermissionProvider = context.getViewPortfolioPermissionProvider();
_isAttached.set(true);
boolean isPaused = getState() == ViewClientState.PAUSED;
_mergingViewProcessListener.setPaused(isPaused);
_completionLatch = new CountDownLatch(1);
}
@Override
public void detachFromViewProcess() {
_clientLock.lock();
try {
processCompleted();
getViewProcessor().detachClientFromViewProcess(getUniqueId());
getLatestCycleRetainer().replaceRetainedCycle(null);
_mergingViewProcessListener.setPaused(true);
_mergingViewProcessListener.reset();
_latestResult.set(null);
_isAttached.set(false);
_permissionProvider = null;
} finally {
_clientLock.unlock();
}
}
@Override
public MarketDataInjector getLiveDataOverrideInjector() {
// [PLAT-1174] - this shouldn't be here
return getViewProcessor().getLiveDataOverrideInjector(getUniqueId());
}
@Override
public ViewDefinition getLatestViewDefinition() {
return getViewProcessor().getLatestViewDefinition(getUniqueId());
}
//-------------------------------------------------------------------------
@Override
public void setResultListener(ViewResultListener resultListener) {
_userResultListener.set(resultListener);
}
@Override
public void setUpdatePeriod(long periodMillis) {
_mergingViewProcessListener.setMinimumUpdatePeriodMillis(periodMillis);
}
@Override
public ViewResultMode getResultMode() {
return _resultMode.get();
}
@Override
public void setResultMode(ViewResultMode resultMode) {
_resultMode.set(resultMode);
}
@Override
public ViewResultMode getFragmentResultMode() {
return _fragmentResultMode.get();
}
@Override
public void setFragmentResultMode(ViewResultMode fragmentResultMode) {
_fragmentResultMode.set(fragmentResultMode);
}
//-------------------------------------------------------------------------
@Override
public void pause() {
_clientLock.lock();
try {
checkNotTerminated();
if (isAttached()) {
_mergingViewProcessListener.setPaused(true);
}
_state = ViewClientState.PAUSED;
} finally {
_clientLock.unlock();
}
}
@Override
public void resume() {
_clientLock.lock();
try {
checkNotTerminated();
if (isAttached()) {
_mergingViewProcessListener.setPaused(false);
}
_state = ViewClientState.STARTED;
} finally {
_clientLock.unlock();
}
}
@Override
public void triggerCycle() {
getViewProcessor().triggerCycle(getUniqueId());
}
@Override
public boolean isCompleted() {
// Race condition between checking attachment and operating on the latch is fine; if the client is being attached
// concurrently then there's no guarantee which process this method refers to.
checkAttached();
return _completionLatch.getCount() == 0;
}
@Override
public void waitForCompletion() throws InterruptedException {
// Race condition between checking attachment and operating on the latch is fine; if the client is being attached
// concurrently then there's no guarantee which process this method refers to.
checkAttached();
try {
_completionLatch.await();
} catch (InterruptedException e) {
s_logger.debug("Interrupted while waiting for completion of the view process");
throw e;
}
}
@Override
public boolean isResultAvailable() {
// Race condition between checking attachment and getting the latest result is fine; if the client is being
// attached concurrently then there's no guarantee which process this method refers to.
checkAttached();
return _latestResult.get() != null;
}
@Override
public ViewComputationResultModel getLatestResult() {
// Race condition between checking attachment and getting the latest result is fine; if the client is being
// attached concurrently then there's no guarantee which process this method refers to.
checkAttached();
return _latestResult.get();
}
@Override
public CompiledViewDefinition getLatestCompiledViewDefinition() {
return _latestCompiledViewDefinition.get();
}
@Override
public boolean isViewCycleAccessSupported() {
return _isViewCycleAccessSupported.get();
}
@Override
public void setViewCycleAccessSupported(boolean isViewCycleAccessSupported) {
_clientLock.lock();
try {
_isViewCycleAccessSupported.set(isViewCycleAccessSupported);
_mergingViewProcessListener.setLatestResultCycleRetained(isViewCycleAccessSupported);
if (!isViewCycleAccessSupported) {
getLatestCycleRetainer().replaceRetainedCycle(null);
}
} finally {
_clientLock.unlock();
}
}
@Override
public EngineResourceReference<? extends ViewCycle> createLatestCycleReference() {
if (!isViewCycleAccessSupported()) {
throw new UnsupportedOperationException("Access to computation cycles is not supported from this client");
}
ViewComputationResultModel latestResult = getLatestResult();
if (latestResult == null) {
return null;
}
return _viewProcessor.getViewCycleManager().createReference(latestResult.getViewCycleId());
}
@Override
public EngineResourceReference<? extends ViewCycle> createCycleReference(UniqueId cycleId) {
if (!isViewCycleAccessSupported()) {
throw new UnsupportedOperationException("Access to computation cycles is not supported from this client");
}
return _viewProcessor.getViewCycleManager().createReference(cycleId);
}
//-------------------------------------------------------------------------
@Override
public void setMinimumLogMode(ExecutionLogMode minimumLogMode, Set<Pair<String, ValueSpecification>> resultSpecifications) {
_clientLock.lock();
try {
checkAttached();
Set<Pair<String, ValueSpecification>> affected = new HashSet<>(resultSpecifications);
switch (minimumLogMode) {
case INDICATORS:
affected.retainAll(_elevatedLogSpecs);
_elevatedLogSpecs.removeAll(affected);
break;
case FULL:
affected.removeAll(_elevatedLogSpecs);
_elevatedLogSpecs.addAll(affected);
break;
}
if (!affected.isEmpty()) {
getViewProcessor().getViewProcessForClient(getUniqueId()).getExecutionLogModeSource().setMinimumLogMode(minimumLogMode, affected);
}
} finally {
_clientLock.unlock();
}
}
//-------------------------------------------------------------------------
@Override
public void shutdown() {
_clientLock.lock();
try {
if (_state == ViewClientState.TERMINATED) {
return;
}
detachFromViewProcess();
getViewProcessor().removeViewClient(getUniqueId());
_mergingViewProcessListener.terminate();
_state = ViewClientState.TERMINATED;
ViewResultListener listener = _userResultListener.get();
if (listener != null) {
listener.clientShutdown(null);
}
} finally {
_clientLock.unlock();
}
}
//-------------------------------------------------------------------------
@Override
public String toString() {
return "ViewClient[" + getUniqueId() + "]";
}
//-------------------------------------------------------------------------
private void processCompleted() {
_completionLatch.countDown();
}
private void checkAttached() {
if (!isAttached()) {
throw new IllegalStateException("This method is not valid on a detached view client.");
}
}
private void checkNotTerminated() {
if (getState() == ViewClientState.TERMINATED) {
throw new IllegalStateException("The client has been terminated. It is not possible to use a terminated client.");
}
}
private EngineResourceRetainer getLatestCycleRetainer() {
return _latestCycleRetainer;
}
/**
* Updates the latest result.
*
* @param result the new result
* @return true if the new result was the first
*/
private boolean updateLatestResult(ViewComputationResultModel result) {
if (isViewCycleAccessSupported()) {
getLatestCycleRetainer().replaceRetainedCycle(result.getViewCycleId());
}
ViewComputationResultModel oldResult = _latestResult.getAndSet(result);
return oldResult == null;
}
private void updateLatestCompiledViewDefinition(CompiledViewDefinition compiledViewDefinition) {
_latestCompiledViewDefinition.set(compiledViewDefinition);
}
private boolean isFullResultRequired(ViewResultMode resultMode, boolean isFirstResult) {
switch (resultMode) {
case BOTH:
case FULL_ONLY:
return true;
case FULL_THEN_DELTA:
return isFirstResult;
default:
return false;
}
}
private boolean isDeltaResultRequired(ViewResultMode resultMode, boolean isFirstResult) {
switch (resultMode) {
case BOTH:
case DELTA_ONLY:
return true;
case FULL_THEN_DELTA:
return !isFirstResult;
default:
return false;
}
}
}