ScriptingObjectMagic.java

/***************************************************************************
   Copyright 2012 Emily Estes

   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 net.metanotion.scripting;


import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import net.metanotion.util.Dictionary;
import net.metanotion.util.Dispatcher;
import net.metanotion.util.DispatcherGenerator;
import net.metanotion.util.IdentityDictionary;
import net.metanotion.util.reflect.ExtensionWalker;
import net.metanotion.util.reflect.GetFieldList;

/** This class generates an {@link net.metanotion.scripting.ObjectServer} instance for a given class via reflection
by examining instance variables anontated as {@literal @}{@link net.metanotion.scripting.Scriptable}. It also models
a tree of scriptable objects by following any variables marked as {@literal @}{@link net.metanotion.util.Extends}.
	@param <I> The type of the object we're generating an object server instance for.
*/
public final class ScriptingObjectMagic<I> {
	private static final class InvokeZero implements Dictionary {
		private static final Object[] args = new Object[0];
		private final Dictionary fields;
		private final Method method;
		public InvokeZero(final Dictionary f, final Method m) {
			this.fields = f;
			this.method = m;
		}
		@Override public Object get(final Object instance) {
			try {
				return method.invoke(fields.get(instance), args);
			} catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); }
		}
	}

	private static final String getName(final Scriptable a, final String blank) {
		final String name = a.value();
		if("".equals(name)) { return blank; }
		if("''".equals(name)) { return ""; }
		return name;
	}

	private final class Visitor implements ExtensionWalker.ClassVisitor {
		@Override public void visit(final Field[] stack, final Class current) {
			final Scriptable a = (Scriptable) current.getAnnotation(Scriptable.class);
			if(a != null) {
				map.put(getName(a, current.getSimpleName()),
					stack.length == 0 ? IdentityDictionary.INSTANCE : new GetFieldList(stack));
			}

			for(final Field f: current.getFields()) {
				f.setAccessible(true);
				final Scriptable fs = f.getAnnotation(Scriptable.class);
				if(fs!=null) {
					final Field[] f1 = Arrays.copyOf(stack, stack.length + 1);
					f1[stack.length] = f;
					map.put(getName(fs, f.getName()), new GetFieldList(f1));
				}
			}

			for(final Method m: current.getMethods()) {
				final Scriptable s = m.getAnnotation(Scriptable.class);
				if((s != null) && (m.getParameterTypes().length == 0)) {
					map.put(getName(s, m.getName()), new InvokeZero(new GetFieldList(stack), m));
				}
			}
		}
	}

	private final Map<String,Dictionary> map = new HashMap<>();

	/** Create a new object server generator for the class specified.
		@param klazz The class to use to create an object server instance generator.
	*/
	public ScriptingObjectMagic(final Class<I> klazz) {
		ExtensionWalker.visit(new Visitor(), klazz);
	}

	/** Return an ObjectServer implementation backed by an object instance that will provide the scriptable objects.
		@param object The instance providing the scriptable objects.
		@return An instance of ObjectServer backed by the object provided.
	*/
	public ObjectServer instance(final I object) { return new OS(object, this.map); }

	private static final DispatcherGenerator dGen = new DispatcherGenerator();

	/** This is the internal implementation of ObjectServer this class uses when the {@link #instance} method is called. */
	private static final class OS implements ObjectServer {
		private final Object obj;
		private final Map<String,Dictionary> getters;
		public OS(final Object o, final Map<String,Dictionary> getters) {
			this.obj = o;
			this.getters = getters;
		}

		@Override public Dispatcher dispatcher(final String name) { return dGen.get(this.get(name)); }
		@Override public Object get(final String name) {
			final Dictionary o = getters.get(name);
			if(o==null) { return null; }
			return o.get(obj);
		}
	}
}