/**
* Copyright 2013 Douglas Campos, and individual contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dynjs.runtime;
import java.util.*;
import org.dynjs.exception.ThrowException;
public class DynObject implements JSObject, Map<String, Object> {
private String className;
private JSObject prototype = null;
private final Map<String, PropertyDescriptor> properties = new LinkedHashMap<>();
private boolean extensible = true;
private ExternalIndexedData externalIndexedData;
// Used by globalObject constructor and for ShadowObjectLinker
public DynObject() {
setClassName("Object");
setExtensible(true);
}
// Copy constructor
public DynObject(DynObject parent) {
this.className = parent.className;
this.prototype = parent.prototype;
this.properties.putAll( parent.properties );
this.extensible = parent.extensible;
this.externalIndexedData = parent.externalIndexedData;
}
public DynObject(GlobalContext globalContext) {
this();
setPrototype(globalContext.getObjectPrototype());
}
// ------------------------------------------------------------------------
// JSObject
// ------------------------------------------------------------------------
@Override
public JSObject getPrototype() {
return this.prototype;
}
public void setPrototype(final JSObject prototype) {
this.prototype = prototype;
}
@Override
public String getClassName() {
return this.className;
}
public void setClassName(String className) {
this.className = className;
}
@Override
public boolean isExtensible() {
return extensible;
}
public void setExtensible(boolean extensible) {
this.extensible = extensible;
}
@Override
public Object get(ExecutionContext context, String name) {
if ( this.externalIndexedData != null ) {
Long num = Types.toUint32(context, name);
if ( name.equals( num.toString() ) ) {
Object value = this.externalIndexedData.get(num);
if ( value == null ) {
return Types.UNDEFINED;
}
return value;
}
}
// 8.12.3
Object d = getProperty(context, name, false);
if (d == Types.UNDEFINED) {
return Types.UNDEFINED;
}
PropertyDescriptor desc = (PropertyDescriptor) d;
if (desc.isDataDescriptor()) {
Object value = desc.getValue();
if (value == null) {
value = Types.UNDEFINED;
}
return value;
}
Object g = desc.getGetter();
if (g == null || g == Types.UNDEFINED) {
return Types.UNDEFINED;
}
JSFunction getter = (JSFunction) g;
return context.call(getter, this);
}
@Override
public Object getOwnProperty(ExecutionContext context, String name) {
return getOwnProperty(context, name, true);
}
@Override
public Object getOwnProperty(ExecutionContext context, String name, boolean dupe) {
// 8.12.1 (step 1)
// Returns PropertyDescriptor or UNDEFINED
PropertyDescriptor x = this.properties.get(name);
// System.err.println("x1: " + name + " > " + x);
if (x == null) {
return Types.UNDEFINED;
}
if (!dupe) {
return x;
}
return x.duplicate();
}
@Override
public Object getProperty(ExecutionContext context, String name) {
return getProperty(context, name, true);
}
@Override
public Object getProperty(ExecutionContext context, String name, boolean dupe) {
// 8.12.2
// Returns PropertyDescriptor or UNDEFINED
Object d = getOwnProperty(context, name, dupe);
if (d != Types.UNDEFINED) {
return d;
}
if (this.prototype == null) {
return Types.UNDEFINED;
}
return this.prototype.getProperty(context, name, dupe);
}
@Override
public boolean hasProperty(ExecutionContext context, String name) {
// 8.12.6
return (getProperty(context, name, false) != Types.UNDEFINED);
}
@Override
public void put(ExecutionContext context, final String name, final Object value, final boolean shouldThrow) {
if ( this.externalIndexedData != null ) {
Long num = Types.toUint32(context, name);
if ( name.equals( num.toString() ) ) {
Object externValue = value;
if ( value == Types.UNDEFINED || value == Types.NULL ) {
externValue = null;
}
this.externalIndexedData.put( num, externValue );
return;
}
}
// 8.12.5
// System.err.println("PUT " + name + " > " + value);
if (!canPut(context, name)) {
// System.err.println("CANNOT PUT");
if (shouldThrow) {
throw new ThrowException(context, context.createTypeError("cannot put property '" + name + "'"));
}
return;
}
Object ownDesc = getOwnProperty(context, name, false);
if ((ownDesc != Types.UNDEFINED) && ((PropertyDescriptor) ownDesc).isDataDescriptor()) {
// System.err.println("setting value on non-UNDEF");
PropertyDescriptor newDesc = new PropertyDescriptor();
newDesc.setValue(value);
defineOwnProperty(context, name, newDesc, shouldThrow);
return;
}
Object desc = getProperty(context, name, false);
if ((desc != Types.UNDEFINED) && ((PropertyDescriptor) desc).isAccessorDescriptor()) {
JSFunction setter = (JSFunction) ((PropertyDescriptor) desc).getSetter();
context.call(setter, this, value);
} else {
defineOwnProperty(context, name,
PropertyDescriptor.newDataPropertyDescriptor(value, true, true, true), shouldThrow);
}
}
@Override
public boolean canPut(ExecutionContext context, String name) {
// 8.12.4
Object d = getOwnProperty(context, name, false);
// Find the property on ourself, or our prototype
if (d == Types.UNDEFINED) {
if (this.prototype != null) {
d = this.prototype.getProperty(context, name, false);
}
}
// System.err.println("canPut?: " + name + " > " + d);
// If either has it, deal with descriptor appropriately
if (d != Types.UNDEFINED) {
PropertyDescriptor desc = (PropertyDescriptor) d;
if (desc.isAccessorDescriptor()) {
if (desc.getSetter() == Types.UNDEFINED) {
// System.err.println("NO SET");
return false;
}
return true;
} else {
// System.err.println("writable? " + writable);
if (!desc.hasWritable()) {
return true;
}
return desc.isWritable();
}
}
// Else, just determine if we are extensible directly
return isExtensible();
}
@Override
public boolean delete(ExecutionContext context, String name, boolean shouldThrow) {
// 8.12.7
if (!this.properties.containsKey(name)) {
return true;
}
Object d = getOwnProperty(context, name, false);
if (d == Types.UNDEFINED) {
return true;
}
PropertyDescriptor desc = (PropertyDescriptor) d;
if (desc.isConfigurable()) {
this.properties.remove(name);
return true;
}
if (shouldThrow) {
throw new ThrowException(context, context.createTypeError("cannot delete unconfigurable property '" + name + "'"));
}
return false;
}
@Override
public Object defaultValue(ExecutionContext context, String hint) {
// 8.12.8
if (hint == null) {
hint = "Number";
}
if (hint.equals("String")) {
Object toString = get(context, "toString");
if (toString instanceof JSFunction) {
Object result = context.call((JSFunction) toString, this);
if (result instanceof String || result instanceof Number || result instanceof Boolean || result == Types.UNDEFINED || result == Types.NULL) {
return result;
}
}
Object valueOf = get(context, "valueOf");
if (valueOf instanceof JSFunction) {
Object result = context.call((JSFunction) valueOf, this);
if (result instanceof String || result instanceof Number || result instanceof Boolean || result == Types.UNDEFINED || result == Types.NULL) {
return result;
}
}
throw new ThrowException(context, context.createTypeError("String coercion must return a primitive value"));
} else if (hint.equals("Number")) {
Object valueOf = get(context, "valueOf");
if (valueOf instanceof JSFunction) {
Object result = context.call((JSFunction) valueOf, this);
if (result instanceof String || result instanceof Number || result instanceof Boolean || result == Types.UNDEFINED || result == Types.NULL) {
return result;
}
}
Object toString = get(context, "toString");
if (toString instanceof JSFunction) {
Object result = context.call((JSFunction) toString, this);
if (result instanceof String || result instanceof Number || result instanceof Boolean || result == Types.UNDEFINED || result == Types.NULL) {
return result;
}
}
throw new ThrowException(context, context.createTypeError("String coercion must return a primitive value"));
}
return null;
}
@Override
public boolean defineOwnProperty(ExecutionContext context, String name, PropertyDescriptor desc, boolean shouldThrow) {
// 8.12.9
Object c = getOwnProperty(context, name, false);
if (c == Types.UNDEFINED) {
if (!isExtensible()) {
return reject(context, shouldThrow);
} else {
// System.err.println("DEF.initial: " + name + " > " + newDesc);
this.properties.put(name, desc.duplicateWithDefaults());
return true;
}
}
if (desc.isEmpty()) {
return true;
}
PropertyDescriptor current = (PropertyDescriptor) c;
if (current.hasConfigurable() && !current.isConfigurable()) {
if (desc.hasConfigurable() && desc.isConfigurable()) {
return reject(context, shouldThrow);
}
if (current.hasEnumerable() && desc.hasEnumerable() && current.isEnumerable() != desc.isEnumerable()) {
return reject(context, shouldThrow);
}
}
PropertyDescriptor newDesc = null;
if (desc.isGenericDescriptor()) {
newDesc = new PropertyDescriptor();
newDesc.copyAll(current);
// System.err.println("DEF.generic: " + name + " > " + newDesc);
} else if (current.isDataDescriptor() != desc.isDataDescriptor()) {
if (!current.isConfigurable()) {
return reject(context, shouldThrow);
}
if (current.isDataDescriptor()) {
// System.err.println("accessor");
newDesc = PropertyDescriptor.newAccessorPropertyDescriptor();
} else {
// System.err.println("data");
newDesc = PropertyDescriptor.newDataPropertyDescriptor();
}
// System.err.println("DEF.mismatch: " + name + " > " + newDesc);
} else if (current.isDataDescriptor() && desc.isDataDescriptor()) {
if (!current.isConfigurable()) {
if (current.hasWritable() && !current.isWritable()) {
if (desc.isWritable()) {
return reject(context, shouldThrow);
}
Object newValue = desc.getValue();
if (newValue != null && !Types.sameValue(current.getValue(), newValue)) {
return reject(context, shouldThrow);
}
}
}
newDesc = PropertyDescriptor.newDataPropertyDescriptor();
if (current.hasValue()) {
newDesc.setValue(current.getValue());
}
if (current.hasWritable()) {
newDesc.setWritable(current.isWritable());
}
// System.err.println("DEF.data: " + name + " > " + newDesc);
} else if (current.isAccessorDescriptor() && desc.isAccessorDescriptor()) {
if (!current.isConfigurable()) {
Object newSetter = desc.getSetter();
if (newSetter != null && !Types.sameValue(current.getSetter(), newSetter)) {
return reject(context, shouldThrow);
}
Object newGetter = desc.getGetter();
if (newGetter != null && !Types.sameValue(current.getGetter(), newGetter)) {
return reject(context, shouldThrow);
}
}
newDesc = PropertyDescriptor.newAccessorPropertyDescriptor();
if (current.hasSet()) {
newDesc.setSetter(current.getSetter());
}
if (current.hasGet()) {
newDesc.setGetter(current.getGetter());
}
// System.err.println("DEF.accessor: " + name + " > " + newDesc);
}
if (current.hasConfigurable()) {
newDesc.setConfigurable(current.isConfigurable());
}
if (current.hasEnumerable()) {
newDesc.setEnumerable(current.isEnumerable());
}
newDesc.copyAll(desc);
this.properties.put(name, newDesc);
return true;
/*
* if (current.hasConfigurable()) {
* newDesc.set(Names.CONFIGURABLE, current.get(Names.CONFIGURABLE));
* }
* if (current.hasEnumerable()) {
* newDesc.set(Names.ENUMERABLE, current.get(Names.ENUMERABLE));
* }
*
* if (current.hasWritable()) {
* newDesc.set(Names.WRITABLE, current.get(Names.WRITABLE));
* }
*
* newDesc.copyAll(current);
* newDesc.copyAll(desc);
* System.err.println( this + " def: " + name + " > " + newDesc );
* this.properties.put(name, newDesc);
* return true;
* }
*/
}
public String toString() {
return "[object Object]";
}
protected boolean reject(ExecutionContext context, boolean shouldThrow) {
if (shouldThrow) {
throw new ThrowException(context, context.createTypeError("unable to perform operation"));
}
return false;
}
@Override
public NameEnumerator getOwnPropertyNames() {
ArrayList<String> names = new ArrayList<String>();
for (String name : this.properties.keySet()) {
names.add(name);
}
return new NameEnumerator(names);
}
@Override
public NameEnumerator getOwnEnumerablePropertyNames() {
ArrayList<String> names = new ArrayList<String>();
for (String name : this.properties.keySet()) {
PropertyDescriptor desc = this.properties.get(name);
if (desc.isEnumerable()) {
names.add(name);
}
}
return new NameEnumerator(names);
}
@Override
public NameEnumerator getAllEnumerablePropertyNames() {
ArrayList<String> names = new ArrayList<String>();
if (this.prototype != null) {
names.addAll(this.prototype.getAllEnumerablePropertyNames().toList());
}
for (String name : this.properties.keySet()) {
PropertyDescriptor desc = this.properties.get(name);
if (desc.isEnumerable()) {
names.add(name);
} else {
names.remove(name);
}
}
return new NameEnumerator(names);
}
@Override
public void defineNonEnumerableProperty(final GlobalContext globalContext, String name, final Object value) {
this.defineOwnProperty(null, name,
PropertyDescriptor.newDataPropertyDescriptor(value, true, true, false), false);
}
@Override
public void defineReadOnlyProperty(final GlobalContext globalContext, String name, final Object value) {
this.defineOwnProperty(null, name,
PropertyDescriptor.newDataPropertyDescriptor(value, false, false, false), false);
}
@Override
public void setExternalIndexedData(ExternalIndexedData data) {
this.externalIndexedData = data;
}
@Override
public ExternalIndexedData getExternalIndexedData() {
return externalIndexedData;
}
@Override
public boolean hasExternalIndexedData() {
return ( this.externalIndexedData != null );
}
// java.util.Map
@Override
public int size() {
return getAllEnumerablePropertyNames().size();
}
@Override
public boolean isEmpty() {
return getAllEnumerablePropertyNames().isEmpty();
}
@Override
public boolean containsKey(Object key) {
Object desc = getProperty(null, key.toString(), false);
return (desc != Types.UNDEFINED);
}
@Override
public boolean containsValue(Object value) {
// TODO Auto-generated method stub
return false;
}
@Override
public Object get(Object key) {
Object result = get(null, key.toString());
if ( result == Types.UNDEFINED ) {
return null;
}
return result;
}
@Override
public Object put(String key, Object value) {
Object oldValue = get(null, key.toString());
put(null, key, value, false);
return oldValue;
}
@Override
public Object remove(Object key) {
Object oldValue = get(null, key.toString());
delete(null, key.toString(), false);
return oldValue;
}
@Override
public void putAll(Map<? extends String, ? extends Object> m) {
// TODO Auto-generated method stub
}
@Override
public void clear() {
// TODO Auto-generated method stub
}
@Override
public Set<String> keySet() {
return new KeySet();
}
@Override
public Collection<Object> values() {
// TODO Auto-generated method stub
return null;
}
@Override
public Set<java.util.Map.Entry<String, Object>> entrySet() {
// TODO Auto-generated method stub
return null;
}
protected class KeySet implements Set<String> {
@Override
public int size() {
return getAllEnumerablePropertyNames().size();
}
@Override
public boolean isEmpty() {
return getAllEnumerablePropertyNames().isEmpty();
}
@Override
public boolean contains(Object o) {
return hasProperty(null, o.toString());
}
@Override
public Iterator<String> iterator() {
return getAllEnumerablePropertyNames().toList().iterator();
}
@Override
public Object[] toArray() {
return getAllEnumerablePropertyNames().toList().toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return getAllEnumerablePropertyNames().toList().toArray(a);
}
@Override
public boolean add(String e) {
if (! hasProperty(null, e)) {
put(e, Types.UNDEFINED);
return true;
}
return false;
}
@Override
public boolean remove(Object o) {
if ( hasProperty(null, o.toString())) {
delete( null, o.toString(), false );
return true;
}
return false;
}
@Override
public boolean containsAll(Collection<?> c) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean addAll(Collection<? extends String> c) {
return false;
}
@Override
public boolean retainAll(Collection<?> c) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean removeAll(Collection<?> c) {
// TODO Auto-generated method stub
return false;
}
@Override
public void clear() {
// TODO Auto-generated method stub
}
public String toString() {
return getAllEnumerablePropertyNames().toList().toString();
}
}
}