// 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.wip; import static org.chromium.sdk.util.BasicUtil.getSafe; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import org.chromium.sdk.FunctionScopeExtension; import org.chromium.sdk.JsArray; import org.chromium.sdk.JsFunction; import org.chromium.sdk.JsObject; import org.chromium.sdk.JsObjectProperty; import org.chromium.sdk.JsScope; import org.chromium.sdk.JsValue; import org.chromium.sdk.JsEvaluateContext.EvaluateCallback; import org.chromium.sdk.JsValue.Type; import org.chromium.sdk.JsVariable; import org.chromium.sdk.RelayOk; import org.chromium.sdk.Script; import org.chromium.sdk.SyncCallback; import org.chromium.sdk.TextStreamPosition; import org.chromium.sdk.internal.wip.WipExpressionBuilder.ObjectPropertyNameBuilder; import org.chromium.sdk.internal.wip.WipExpressionBuilder.PropertyNameBuilder; import org.chromium.sdk.internal.wip.WipExpressionBuilder.QualifiedNameBuilder; import org.chromium.sdk.internal.wip.WipExpressionBuilder.ValueNameBuilder; import org.chromium.sdk.internal.wip.WipValueLoader.Getter; import org.chromium.sdk.internal.wip.WipValueLoader.ObjectProperties; import org.chromium.sdk.internal.wip.protocol.input.debugger.FunctionDetailsValue; import org.chromium.sdk.internal.wip.protocol.input.debugger.LocationValue; import org.chromium.sdk.internal.wip.protocol.input.debugger.ScopeValue; import org.chromium.sdk.internal.wip.protocol.input.runtime.PropertyDescriptorValue; import org.chromium.sdk.internal.wip.protocol.input.runtime.RemoteObjectValue; import org.chromium.sdk.util.AsyncFutureRef; import org.chromium.sdk.util.MethodIsBlockingException; /** * A builder for implementations of {@link JsValue} and {@link JsVariable}. * It works in pair with {@link WipValueLoader}. */ class WipValueBuilder { private static final Logger LOGGER = Logger.getLogger(WipValueBuilder.class.getName()); private final WipValueLoader valueLoader; WipValueBuilder(WipValueLoader valueLoader) { this.valueLoader = valueLoader; } public JsObjectProperty createObjectProperty(final PropertyDescriptorValue propertyDescriptor, final String hostObjectRefId, ValueNameBuilder nameBuilder) { final QualifiedNameBuilder qualifiedNameBuilder = nameBuilder.getQualifiedNameBuilder(); JsValue jsValue = wrap(propertyDescriptor.value(), qualifiedNameBuilder); final JsValue getter = wrapPropertyDescriptorFunction(propertyDescriptor.get(), qualifiedNameBuilder, "getter"); final JsValue setter = wrapPropertyDescriptorFunction(propertyDescriptor.set(), qualifiedNameBuilder, "setter"); return new ObjectPropertyBase(jsValue, nameBuilder) { @Override public boolean isWritable() { return propertyDescriptor.writable(); } @Override public JsValue getGetter() { return getter; } @Override public JsValue getSetter() { return setter; } @Override public boolean isConfigurable() { return propertyDescriptor.configurable(); } @Override public boolean isEnumerable() { return propertyDescriptor.enumerable(); } @Override public JsFunction getGetterAsFunction() { JsObject getterObject = getter.asObject(); if (getterObject == null) { return null; } return getterObject.asFunction(); } @Override public RelayOk evaluateGet(EvaluateCallback callback, SyncCallback syncCallback) { WipContextBuilder.GlobalEvaluateContext evaluateContext = new WipContextBuilder.GlobalEvaluateContext(valueLoader); JsFunction getterFunction = getGetterAsFunction(); if (getterFunction == null) { throw new RuntimeException("Getter is not a function"); } Map<String, String> context = new HashMap<String, String>(2); context.put(GETTER_VAR_NAME, getterFunction.getRefId()); context.put(OBJECT_VAR_NAME, hostObjectRefId); final QualifiedNameBuilder pseudoPropertyNameBuilder = createPseudoPropertyNameBuilder(qualifiedNameBuilder, "value"); ValueNameBuilder valueNameBuilder = new ValueNameBuilder() { @Override public String getShortName() { return "value"; } @Override public QualifiedNameBuilder getQualifiedNameBuilder() { return pseudoPropertyNameBuilder; } }; return evaluateContext.evaluateAsync(EVALUATE_EXPRESSION, valueNameBuilder, context, callback, syncCallback); } private static final String GETTER_VAR_NAME = "gttr"; private static final String OBJECT_VAR_NAME = "obj"; private static final String EVALUATE_EXPRESSION = GETTER_VAR_NAME + ".call(" + OBJECT_VAR_NAME + ")"; }; } private static QualifiedNameBuilder createPseudoPropertyNameBuilder( final QualifiedNameBuilder propertyValueNameBuilder, final String symbolicName) { return new QualifiedNameBuilder() { @Override public boolean needsParentheses() { return false; } @Override public void append(StringBuilder output) { propertyValueNameBuilder.append(output); output.append("::[[").append(symbolicName).append("]]"); } }; } private JsValue wrapPropertyDescriptorFunction(RemoteObjectValue value, QualifiedNameBuilder propertyValueNameBuilder, String symbolicName) { if (value == null) { return null; } QualifiedNameBuilder qualifiedNameBuilder = createPseudoPropertyNameBuilder(propertyValueNameBuilder, symbolicName); return wrap(value, qualifiedNameBuilder); } public JsVariable createVariable(RemoteObjectValue valueData, ValueNameBuilder nameBuilder) { QualifiedNameBuilder qualifiedNameBuilder; if (nameBuilder == null) { qualifiedNameBuilder = null; } else { qualifiedNameBuilder = nameBuilder.getQualifiedNameBuilder(); } JsValue jsValue = wrap(valueData, qualifiedNameBuilder); return createVariable(jsValue, nameBuilder); } public JsValue wrap(RemoteObjectValue valueData, QualifiedNameBuilder nameBuilder) { if (valueData == null) { return null; } return getValueType(valueData).build(valueData, valueLoader, nameBuilder); } public static JsVariable createVariable(JsValue jsValue, ValueNameBuilder nameBuilder) { return new VariableImpl(jsValue, nameBuilder); } private static ValueType getValueType(RemoteObjectValue valueData) { RemoteObjectValue.Type protocolType = valueData.type(); ValueType result = getSafe(PROTOCOL_TYPE_TO_VALUE_TYPE, protocolType); if (result == null) { LOGGER.severe("Unexpected value type: " + protocolType); result = DEFAULT_VALUE_TYPE; } return result; } private static abstract class ValueType { abstract JsValue build(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder qualifiedNameBuilder); } private static abstract class PrimitiveType extends ValueType { private final JsValue.Type jsValueType; PrimitiveType(JsValue.Type jsValueType) { this.jsValueType = jsValueType; } protected abstract String getValueString(RemoteObjectValue valueData); @Override JsValue build(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder qualifiedNameBuilder) { final String valueString = getValueString(valueData); return new JsValue() { @Override public Type getType() { return jsValueType; } @Override public String getValueString() { return valueString; } @Override public JsObject asObject() { return null; } @Override public boolean isTruncated() { return false; } @Override public RelayOk reloadHeavyValue(ReloadBiggerCallback callback, SyncCallback syncCallback) { throw new UnsupportedOperationException(); } }; } } private static class SingletonPrimitiveType extends PrimitiveType { private final String stringValue; SingletonPrimitiveType(Type jsValueType, String stringValue) { super(jsValueType); this.stringValue = stringValue; } @Override protected String getValueString(RemoteObjectValue valueData) { return stringValue; } } private static class PrimitiveTypeWithDescription extends PrimitiveType { PrimitiveTypeWithDescription(Type jsValueType) { super(jsValueType); } @Override protected String getValueString(RemoteObjectValue valueData) { return valueData.description(); } } private static class PrimitiveTypeWithValue extends PrimitiveType { PrimitiveTypeWithValue(Type jsValueType) { super(jsValueType); } @Override protected String getValueString(RemoteObjectValue valueData) { return valueData.value().toString(); } } private static abstract class ObjectTypeBase extends ValueType { private final JsValue.Type jsValueType; ObjectTypeBase(Type jsValueType) { this.jsValueType = jsValueType; } @Override JsValue build(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder qualifiedNameBuilder) { // TODO: Implement caching here. return buildNewInstance(valueData, valueLoader, qualifiedNameBuilder); } abstract JsValue buildNewInstance(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder qualifiedNameBuilder); abstract class JsObjectBase implements JsObject { private final RemoteObjectValue valueData; private final WipValueLoader valueLoader; private final QualifiedNameBuilder nameBuilder; private final AsyncFutureRef<Getter<ObjectProperties>> loadedPropertiesRef = new AsyncFutureRef<Getter<ObjectProperties>>(); JsObjectBase(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder nameBuilder) { this.valueData = valueData; this.valueLoader = valueLoader; this.nameBuilder = nameBuilder; } @Override public Type getType() { return jsValueType; } @Override public String getValueString() { return valueData.description(); } @Override public JsObject asObject() { return this; } @Override public boolean isTruncated() { return false; } @Override public String getClassName() { return valueData.className(); } @Override public RelayOk reloadHeavyValue(ReloadBiggerCallback callback, SyncCallback syncCallback) { throw new UnsupportedOperationException(); } @Override public Collection<? extends JsObjectProperty> getProperties() throws MethodIsBlockingException { return getLoadedProperties().properties(); } @Override public Collection<? extends JsVariable> getInternalProperties() throws MethodIsBlockingException { return getLoadedProperties().internalProperties(); } @Override public JsVariable getProperty(String name) throws MethodIsBlockingException { return getLoadedProperties().getProperty(name); } @Override public String getRefId() { return valueData.objectId(); } @Override public WipValueLoader getRemoteValueMapping() { return valueLoader; } protected RemoteObjectValue getValueData() { return valueData; } protected ObjectProperties getLoadedProperties() throws MethodIsBlockingException { int currentCacheState = getRemoteValueMapping().getCacheState(); if (loadedPropertiesRef.isInitialized()) { ObjectProperties result = loadedPropertiesRef.getSync().get(); if (currentCacheState == result.getCacheState()) { return result; } doLoadProperties(true, currentCacheState); } else { doLoadProperties(false, currentCacheState); } return loadedPropertiesRef.getSync().get(); } private void doLoadProperties(boolean reload, int currentCacheState) throws MethodIsBlockingException { PropertyNameBuilder innerNameBuilder; if (nameBuilder == null) { innerNameBuilder = null; } else { innerNameBuilder = new ObjectPropertyNameBuilder(nameBuilder); } valueLoader.loadJsObjectPropertiesInFuture(valueData.objectId(), innerNameBuilder, reload, currentCacheState, loadedPropertiesRef); } } } private static class ObjectSubtype extends ObjectTypeBase { ObjectSubtype(JsValue.Type type) { super(type); } @Override JsValue buildNewInstance(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder qualifiedNameBuilder) { return new ObjectTypeBase.JsObjectBase(valueData, valueLoader, qualifiedNameBuilder) { @Override public JsArray asArray() { return null; } @Override public JsFunction asFunction() { return null; } }; } } private static class ArrayType extends ObjectTypeBase { ArrayType() { super(JsValue.Type.TYPE_ARRAY); } @Override JsValue buildNewInstance(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder nameBuilder) { return new Array(valueData, valueLoader, nameBuilder); } private class Array extends JsObjectBase implements JsArray { private final AtomicReference<ArrayProperties> arrayPropertiesRef = new AtomicReference<ArrayProperties>(null); Array(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder nameBuilder) { super(valueData, valueLoader, nameBuilder); } @Override public JsArray asArray() { return this; } @Override public JsFunction asFunction() { return null; } @Override public int length() throws MethodIsBlockingException { return getArrayProperties().getLength(); } @Override public JsVariable get(int index) throws MethodIsBlockingException { return getArrayProperties().getSparseArrayMap().get(index); } @Override public SortedMap<Integer, ? extends JsVariable> toSparseArray() throws MethodIsBlockingException { return getArrayProperties().getSparseArrayMap(); } private ArrayProperties getArrayProperties() throws MethodIsBlockingException { ArrayProperties result = arrayPropertiesRef.get(); if (result == null) { ArrayProperties arrayProperties = buildArrayProperties(); // Only set if concurrent thread hasn't set its version arrayPropertiesRef.compareAndSet(null, arrayProperties); return arrayPropertiesRef.get(); } else { return result; } } private ArrayProperties buildArrayProperties() throws MethodIsBlockingException { ObjectProperties loadedProperties = getLoadedProperties(); final TreeMap<Integer, JsVariable> map = new TreeMap<Integer, JsVariable>(); JsValue lengthValue = null; for (JsVariable variable : loadedProperties.properties()) { String name = variable.getName(); if (WipExpressionBuilder.ALL_DIGITS.matcher(name).matches()) { Integer number = Integer.valueOf(name); map.put(number, variable); } else if ("length".equals(name)) { lengthValue = variable.getValue(); } } int length; try { length = Integer.parseInt(lengthValue.getValueString()); } catch (NumberFormatException e) { length = -1; } return new ArrayProperties(length, map); } } private static class ArrayProperties { final int length; final SortedMap<Integer, ? extends JsVariable> sparseArrayMap; ArrayProperties(int length, SortedMap<Integer, ? extends JsVariable> sparseArrayMap) { this.length = length; this.sparseArrayMap = sparseArrayMap; } public int getLength() { return length; } public SortedMap<Integer, ? extends JsVariable> getSparseArrayMap() { return sparseArrayMap; } } } private static class FunctionType extends ObjectTypeBase { FunctionType() { super(JsValue.Type.TYPE_FUNCTION); } @Override JsValue buildNewInstance(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder nameBuilder) { return new FunctionValueImpl(valueData, valueLoader, nameBuilder); } private class FunctionValueImpl extends ObjectTypeBase.JsObjectBase implements JsFunction, FunctionScopeAccess { private final AsyncFutureRef<Getter<FunctionDetailsValue>> loadedPositionRef = new AsyncFutureRef<Getter<FunctionDetailsValue>>(); FunctionValueImpl(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder nameBuilder) { super(valueData, valueLoader, nameBuilder); } @Override public JsArray asArray() { return null; } @Override public JsFunction asFunction() { return this; } @Override public Script getScript() throws MethodIsBlockingException { FunctionDetailsValue functionDetails = getFunctionDetails(); WipScriptManager scriptManager = getRemoteValueMapping().getTabImpl().getScriptManager(); return scriptManager.getScript(functionDetails.location().scriptId()); } @Override public TextStreamPosition getOpenParenPosition() throws MethodIsBlockingException { final LocationValue functionPosition = getFunctionDetails().location(); return new TextStreamPosition() { @Override public int getOffset() { return WipBrowserImpl.throwUnsupported(); } @Override public int getLine() { return (int) functionPosition.lineNumber(); } @Override public int getColumn() { Long columnObject = functionPosition.columnNumber(); if (columnObject == null) { return NO_POSITION; } return columnObject.intValue(); } }; } @Override public List<? extends JsScope> getScopes() { List<ScopeValue> data = getFunctionDetails().scopeChain(); if (data == null) { return Collections.emptyList(); } List<JsScope> result = new ArrayList<JsScope>(data.size()); for (ScopeValue scopeValue : data) { result.add(WipContextBuilder.createScope(scopeValue, getRemoteValueMapping())); } return result; } private FunctionDetailsValue getFunctionDetails() throws MethodIsBlockingException { if (!loadedPositionRef.isInitialized()) { getRemoteValueMapping().loadFunctionLocationInFuture(getValueData().objectId(), loadedPositionRef); } return loadedPositionRef.getSync().get(); } } } private interface FunctionScopeAccess { List<? extends JsScope> getScopes(); } static final FunctionScopeExtension FUNCTION_SCOPE_EXTENSION = new FunctionScopeExtension() { @Override public List<? extends JsScope> getScopes(JsFunction function) throws MethodIsBlockingException { FunctionScopeAccess functionScopeAccess = (FunctionScopeAccess) function; return functionScopeAccess.getScopes(); } }; private static abstract class VariableBase implements JsVariable { private final JsValue jsValue; private final ValueNameBuilder nameBuilder; private volatile String qualifiedName = null; VariableBase(JsValue jsValue, ValueNameBuilder nameBuilder) { this.jsValue = jsValue; this.nameBuilder = nameBuilder; } @Override public boolean isReadable() { return true; } @Override public JsValue getValue() { return jsValue; } @Override public String getName() { return nameBuilder.getShortName(); } @Override public boolean isMutable() { return false; } @Override public void setValue(String newValue, SetValueCallback callback) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public String getFullyQualifiedName() { String result = qualifiedName; if (result == null) { QualifiedNameBuilder qualifiedNameBuilder = nameBuilder.getQualifiedNameBuilder(); if (qualifiedNameBuilder == null) { return null; } StringBuilder builder = new StringBuilder(); qualifiedNameBuilder.append(builder); result = builder.toString(); qualifiedName = result; } return result; } } private static class VariableImpl extends VariableBase { VariableImpl(JsValue jsValue, ValueNameBuilder nameBuilder) { super(jsValue, nameBuilder); } @Override public JsObjectProperty asObjectProperty() { return null; } } private static abstract class ObjectPropertyBase extends VariableBase implements JsObjectProperty { ObjectPropertyBase(JsValue jsValue, ValueNameBuilder nameBuilder) { super(jsValue, nameBuilder); } @Override public JsObjectProperty asObjectProperty() { return this; } } private static class ObjectType extends ValueType { @Override JsValue build(RemoteObjectValue valueData, WipValueLoader valueLoader, QualifiedNameBuilder nameBuilder) { ValueType secondLevelValueType = getSafe(PROTOCOL_SUBTYPE_TO_VALUE_TYPE, valueData.subtype()); if (secondLevelValueType == null) { LOGGER.severe("Unexpected value type: " + valueData.type() + " " + valueData.subtype()); secondLevelValueType = DEFAULT_VALUE_TYPE; } return secondLevelValueType.build(valueData, valueLoader, nameBuilder); } private static final Map<RemoteObjectValue.Subtype, ValueType> PROTOCOL_SUBTYPE_TO_VALUE_TYPE; static { PROTOCOL_SUBTYPE_TO_VALUE_TYPE = new HashMap<RemoteObjectValue.Subtype, ValueType>(); ObjectSubtype objectSubtype = new ObjectSubtype(JsValue.Type.TYPE_OBJECT); // TODO: null? PROTOCOL_SUBTYPE_TO_VALUE_TYPE.put(null, objectSubtype); PROTOCOL_SUBTYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Subtype.NULL, new SingletonPrimitiveType(JsValue.Type.TYPE_NULL, "null")); PROTOCOL_SUBTYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Subtype.ARRAY, new ArrayType()); PROTOCOL_SUBTYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Subtype.REGEXP, objectSubtype); PROTOCOL_SUBTYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Subtype.DATE, objectSubtype); PROTOCOL_SUBTYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Subtype.NODE, objectSubtype); // Plus 1 for null - object. assert PROTOCOL_SUBTYPE_TO_VALUE_TYPE.size() == RemoteObjectValue.Subtype.values().length + 1; } } private static final Map<RemoteObjectValue.Type, ValueType> PROTOCOL_TYPE_TO_VALUE_TYPE; static { PROTOCOL_TYPE_TO_VALUE_TYPE = new HashMap<RemoteObjectValue.Type, ValueType>(); PROTOCOL_TYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Type.STRING, new PrimitiveTypeWithValue(JsValue.Type.TYPE_STRING)); PROTOCOL_TYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Type.BOOLEAN, new PrimitiveTypeWithValue(JsValue.Type.TYPE_BOOLEAN)); PROTOCOL_TYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Type.NUMBER, new PrimitiveTypeWithDescription(JsValue.Type.TYPE_NUMBER)); PROTOCOL_TYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Type.UNDEFINED, new SingletonPrimitiveType(JsValue.Type.TYPE_UNDEFINED, "undefined")); PROTOCOL_TYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Type.OBJECT, new ObjectType()); PROTOCOL_TYPE_TO_VALUE_TYPE.put(RemoteObjectValue.Type.FUNCTION, new FunctionType()); assert PROTOCOL_TYPE_TO_VALUE_TYPE.size() == RemoteObjectValue.Type.values().length; } private static final ValueType DEFAULT_VALUE_TYPE = new ObjectSubtype(JsValue.Type.TYPE_OBJECT); }