// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.sdk.internal.v8native.value;
import static org.chromium.sdk.util.BasicUtil.getSafe;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.internal.JsonUtil;
import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException;
import org.chromium.sdk.internal.v8native.DebugSession;
import org.chromium.sdk.internal.v8native.InternalContext;
import org.chromium.sdk.internal.v8native.InternalContext.ContextDismissedCheckedException;
import org.chromium.sdk.internal.v8native.V8BlockingCallback;
import org.chromium.sdk.internal.v8native.V8CommandCallbackBase;
import org.chromium.sdk.internal.v8native.V8Helper;
import org.chromium.sdk.internal.v8native.protocol.input.FailedCommandResponse;
import org.chromium.sdk.internal.v8native.protocol.input.ScopeBody;
import org.chromium.sdk.internal.v8native.protocol.input.SuccessCommandResponse;
import org.chromium.sdk.internal.v8native.protocol.input.V8ProtocolParserAccess;
import org.chromium.sdk.internal.v8native.protocol.input.data.ObjectValueHandle;
import org.chromium.sdk.internal.v8native.protocol.input.data.RefWithDisplayData;
import org.chromium.sdk.internal.v8native.protocol.input.data.SomeHandle;
import org.chromium.sdk.internal.v8native.protocol.input.data.ValueHandle;
import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessage;
import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessageFactory;
import org.chromium.sdk.internal.v8native.protocol.output.EvaluateMessage;
import org.chromium.sdk.internal.v8native.protocol.output.LookupMessage;
import org.chromium.sdk.internal.v8native.protocol.output.ScopeMessage;
import org.chromium.sdk.util.GenericCallback;
import org.chromium.sdk.util.MethodIsBlockingException;
import org.json.simple.JSONObject;
/**
* Implementation of {@link ValueLoader} that loads data by 'lookup' command and additionally
* collects data as reported by various addDataToMap methods.
* <p>V8 typically sends a lot of (unsolicited) data about properties. There could be various
* strategies about whether to parse and add them into a map or save parsing time and ignore.
*/
public class ValueLoaderImpl extends ValueLoader {
private final ConcurrentMap<Long, ValueMirror> refToMirror =
new ConcurrentHashMap<Long, ValueMirror>();
private final HandleManager specialHandleManager = new HandleManager();
private final InternalContext context;
private final LoadableString.Factory loadableStringFactory;
private final AtomicInteger cacheStateRef = new AtomicInteger(1);
public ValueLoaderImpl(InternalContext context) {
this.context = context;
this.loadableStringFactory = new StringFactory();
}
public LoadableString.Factory getLoadableStringFactory() {
return loadableStringFactory;
}
public HandleManager getSpecialHandleManager() {
return specialHandleManager;
}
@Override
public void clearCaches() {
cacheStateRef.incrementAndGet();
refToMirror.clear();
}
@Override
int getCurrentCacheState() {
return cacheStateRef.get();
}
@Override
public InternalContext getInternalContext() {
return context;
}
public void addHandleFromRefs(SomeHandle handle) {
if (HandleManager.isSpecialType(handle.type())) {
specialHandleManager.put(handle.handle(), handle);
} else {
ValueHandle valueHandle;
try {
valueHandle = handle.asValueHandle();
} catch (JsonProtocolParseException e) {
throw new RuntimeException(e);
}
addDataToMap(valueHandle);
}
}
public void addDisplayDataToMap(RefWithDisplayData refWithDisplayData) {
ValueMirror mirror = ValueMirror.createIfSure(refWithDisplayData);
if (mirror != null) {
mergeValueMirrorIntoMap(mirror.getRef(), mirror);
}
}
public ValueMirror addDataToMap(ValueHandle valueHandle) {
ValueMirror mirror = ValueMirror.create(valueHandle, getLoadableStringFactory());
return putValueMirrorIntoMapRecursive(mirror);
}
public ValueMirror addDataToMap(Long ref, JsValue.Type type, String className,
LoadableString loadableString, SubpropertiesMirror subpropertiesMirror) {
ValueMirror mirror =
ValueMirror.create(ref, type, className, loadableString, subpropertiesMirror);
return putValueMirrorIntoMapRecursive(mirror);
}
private ValueMirror putValueMirrorIntoMapRecursive(ValueMirror mirror) {
if (PRE_PARSE_PROPERTIES) {
SubpropertiesMirror subpropertiesMirror = mirror.getProperties();
if (subpropertiesMirror != null) {
subpropertiesMirror.reportAllProperties(this);
}
}
return mergeValueMirrorIntoMap(mirror.getRef(), mirror);
}
private ValueMirror mergeValueMirrorIntoMap(Long ref, ValueMirror mirror) {
while (true) {
ValueMirror old = refToMirror.putIfAbsent(ref, mirror);
if (old == null) {
return mirror;
}
ValueMirror merged = ValueMirror.merge(old, mirror);
if (merged == old) {
return merged;
}
boolean updated = refToMirror.replace(ref, old, merged);
if (updated) {
return merged;
}
}
}
private static final boolean PRE_PARSE_PROPERTIES = false;
/**
* Looks up {@link ValueMirror} in map, loads them if needed or reloads them
* if property data is unavailable (or expired).
*/
@Override
public SubpropertiesMirror getOrLoadSubproperties(Long ref) throws MethodIsBlockingException {
ValueMirror mirror = getSafe(refToMirror, ref);
SubpropertiesMirror references;
if (mirror == null) {
references = null;
} else {
references = mirror.getProperties();
}
if (references == null) {
// need to look up this value again
List<ValueMirror> loadedMirrors =
loadValuesFromRemote(Collections.singletonList(ref));
ValueMirror loadedMirror = loadedMirrors.get(0);
references = loadedMirror.getProperties();
if (references == null) {
throw new RuntimeException("Failed to load properties");
}
}
return references;
}
/**
* Looks up data for scope on remote in form of scope object handle.
*/
public ObjectValueHandle loadScopeFields(ScopeMessage.Ref scopeRef)
throws MethodIsBlockingException {
DebuggerMessage message = new ScopeMessage(scopeRef);
V8BlockingCallback<ObjectValueHandle> callback = new V8BlockingCallback<ObjectValueHandle>() {
@Override
protected ObjectValueHandle handleSuccessfulResponse(
SuccessCommandResponse response) {
return readFromScopeResponse(response);
}
};
try {
return V8Helper.callV8Sync(context, message, callback);
} catch (ContextDismissedCheckedException e) {
context.getDebugSession().maybeRethrowContextException(e);
// or
return null;
}
}
private ObjectValueHandle readFromScopeResponse(SuccessCommandResponse response) {
List<SomeHandle> refs = response.refs();
for (SomeHandle handle : refs) {
addHandleFromRefs(handle);
}
ScopeBody body;
try {
body = response.body().asScopeBody();
} catch (JsonProtocolParseException e) {
throw new ValueLoadException(e);
}
return body.object();
}
/**
* For each PropertyReference from propertyRefs tries to either: 1. read it from PropertyReference
* (possibly cached value) or 2. lookup value by refId from remote
*/
@Override
public List<ValueMirror> getOrLoadValueFromRefs(List<? extends PropertyReference> propertyRefs)
throws MethodIsBlockingException {
ValueMirror[] result = new ValueMirror[propertyRefs.size()];
Map<Long, Integer> refToRequestIndex = new HashMap<Long, Integer>();
List<PropertyReference> needsLoading = new ArrayList<PropertyReference>();
for (int i = 0; i < propertyRefs.size(); i++) {
PropertyReference property = propertyRefs.get(i);
DataWithRef dataWithRef = property.getValueObject();
long ref = dataWithRef.ref();
ValueMirror mirror = getSafe(refToMirror, ref);
if (mirror == null) {
RefWithDisplayData dataWithDisplayData = dataWithRef.getWithDisplayData();
if (dataWithDisplayData != null) {
mirror = ValueMirror.createIfSure(dataWithDisplayData);
}
}
if (mirror == null) {
// We don't have the data (enough) right now. We are requesting them from server.
// There might be simultaneous request for the same value, which is a normal though
// undesired case.
Integer requestPos = getSafe(refToRequestIndex, ref);
if (requestPos == null) {
refToRequestIndex.put(ref, needsLoading.size());
needsLoading.add(property);
}
} else {
result[i] = mirror;
}
}
if (!needsLoading.isEmpty()) {
List<Long> refIds = getRefIdFromReferences(needsLoading);
List<ValueMirror> loadedMirrors = loadValuesFromRemote(refIds);
assert refIds.size() == loadedMirrors.size();
for (int i = 0; i < propertyRefs.size(); i++) {
if (result[i] == null) {
PropertyReference property = propertyRefs.get(i);
DataWithRef dataWithRef = property.getValueObject();
Long ref = dataWithRef.ref();
int pos = getSafe(refToRequestIndex, ref);
result[i] = loadedMirrors.get(pos);
}
}
}
return Arrays.asList(result);
}
private static List<Long> getRefIdFromReferences(final List<PropertyReference> propertyRefs) {
List<Long> result = new ArrayList<Long>(propertyRefs.size());
for (PropertyReference ref : propertyRefs) {
result.add(Long.valueOf(ref.getRef()));
}
return result;
}
/**
* Requests values from remote via "lookup" command. Automatically caches received data.
* @param propertyRefIds list of ref ids we need to look up
* @return loaded value mirrors in the same order as in propertyRefIds
*/
public List<ValueMirror> loadValuesFromRemote(final List<Long> propertyRefIds)
throws MethodIsBlockingException {
if (propertyRefIds.isEmpty()) {
return Collections.emptyList();
}
DebuggerMessage message = DebuggerMessageFactory.lookup(propertyRefIds, false);
V8BlockingCallback<List<ValueMirror>> callback =
new V8BlockingCallback<List<ValueMirror>>() {
@Override
protected List<ValueMirror> handleSuccessfulResponse(
SuccessCommandResponse response) {
return readResponseFromLookup(response, propertyRefIds);
}
};
try {
return V8Helper.callV8Sync(context, message, callback);
} catch (ContextDismissedCheckedException e) {
context.getDebugSession().maybeRethrowContextException(e);
// or
throw new ValueLoadException("Invalid context", e);
}
}
private List<ValueMirror> readResponseFromLookup(
SuccessCommandResponse successResponse, List<Long> propertyRefIds) {
List<ValueMirror> result = new ArrayList<ValueMirror>(propertyRefIds.size());
JSONObject body;
try {
body = successResponse.body().asLookupMap();
} catch (JsonProtocolParseException e) {
throw new ValueLoadException(e);
}
for (int i = 0; i < propertyRefIds.size(); i++) {
int ref = propertyRefIds.get(i).intValue();
JSONObject value = JsonUtil.getAsJSON(body, String.valueOf(ref));
if (value == null) {
throw new ValueLoadException("Failed to find value for ref=" + ref);
}
ValueHandle valueHandle;
try {
valueHandle = V8ProtocolParserAccess.get().parseValueHandle(value);
} catch (JsonProtocolParseException e) {
throw new RuntimeException(e);
}
long refLong = valueHandle.handle();
if (refLong != ref) {
throw new ValueLoadException("Inconsistent ref in response, ref=" + ref);
}
ValueMirror mirror = addDataToMap(valueHandle);
result.add(mirror);
}
return result;
}
private List<ValueHandle> readResponseFromLookupRaw(SuccessCommandResponse successResponse,
List<Long> propertyRefIds) {
List<ValueHandle> result = new ArrayList<ValueHandle>(propertyRefIds.size());
JSONObject body;
try {
body = successResponse.body().asLookupMap();
} catch (JsonProtocolParseException e) {
throw new ValueLoadException(e);
}
for (int i = 0; i < propertyRefIds.size(); i++) {
int ref = propertyRefIds.get(i).intValue();
JSONObject value = JsonUtil.getAsJSON(body, String.valueOf(ref));
if (value == null) {
throw new ValueLoadException("Failed to find value for ref=" + ref);
}
ValueHandle valueHandle;
try {
valueHandle = V8ProtocolParserAccess.get().parseValueHandle(value);
} catch (JsonProtocolParseException e) {
throw new ValueLoadException(e);
}
addDataToMap(valueHandle);
result.add(valueHandle);
}
return result;
}
private RelayOk relookupValue(long handleId, Long maxLength,
final GenericCallback<ValueHandle> callback,
SyncCallback syncCallback) throws ContextDismissedCheckedException {
final List<Long> ids = Collections.singletonList(handleId);
DebuggerMessage message = new LookupMessage(ids, false, maxLength);
V8CommandCallbackBase innerCallback = new V8CommandCallbackBase() {
@Override
public void success(SuccessCommandResponse successResponse) {
List<ValueHandle> handleList = readResponseFromLookupRaw(successResponse, ids);
callback.success(handleList.get(0));
}
@Override
public void failure(String message, FailedCommandResponse.ErrorDetails errorDetails) {
callback.failure(new Exception(message));
}
};
return this.context.sendV8CommandAsync(message, true, innerCallback, syncCallback);
}
private class StringFactory implements LoadableString.Factory {
@Override
public LoadableString create(ValueHandle handle) {
final long handleId = handle.handle();
final LoadedValue initialValue = new LoadedValue(handle);
return new LoadableString() {
private final AtomicReference<LoadedValue> valueRef =
new AtomicReference<LoadedValue>(initialValue);
@Override
public String getCurrentString() {
return valueRef.get().stringValue;
}
@Override
public boolean needsReload() {
LoadedValue loadedValue = valueRef.get();
return loadedValue.loadedSize < loadedValue.actualSize;
}
@Override
public RelayOk reloadBigger(final GenericCallback<Void> callback,
SyncCallback syncCallback) {
long currentlyLoadedSize = valueRef.get().loadedSize;
long newRequstedSize = chooseNewMaxStringLength(currentlyLoadedSize);
GenericCallback<ValueHandle> innerCallback =
new GenericCallback<ValueHandle>() {
@Override
public void success(ValueHandle handle) {
LoadedValue newLoadedValue = new LoadedValue(handle);
replaceValue(handle, newLoadedValue);
if (callback != null) {
callback.success(null);
}
}
@Override
public void failure(Exception e) {
if (callback != null) {
callback.failure(new Exception(e));
}
}
};
try {
return relookupValue(handleId, newRequstedSize, innerCallback, syncCallback);
} catch (final ContextDismissedCheckedException e) {
DebugSession debugSession = context.getDebugSession();
debugSession.maybeRethrowContextException(e);
// or
return debugSession.sendLoopbackMessage(new Runnable() {
@Override
public void run() {
if (callback != null) {
callback.failure(e);
}
}
}, syncCallback);
}
}
@Override
public EvaluateMessage.Value getProtocolDescription(InternalContext hostInternalContext) {
hostInternalContext.checkContextIsCompatible(context);
return EvaluateMessage.Value.createForId(handleId);
}
private void replaceValue(ValueHandle handle, LoadedValue newValue) {
while (true) {
LoadedValue currentValue = valueRef.get();
if (currentValue.loadedSize >= newValue.loadedSize) {
return;
}
boolean updated = valueRef.compareAndSet(currentValue, newValue);
if (updated) {
return;
}
}
}
private long chooseNewMaxStringLength(long currentSize) {
return Math.max(currentSize * 10, 64 * 1024);
}
};
}
private class LoadedValue {
final String stringValue;
final long loadedSize;
final long actualSize;
LoadedValue(ValueHandle handle) {
this.stringValue = (String) handle.value();
Long toIndex = handle.toIndex();
if (toIndex == null) {
this.loadedSize = this.stringValue.length();
this.actualSize = this.loadedSize;
} else {
this.loadedSize = toIndex;
this.actualSize = handle.length();
}
}
}
}
}