2010-07-23

Rhino ClassShutter replacement: SandboxShutter

Because the ClassShutter in Rhino doesn't usually provide enough information to make decisions on whether or not certain Java objects, fields or methods should be accessible, I wrote the SandboxShutter which will enable you to control accessibility for each field and each method of each Java object. The performance impact is negligible, because JavaScript itself is an order of magnitude slower than the injected checks.


The SandboxShutter interface:
public interface SandboxShutter
{
   public boolean allowClassAccess(Class<?> type);

   public boolean allowFieldAccess(Class<?> type, Object instance, String fieldName);

   public boolean allowMethodAccess(Class<?> type, Object instance, String methodName);

   public boolean allowStaticFieldAccess(Class<?> type, String fieldName);

   public boolean allowStaticMethodAccess(Class<?> type, String methodName);
}


When the following javascript code is executed:
importPackage(Packages.my.game); // assuming the Java class my.game.Player exists

var player = new Player("Jake");
player.gender = "female"; // this is a Java method!
player.setGender("male"); // this the same Java method.
player.age = 18; // this is a Java field
player.age += 3;

var player = new Player("Jane");
player.gender = "male";
player.gender = "female";
player.age = 19;
player.age += 2;

var count = Player.PLAYER_COUNT;
Player.PLAYER_COUNT += 2;


The following SandboxShutter calls will be made:
allowClassAccess:       my.game.Player

allowMethodAccess:      my.game.Player.setGender() instance=Player@2346
allowFieldAccess:       my.game.Player.age         instance=Player@2346

allowMethodAccess:      my.game.Player.setGender() instance=Player@54326
allowFieldAccess:       my.game.Player.age         instance=Player@54326

allowStaticFieldAccess: my.game.Player.PLAYER_COUNT
As shown, there will be at most one call for each field/method in each object, and one call per class. This allows you to control accessibility per Java object.


Create the SandboxContextFactory:
   public static class SandboxContextFactory extends ContextFactory
   {
      final SandboxShutter shutter;

      public SandboxContextFactory(SandboxShutter shutter)
      {
         this.shutter = shutter;
      }

      @Override
      protected Context makeContext()
      {
         Context cx = super.makeContext();
         cx.setWrapFactory(new SandboxWrapFactory());
         cx.setClassShutter(new ClassShutter()
         {
            private final Map<String, Boolean> nameToAccepted = new HashMap<String, Boolean>();

            @Override
            public boolean visibleToScripts(String name)
            {
               Boolean granted = this.nameToAccepted.get(name);

               if (granted != null)
               {
                  return granted.booleanValue();
               }

               Class< ? > staticType;
               try
               {
                  staticType = Class.forName(name);
               }
               catch (Exception exc)
               {
                  this.nameToAccepted.put(name, Boolean.FALSE);
                  return false;
               }

               boolean grant = shutter.allowClassAccess(staticType);
               this.nameToAccepted.put(name, Boolean.valueOf(grant));
               return grant;
            }
         });
         return cx;
      }

      class SandboxWrapFactory extends WrapFactory
      {
         @Override
         public Scriptable wrapNewObject(Context cx, Scriptable scope, Object obj)
         {
            this.ensureReplacedClass(scope, obj, null);

            return super.wrapNewObject(cx, scope, obj);
         }

         @Override
         public Object wrap(Context cx, Scriptable scope, Object obj, Class< ? > staticType)
         {
            this.ensureReplacedClass(scope, obj, staticType);

            return super.wrap(cx, scope, obj, staticType);
         }

         @Override
         public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class< ? > staticType)
         {
            final Class< ? > type = this.ensureReplacedClass(scope, javaObject, staticType);

            return new NativeJavaObject(scope, javaObject, staticType)
            {
               private final Map<String, Boolean> instanceMethodToAllowed = new HashMap<String, Boolean>();

               @Override
               public Object get(String name, Scriptable scope)
               {
                  Object wrapped = super.get(name, scope);

                  if (wrapped instanceof BaseFunction)
                  {
                     String id = type.getName() + "." + name;
                     Boolean allowed = this.instanceMethodToAllowed.get(id);

                     if (allowed == null)
                     {
                        boolean allow = shutter.allowMethodAccess(type, javaObject, name);
                        this.instanceMethodToAllowed.put(id, allowed = Boolean.valueOf(allow));
                     }

                     if (!allowed.booleanValue())
                     {
                        return NOT_FOUND;
                     }
                  }
                  else
                  {
                     // NativeJavaObject + only boxed primitive types?
                     if (!shutter.allowFieldAccess(type, javaObject, name))
                     {
                        return NOT_FOUND;
                     }
                  }

                  return wrapped;
               }
            };
         }

         //

         private final Set<Class< ? >> replacedClasses = new HashSet<Class< ? >>();

         private Class< ? > ensureReplacedClass(Scriptable scope, Object obj, Class< ? > staticType)
         {
            final Class< ? > type = (staticType == null && obj != null) ? obj.getClass() : staticType;

            if (!type.isPrimitive() && !type.getName().startsWith("java.") && this.replacedClasses.add(type))
            {
               this.replaceJavaNativeClass(type, scope);
            }

            return type;
         }

         private void replaceJavaNativeClass(final Class< ? > type, Scriptable scope)
         {
            Object clazz = Context.jsToJava(ScriptableObject.getProperty(scope, "Packages"), Object.class);
            Object holder = null;
            for (String part : Text.split(type.getName(), '.'))
            {
               holder = clazz;
               clazz = ScriptableObject.getProperty((Scriptable) clazz, part);
            }
            NativeJavaClass nativeClass = (NativeJavaClass) clazz;

            nativeClass = new NativeJavaClass(scope, type)
            {
               @Override
               public Object get(String name, Scriptable start)
               {
                  Object wrapped = super.get(name, start);

                  if (wrapped instanceof BaseFunction)
                  {
                     if (!shutter.allowStaticMethodAccess(type, name))
                     {
                        return NOT_FOUND;
                     }
                  }
                  else
                  {
                     // NativeJavaObject + only boxed primitive types?
                     if (!shutter.allowStaticFieldAccess(type, name))
                     {
                        return NOT_FOUND;
                     }
                  }

                  return wrapped;
               }
            };

            ScriptableObject.putProperty((Scriptable) holder, type.getSimpleName(), nativeClass);
            ScriptableObject.putProperty(scope, type.getSimpleName(), nativeClass);
         }
      }
   }


Install the (global) SandboxContextFactory:
      ContextFactory.initGlobal(new SandboxContextFactory(new SandboxShutter()
      {
         ...
      }));

      // create and initialize Rhino Context
      Context cx = Context.enter();
      Scriptable prototype = cx.initStandardObjects();
      Scriptable topLevel = new ImporterTopLevel(cx);
      prototype.setParentScope(topLevel);
      Scriptable scope = cx.newObject(prototype);
      scope.setPrototype(prototype);

      // your scripts

2010-07-20

Functions on Iterables :: Consume

public class Functional
{
   /**
    * Consumes (removes) all items while iterating
    */

   public static <T> Iterable<T> consume(final Iterable<T> iterable)
   {
      if (iterable == null)
         throw new NullPointerException();

      return new Iterable<T>()
      {
         @Override
         public Iterator<T> iterator()
         {
            final Iterator<T> iterator = iterable.iterator();

            return new Iterator<T>()
            {
               @Override
               public boolean hasNext()
               {
                  return iterator.hasNext();
               }

               @Override
               public T next()
               {
                  T result = iterator.next();
                  iterator.remove();
                  return result;
               }

               @Override
               public void remove()
               {
                  throw new NoSuchElementException("already removed");
               }
            };
         }
      };
   }




To remove all short texts in a list:
List<String> data = ...;

Filter<String> shortText = new Filter<String>()
{
   public boolean accept(String item) { return item == null || item.length() <= 3; }
}

for(String item: consume(filter(data, shortText)))
{
   System.out.println("By the time you see this, '"+item+"' has been removed.");
}

Functions on Iterables :: Event Hooks

public interface Operator
{
   public void operate(T item);
}

public class Functional
{
   /**
    * Performs a callback on each element-visit, and each element removal
    */

    public static <T> Iterable<T> eventHook(final Iterable<T> iterable,
                                            final Operator<T> onVisit,
                                            final Operator<T> onRemove)
   {
      if (iterable == null)
         throw new NullPointerException();
      if (onVisit == null && onRemove == null)
         throw new NullPointerException("must specify either onVisit, onRemove or both");

      return new Iterable<T>()
      {
         @Override
         public Iterator<T> iterator()
         {
            final Iterator<T> iterator = iterable.iterator();

            return new Iterator<T>()
            {
               @Override
               public boolean hasNext()
               {
                  return iterator.hasNext();
               }

               @Override
               public T next()
               {
                  this.current = iterator.next();
                  if (onVisit != null)
                     onVisit.operate(this.current);
                  return this.current;
               }

               @Override
               public void remove()
               {
                  iterator.remove();
                  if (onRemove != null)
                     onRemove.operate(this.current);
               }

               //

               private T current;
            };
         }
      };
   }
}

Example:
Operator<String> onVisit= new Operator<String>()
{
   public void operate(String text)
   {
      System.out.println("visited: "+text);
   }
};

Operator<String> onRemove= new Operator<String>()
{
   public void operate(String text)
   {
      System.out.println("removed: "+text);
   }
};

Iterable<String> data = ...;

for(String item: eventHook(data, onVisit, onRemove))
{
   if(item.length() > String.valueOf(Math.PI))
      System.out.println("this text is longer than PI!");
}


Once you created an Iterable (say, a filtered view on a List) you can iterate over it as often as you want. No need to create a new Iterable.


With the support for closures in Java 7 a lot of the boilerplate code will disappear.

Functions on Iterables :: Transformer

public interface Transformer<I, O>
{
   public O transform(I item);
}

public class Functional
{
   /**
    * Transforms Iterable<I> into Iterable<O> using a Transformer<I, O>
    */

   public static <I, O> Iterable<O> transform(final Iterable<I> iterable,
                                              final Transformer<I, O> transformer)
   {
      if (iterable == null)
         throw new NullPointerException();
      if (transformer == null)
         throw new NullPointerException();

      return new Iterable<O>()
      {
         @Override
         public Iterator<O> iterator()
         {
            final Iterator<I> iterator = iterable.iterator();

            return new Iterator<O>()
            {
               @Override
               public boolean hasNext()
               {
                  return iterator.hasNext();
               }

               @Override
               public O next()
               {
                  return transformer.transform(iterator.next());
               }

               @Override
               public void remove()
               {
                  iterator.remove();
               }
            };
         }
      };
   }
}

Example:
Transformer<String, Integer> textToNumber = new Transformer<String, Integer>()
{
   public Integer transform(String text)
   {
      return Integer.valueOf(text);
   }
};

Iterable<String> data = ...;

for(Integer number: transform(data, textToNumber))
{
   System.out.println("number: "+number.intValue());
}

Functions on Iterables :: Filter

Functional programming has its strengths when iterating over data. If you want a view on a subset data, you can make a filter, and iterate over your data only seeing the elements that the filter accepted:

public interface Filter<T>
{
   public boolean accept(T value);
}

public class Functional
{
   /**
    * Filter items from the view of the returned Iterable<T>
    */

   public static <T> Iterable<T> filter(final Iterable<T> iterable, final Filter<T> filter)
   {
      if (iterable == null)
         throw new NullPointerException();
      if (filter == null)
         throw new NullPointerException();

      return new Iterable<T>()
      {
         @Override
         public Iterator<T> iterator()
         {
            final Iterator<T> iterator = iterable.iterator();

            return new Iterator<T>()
            {
               @Override
               public boolean hasNext()
               {
                  this.ensureReady();
                  return this.ready;
               }

               @Override
               public T next()
               {
                  this.ensureReady();
                  if (!this.ready)
                     throw new NoSuchElementException();

                  T result = this.current;
                  this.ready = false;
                  this.current = null;
                  return result;
               }

               @Override
               public void remove()
               {
                  iterator.remove();
               }

               //

               private boolean ready = false;
               private T       current;

               private void ensureReady()
               {
                  while (!this.ready && iterator.hasNext())
                  {
                     T item = iterator.next();
                     if (!filter.accept(item))
                        continue;

                     this.ready = true;
                     this.current = item;
                  }
               }
            };
         }
      };
   }
}

Example:
Filter<String> nonNull = new Filter<String>()
{
   public boolean accept(String value)
   {
      return value != null;
   }
};

Iterable<String> data = ...;

for(String item: filter(data, nonNull))
{
   System.out.println("item: "+item); // guaranteed not to be null
}