001/* 002 * Copyright (c) 2012-2017 Institut National des Sciences Appliquées de Lyon (INSA-Lyon) 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 */ 009 010package gololang; 011 012import java.lang.invoke.MethodHandle; 013import java.lang.invoke.MethodHandles; 014import java.lang.invoke.MethodType; 015import java.util.HashMap; 016import java.util.Map; 017import java.util.Set; 018import java.util.List; 019import java.util.LinkedList; 020import java.util.Objects; 021 022import static java.lang.System.arraycopy; 023import static java.lang.invoke.MethodType.genericMethodType; 024import static java.lang.invoke.MethodType.methodType; 025import static gololang.Predefined.isClosure; 026 027/** 028 * A dynamic object is an object whose properties can be dynamically added, changed and removed. Properties can be any 029 * object value or a function reference. 030 * <p> 031 * The methods <code>plug</code> and <code>propertyMissing</code> are left undocumented. They are being used 032 * by the Golo runtime to dispatch method invocations on dynamic objects. 033 */ 034public final class DynamicObject { 035 036 private final Object kind; 037 private final HashMap<String, Object> properties = new HashMap<>(); 038 private boolean frozen = false; 039 040 041 public DynamicObject() { 042 this("DynamicObject"); 043 } 044 045 public DynamicObject(Object kind) { 046 this.kind = kind; 047 } 048 049 public boolean hasKind(Object k) { 050 return Objects.equals(kind, k); 051 } 052 053 public boolean sameKind(DynamicObject other) { 054 return Objects.equals(kind, other.kind); 055 } 056 057 @Override 058 public String toString() { 059 List<String> props = new LinkedList<>(); 060 for (Map.Entry<String, Object> prop : properties.entrySet()) { 061 if (!isClosure(prop.getValue())) { 062 props.add(String.format("%s=%s", prop.getKey(), prop.getValue().toString())); 063 } 064 } 065 return String.format("%s{%s}", kind, String.join(", ", props)); 066 } 067 068 /** 069 * Defines a property. 070 * 071 * @param name the property name. 072 * @param value the property value. 073 * @return the same dynamic object. 074 * @throws IllegalStateException if the dynamic object is frozen. 075 */ 076 public DynamicObject define(String name, Object value) { 077 frozenMutationCheck(); 078 properties.put(name, value); 079 return this; 080 } 081 082 /** 083 * @return a view of all properties. 084 */ 085 public Set<Map.Entry<String, Object>> properties() { 086 return properties.entrySet(); 087 } 088 089 /** 090 * @param name the property name. 091 * @return the property value. 092 */ 093 public Object get(String name) { 094 return properties.get(name); 095 } 096 097 /** 098 * Removes a property. 099 * 100 * @param name the property name. 101 * @return the same dynamic object. 102 */ 103 public DynamicObject undefine(String name) { 104 properties.remove(name); 105 return this; 106 } 107 108 /** 109 * @return a new dynamic object whose properties point to the same objects. 110 */ 111 public DynamicObject copy() { 112 DynamicObject copy = new DynamicObject(this.kind); 113 for (Map.Entry<String, Object> entry : properties.entrySet()) { 114 copy.properties.put(entry.getKey(), entry.getValue()); 115 } 116 return copy; 117 } 118 119 /** 120 * Mixes all properties from another dynamic object into this one, overwriting existing properties. 121 * 122 * @param other the dynamic object to mix the properties from. 123 * @return the same dynamic object. 124 */ 125 public DynamicObject mixin(DynamicObject other) { 126 frozenMutationCheck(); 127 for (Map.Entry<String, Object> entry : other.properties.entrySet()) { 128 properties.put(entry.getKey(), entry.getValue()); 129 } 130 return this; 131 } 132 133 /** 134 * Freezes a dynamic object, meaning that its properties cannot be added, updated and removed anymore. 135 * 136 * @return the same dynamic object. 137 */ 138 public DynamicObject freeze() { 139 this.frozen = true; 140 return this; 141 } 142 143 /** 144 * Tells whether the dynamic object is frozen or not. 145 * 146 * @return {@code true} if frozen, {@code false} otherwise. 147 */ 148 public boolean isFrozen() { 149 return frozen; 150 } 151 152 /** 153 * Dispatch dynamic object "methods". The receiver dynamic object is expected to be the first element of {@code args}. 154 * 155 * @param property the method property in the dynamic object. 156 * @param args the arguments. 157 * @return the return value. 158 * @throws Throwable in case everything is wrong. 159 */ 160 public static Object dispatchCall(String property, Object... args) throws Throwable { 161 DynamicObject obj = (DynamicObject) args[0]; 162 Object value = obj.properties.get(property); 163 if (value != null) { 164 if (value instanceof FunctionReference) { 165 FunctionReference funRef = (FunctionReference) value; 166 if (funRef.isVarargsCollector() && args[args.length - 1] instanceof Object[]) { 167 return funRef.spread(args); 168 } 169 return funRef.invoke(args); 170 } else { 171 throw new UnsupportedOperationException("There is no dynamic object method defined for " + property); 172 } 173 } 174 if (obj.hasFallback()) { 175 FunctionReference handle = (FunctionReference) obj.properties.get("fallback"); 176 Object[] fallback_args = new Object[args.length + 1]; 177 fallback_args[0] = obj; 178 fallback_args[1] = property; 179 arraycopy(args, 1, fallback_args, 2, args.length - 1); 180 return handle.invoke(fallback_args); 181 } 182 throw new UnsupportedOperationException("There is neither a dynamic object method defined for " + property + " nor a 'fallback' method"); 183 } 184 185 /** 186 * Dispatches getter-style dynamic object methods, i.e., methods with a receiver and no argument. 187 * 188 * @param property the method property in the dynamic object. 189 * @param object the receiver object. 190 * @return the return value. 191 * @throws Throwable in case everything is wrong. 192 */ 193 public static Object dispatchGetterStyle(String property, DynamicObject object) throws Throwable { 194 Object value = object.get(property); 195 if (value != null || object.properties.containsKey(property)) { 196 if (value instanceof FunctionReference) { 197 FunctionReference funRef = (FunctionReference) value; 198 if (funRef.acceptArity(1)) { 199 return funRef.invoke(object); 200 } 201 } 202 return value; 203 } 204 if (object.hasFallback()) { 205 FunctionReference funRef = (FunctionReference) object.properties.get("fallback"); 206 return funRef.invoke(object, property); 207 } 208 return null; 209 } 210 211 /** 212 * Dispatches setter-style dynamic object methods, i.e., methods with a receiver and exactly 1 argument. 213 * 214 * @param property the method property in the dynamic object. 215 * @param object the receiver object. 216 * @param arg the arguments. 217 * @return the return value. 218 * @throws Throwable in case everything is wrong. 219 */ 220 public static Object dispatchSetterStyle(String property, DynamicObject object, Object arg) throws Throwable { 221 Object value = object.get(property); 222 if (value != null || object.properties.containsKey(property)) { 223 if (value instanceof FunctionReference) { 224 FunctionReference funRef = (FunctionReference) value; 225 if (funRef.arity() == 2) { 226 if (funRef.isVarargsCollector() && arg instanceof Object[]) { 227 return funRef.handle().invokeExact((Object) object, (Object[]) arg); 228 } 229 return funRef.invoke(object, arg); 230 } 231 } 232 } 233 // XXX: should we try the fallback method here ? 234 return object.define(property, arg); 235 } 236 237 /** 238 * Dispatches on another dynamic object (fallback helper). 239 * 240 * @param deleguee the object to delegate to. 241 * @param receiver the receiver object. 242 * @param property the method property in the dynamic object. 243 * @param args the arguments. 244 * @return the return value. 245 * @throws Throwable in case everything is wrong. 246 */ 247 public static Object dispatchDelegate(DynamicObject deleguee, DynamicObject receiver, String property, Object... args) throws Throwable { 248 return deleguee 249 .invoker(property, genericMethodType(args.length + 1)) 250 .bindTo(deleguee) 251 .invokeWithArguments(args); 252 } 253 254 /** 255 * Creates a function suitable for the {@code fallback} property delegating to the given dynamic object. 256 * 257 * Example: 258 * <pre class="listing"><code class="lang-golo" data-lang="golo"> 259 * let d = DynamicObject(): name("Zaphod") 260 * let o = DynamicObject(): fallback(delegate(d)) 261 * </code></pre> 262 * 263 * @param deleguee the object to delegate to. 264 * @return a function delegating to {@code deleguee} 265 */ 266 public static FunctionReference delegate(DynamicObject deleguee) { 267 return new FunctionReference( 268 DISPATCH_DELEGATE.bindTo(deleguee).asVarargsCollector(Object[].class), //.asType(genericMethodType(2, true)), 269 new String[]{"this", "name", "args"}); 270 } 271 272 /** 273 * Gives an invoker method handle for a given property. 274 * <p> 275 * While this method may be useful in itself, it is mostly relevant for the Golo runtime internals so as 276 * to allow calling "methods" on dynamic objects, as in: 277 * <pre class="listing"><code class="lang-golo" data-lang="golo"> 278 * # obj is some dynamic object... 279 * obj: foo("bar") 280 * println(foo: bar()) 281 * 282 * obj: define("plop", |this| -> "Plop!") 283 * println(obj: plop()) 284 * </code></pre> 285 * 286 * @param property the name of a property. 287 * @param type the expected invoker type with at least one parameter (the dynamic object as a receiver). 288 * @return a method handle. 289 */ 290 public MethodHandle invoker(String property, MethodType type) { 291 switch (type.parameterCount()) { 292 case 0: 293 throw new IllegalArgumentException("A dynamic object invoker type needs at least 1 argument (the receiver)"); 294 case 1: 295 return DISPATCH_GET.bindTo(property).asType(genericMethodType(1)); 296 case 2: 297 return DISPATCH_SET.bindTo(property).asType(genericMethodType(2)); 298 default: 299 return DISPATCH_CALL.bindTo(property).asCollector(Object[].class, type.parameterCount()); 300 } 301 } 302 303 /** 304 * Verify if a method is defined for the dynamic object. 305 * 306 * @param method the method name. 307 * @return {@code true} if method is defined, {@code false} otherwise. 308 */ 309 public boolean hasMethod(String method) { 310 Object obj = properties.get(method); 311 if (obj != null) { 312 return isClosure(obj); 313 } 314 return false; 315 } 316 317 /** 318 * Let the user define a fallback behavior. 319 * 320 * @param value the fallback value 321 * @return the current object 322 */ 323 public DynamicObject fallback(Object value) { 324 return define("fallback", value); 325 } 326 327 /** 328 * Verify a fallback property exists. 329 * 330 * @return {@code true} if a fallback behavior is defined, {@code false} otherwise. 331 */ 332 private boolean hasFallback() { 333 return properties.containsKey("fallback"); 334 } 335 336 public static final MethodHandle DISPATCH_CALL; 337 public static final MethodHandle DISPATCH_GET; 338 public static final MethodHandle DISPATCH_SET; 339 public static final MethodHandle DISPATCH_DELEGATE; 340 341 static { 342 MethodHandles.Lookup lookup = MethodHandles.lookup(); 343 try { 344 DISPATCH_DELEGATE = lookup.findStatic(DynamicObject.class, "dispatchDelegate", 345 methodType(Object.class, DynamicObject.class, DynamicObject.class, String.class, Object[].class)); 346 DISPATCH_CALL = lookup.findStatic(DynamicObject.class, "dispatchCall", methodType(Object.class, String.class, Object[].class)); 347 DISPATCH_GET = lookup.findStatic(DynamicObject.class, "dispatchGetterStyle", methodType(Object.class, String.class, DynamicObject.class)); 348 DISPATCH_SET = lookup.findStatic(DynamicObject.class, "dispatchSetterStyle", methodType(Object.class, String.class, DynamicObject.class, Object.class)); 349 } catch (NoSuchMethodException | IllegalAccessException e) { 350 e.printStackTrace(); 351 throw new Error("Could not bootstrap the required method handles"); 352 } 353 } 354 355 private void frozenMutationCheck() { 356 if (frozen) { 357 throw new IllegalStateException("the object is frozen"); 358 } 359 } 360}