// Copyright (c) 2009 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 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.concurrent.atomic.AtomicReference; import org.chromium.sdk.JsFunction; import org.chromium.sdk.JsObject; import org.chromium.sdk.JsVariable; import org.chromium.sdk.internal.v8native.InternalContext; import org.chromium.sdk.util.AsyncFuture; import org.chromium.sdk.util.MethodIsBlockingException; /** * A generic implementation of the JsObject interface. * @param <D> type of convenience user-provided property-data-holding structure, that always * includes {@link BasicPropertyData} but may also contain other fields that expire when * properties expire */ public abstract class JsObjectBase<D> extends JsValueBase implements JsObject { private final long ref; private final String className; private final ValueLoader valueLoader; /** * Fully qualified name of variable holding this object. */ private final String variableFqn; /** * Property data in form of {@link AsyncFuture} for property load operation * that several threads may access simultaneously. The future gets reinitialized * on the next access after cache state was updated. */ private final AtomicReference<AsyncFuture<D>> propertyDataRef = new AtomicReference<AsyncFuture<D>>(null); /** * This constructor implies the lazy resolution of object properties. * * @param valueLoader where this instance belongs in * @param variableFqn the fully qualified name of the variable holding this object * @param valueState the value data from the JS VM */ JsObjectBase(ValueLoader valueLoader, String variableFqn, ValueMirror mirror) { super(mirror); this.valueLoader = valueLoader; this.ref = mirror.getRef(); this.className = mirror.getClassName(); this.variableFqn = variableFqn; } @Override public Collection<JsVariableImpl> getProperties() throws MethodIsBlockingException { return getBasicPropertyData(true).getPropertyList(); } @Override public Collection<JsVariableImpl> getInternalProperties() throws MethodIsBlockingException { return getBasicPropertyData(true).getIntenalPropertyList(); } @Override public String getRefId() { if (ref < 0) { // Negative handle means that it's transient. We don't expose it. return null; } else { return String.valueOf(ref); } } @Override public ValueLoader getRemoteValueMapping() { return valueLoader; } public static int parseRefId(String value) { return Integer.parseInt(value); } @Override public JsObjectBase<D> asObject() { return this; } @Override public JsVariable getProperty(String name) throws MethodIsBlockingException { return getBasicPropertyData(true).getPropertyMap().get(name); } @Override public String getClassName() { return className; } @Override public String getValueString() { switch (getType()) { case TYPE_OBJECT: case TYPE_ARRAY: return "[" + getClassName() + "]"; case TYPE_FUNCTION: return "[Function]"; default: return ""; } } protected InternalContext getInternalContext() { return valueLoader.getInternalContext(); } protected long getRef() { return ref; } /** * Gets or creates property data. The data have cache timestamp inside and gets recreated * if checkFreshness is true and if global timestamp has been updated. * @param checkFreshness whether data freshness is to be check against global cache timestamp * @return property data wrapped in convenience class as specified by * {@link #wrapBasicData(BasicPropertyData)} method */ protected D getPropertyData(boolean checkFreshness) throws MethodIsBlockingException { if (propertyDataRef.get() == null) { int currentCacheState = getRemoteValueMapping().getCurrentCacheState(); startPropertyLoadOperation(false, currentCacheState); } else { if (checkFreshness) { int currentCacheState = getRemoteValueMapping().getCurrentCacheState(); D result = propertyDataRef.get().getSync(); BasicPropertyData basicPropertyData = unwrapBasicData(result); if (basicPropertyData.getCacheState() == currentCacheState) { return result; } startPropertyLoadOperation(true, currentCacheState); } } return propertyDataRef.get().getSync(); } /** * Convenience method that gets property data and returns wrapped {@link BasicPropertyData}. */ protected BasicPropertyData getBasicPropertyData(boolean checkFreshness) throws MethodIsBlockingException { D propertyData = getPropertyData(checkFreshness); return unwrapBasicData(propertyData); } private void startPropertyLoadOperation(boolean reload, final int currentCacheState) throws MethodIsBlockingException { // The operation is blocking, because we will wait for its result anyway. // On the other hand there is a post-load job that we need a thread to occupy with. AsyncFuture.SyncOperation<D> blockingOperation = new AsyncFuture.SyncOperation<D>() { @Override protected D runSync() throws MethodIsBlockingException { SubpropertiesMirror subpropertiesMirror = getRemoteValueMapping().getOrLoadSubproperties(ref); List<JsVariableImpl> properties = wrapProperties(subpropertiesMirror.getProperties()); List<JsVariableImpl> internalProperties = wrapProperties(subpropertiesMirror.getInternalProperties()); BasicPropertyData data = new BasicPropertyData(currentCacheState, properties, internalProperties, subpropertiesMirror); return wrapBasicData(data); } private List<JsVariableImpl> wrapProperties(List<? extends PropertyReference> propertyRefs) throws MethodIsBlockingException { List<ValueMirror> subMirrors = valueLoader.getOrLoadValueFromRefs(propertyRefs); List<JsVariableImpl> wrappedProperties = createPropertiesFromMirror(subMirrors, propertyRefs); return Collections.unmodifiableList(wrappedProperties); } }; if (reload) { AsyncFuture.reinitializeReference(propertyDataRef, blockingOperation.asAsyncOperation()); } else { AsyncFuture.initializeReference(propertyDataRef, blockingOperation.asAsyncOperation()); } blockingOperation.execute(); } /** * User-provided method that wraps basic property data in the class of user choice D. * User wrapper will be kept by {@link JsObjectBase} and easily accessible when needed, * plus it will be dropped when caches become reset. * <p> * Alternative design would be to require D to extend BasicPropertyData, but we would have * to expose all of its constructor parameters in this case. */ protected abstract D wrapBasicData(BasicPropertyData basicPropertyData); /** * User-provided method that extracts basic property data from user-provided data class. */ protected abstract BasicPropertyData unwrapBasicData(D wrappedBasicData); /** * Contains immutable data about object properties plus lazy-initialized fields that are * derived from property data. There can be more fields in user-provided wrapper class. */ protected static class BasicPropertyData { private final int cacheState; private final List<JsVariableImpl> propertyList; private final List<JsVariableImpl> intenalPropertyList; private final SubpropertiesMirror subpropertiesMirror; private volatile Map<String, JsVariableImpl> propertyMap = null; BasicPropertyData(int cacheState, List<JsVariableImpl> propertyList, List<JsVariableImpl> intenalPropertyList, SubpropertiesMirror subpropertiesMirror) { this.cacheState = cacheState; this.propertyList = propertyList; this.intenalPropertyList = intenalPropertyList; this.subpropertiesMirror = subpropertiesMirror; } int getCacheState() { return cacheState; } List<JsVariableImpl> getPropertyList() { return propertyList; } List<JsVariableImpl> getIntenalPropertyList() { return intenalPropertyList; } SubpropertiesMirror getSubpropertiesMirror() { return subpropertiesMirror; } Map<String, JsVariableImpl> getPropertyMap() { // Method is not synchronized -- it's OK if we initialize volatile propertyMap field // several times. if (propertyMap == null) { Map<String, JsVariableImpl> map = new HashMap<String, JsVariableImpl>(propertyList.size() * 2, 0.75f); for (JsVariableImpl prop : propertyList) { map.put(prop.getName(), prop); } // Make make synchronized for such not thread-safe methods as entrySet. propertyMap = Collections.unmodifiableMap(Collections.synchronizedMap(map)); } return propertyMap; } } private List<JsVariableImpl> createPropertiesFromMirror(List<ValueMirror> mirrorProperties, List<? extends PropertyReference> propertyRefs) { // TODO(peter.rybin) Maybe assert that context is valid here List<JsVariableImpl> result = new ArrayList<JsVariableImpl>(mirrorProperties.size()); for (int i = 0; i < mirrorProperties.size(); i++) { ValueMirror mirror = mirrorProperties.get(i); Object varName = propertyRefs.get(i).getName(); String fqn = getFullyQualifiedName(varName); if (fqn == null) { continue; } String decoratedName = JsVariableImpl.NameDecorator.decorateVarName(varName); result.add(new JsVariableImpl(valueLoader, mirror, varName, decoratedName, fqn)); } return result; } private String getFullyQualifiedName(Object propName) { if (variableFqn == null) { return null; } if (propName instanceof String) { String propNameStr = (String) propName; if (propNameStr.startsWith(".")) { // ".arguments" is not legal return null; } } return variableFqn + JsVariableImpl.NameDecorator.buildAccessSuffix(propName); } public static class Impl extends JsObjectBase<BasicPropertyData> { Impl(ValueLoader valueLoader, String variableFqn, ValueMirror valueState) { super(valueLoader, variableFqn, valueState); } @Override public JsArrayImpl asArray() { return null; } @Override public JsFunction asFunction() { return null; } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("[JsObject: type=").append(getType()); try { for (JsVariable prop : getProperties()) { result.append(',').append(prop); } } catch (MethodIsBlockingException e) { return "[JsObject: Exception in retrieving data]"; } result.append(']'); return result.toString(); } @Override protected BasicPropertyData wrapBasicData(BasicPropertyData basicPropertyData) { return basicPropertyData; } @Override protected BasicPropertyData unwrapBasicData(BasicPropertyData additionalPropertyStore) { return additionalPropertyStore; } } }