Giter VIP home page Giter VIP logo

Comments (5)

eamonnmcmanus avatar eamonnmcmanus commented on September 1, 2024

Thanks, those are interesting ideas!

We've tried to keep the public API of EscapeVelocity very small (one public class, one public interface, two exceptions). While we could enlarge it as suggested, I'd be interested in seeing the use cases first. Did you run into this in a project?

An alternative would be to document how to use custom implementations of Map to achieve specialized behaviours. For example, evaluating ${foo} ends up calling vars.containsKey("foo") and vars.get("foo"), where vars is the Map passed to the evaluate method. You can do dynamic things in those methods; you don't need to start with a static Map where everything is present at the start. Similarly, evaluating ${foo.bar} calls the same two methods as before and then, if the result is also a Map, it will call map.get("bar"). So you can implement Map so it does reflection or JSON or whatever.

from escapevelocity.

sangupta avatar sangupta commented on September 1, 2024

I have been experimenting merge templates with arbitrary JSON data from RPC calls, including providing an intermediate layer that massages the response. I could theoretically convert the the response to a Map instance via Gson first, but that adds some overhead, as we are already working with JsonObject instances.

We could add a method (optionally one more) as:

public void registerResolver(Class<?> objectClass, Resolver resolver) ;
public void removeResolver(Class<?> objectClass);

where Resolver interface that may be defined as:

interface Resolver {

    Object resolve(Object entity, String attribute);

}

This adds a new interface called Resolver which we may even replace with Java interface BiFunction such as:

public void registerResolver(Class<?> objectClass, BiFunction<Object, String, Object> resolver);

Usage would simplify to:

registerResolver(JsonObject.class, new BiFunction() {
    @Override
    public Object apply(Object entity, String attribute) {
        return ((JsonObject) entity).getAsObject(attribute);
    }
});

This way we would not have added any new class/interface, yet provided an extensibility to the system.

from escapevelocity.

eamonnmcmanus avatar eamonnmcmanus commented on September 1, 2024

Let me see if I understand. You'd like to give the Template.evaluate method a Map, call it vars, where some of the values in the map are instances of JsonObject. Let's say vars.get("foo") is fooObject. Then, when fooObject is a JsonObject, you'd like for ${foo.bar} to call fooObject.getAsObject("bar"). Otherwise it should do whatever it would normally would do.

I think it is possible to do this with the existing API. You could wrap all your JsonObject variables in a Wrapper class, like this:

    JsonObject fooObject = new JsonObject();
    fooObject.add("bar", new JsonObject());
    fooObject.add("baz", new JsonPrimitive("not an object"));
    Map<String, Object> vars1 = Map.of("foo", fooObject, "bar", 23);
    Map<String, Object> vars2 =
        Maps.transformValues(vars1,
            value -> value instanceof JsonObject jsonObject ? new Wrapper(jsonObject) : value);
    Template template = Template.parseFrom(new StringReader("$bar $foo.bar"));
    System.out.println(template.evaluate(vars2));

(This is Maps.transformValues from Guava.)

The Wrapper class can look like this:

  class Wrapper extends AbstractMap<String, JsonObject> {
    private final JsonObject wrapped;

    Wrapper(JsonObject wrapped) {
      this.wrapped = wrapped;
    }

    @Override
    public Set<Map.Entry<String, JsonObject>> entrySet() {
      return wrapped.entrySet().stream()
          .filter(e -> e.getValue() instanceof JsonObject)
          .map(e -> Map.entry(e.getKey(), (JsonObject) e.getValue())).collect(toSet());
    }

    @Override
    public JsonObject get(Object o) {
      // This method is implemented for efficiency; AbstractMap.get would work but be slower.
      if (o instanceof String key) {
        JsonElement element = wrapped.get(key);
        if (element instanceof JsonObject object) {
          return object;
        }
      }
      return null;
    }
  }

Then the System.out.println above will print this:

23 {}

where {} is $foo.bar, which has correctly evaluated the fooObject.getAsObject("bar").

This is what I was talking about earlier when I said we could usefully document this technique.

Also, incidentally, I realized when looking into this that EscapeVelocity has a bug: if $foo is a Map and you write $foo.bar, and if the map doesn't actually have "bar" as a key, then it will render as null instead of producing an exception as Apache Velocity would. I'm going to work on fixing that.

from escapevelocity.

sangupta avatar sangupta commented on September 1, 2024

For the current use-case at hand, the above technique shall work. But this destructs the original JsonObject - thus, requiring a deep-clone before working with it. One would need to deep-traverse the Map and and wrap all possible objects before using them directly. This becomes expensive if the objects are being generated in a 3rd party framework where you have no control on objects (Json, Xml, Database entities etc).

In my thoughts it would be simpler to use a resolver functionality. This way the client can register for all possible object types, and then work with values without altering them in any way.

from escapevelocity.

eamonnmcmanus avatar eamonnmcmanus commented on September 1, 2024

I'm going to reply in two parts, first of all specifically for the Gson use case, and secondly for more general use cases.


It's unfortunate that JsonObject doesn't implement Map<String, JsonElement> and that JsonArray doesn't implement List<JsonElement>. Otherwise you could use them straightforwardly in templates: $object.foo or $object["foo"] or $array[3].

However, it is not too difficult to introduce wrappers to remedy that. You would need to wrap every starting JsonObject in the Map passed to Template.evaluate, but then wrappers can be introduced on-demand for accesses to nested objects and arrays. I generalized the code earlier to produce this:

public class Experiment {
  private static Object wrap(JsonElement element) {
    if (element.isJsonObject()) {
      return new WrappedJsonObject((JsonObject) element);
    } else if (element.isJsonArray()) {
      return new WrappedJsonArray((JsonArray) element);
    } else if (element.isJsonPrimitive()) {
      JsonPrimitive primitive = (JsonPrimitive) element;
      if (primitive.isBoolean()) {
        return primitive.getAsBoolean();
      } else if (primitive.isString()) {
        return primitive.getAsString();
      } else if (primitive.isNumber()) {
        return primitive.getAsNumber();
      }
    } else if (element.isJsonNull() || element == null) {
      return null;
    }
    throw new IllegalArgumentException("Unrecognized Gson type: " + element.getClass().getName());
  }

  private static class WrappedJsonObject extends AbstractMap<String, Object> {
    private final JsonObject wrapped;

    WrappedJsonObject(JsonObject wrapped) {
      this.wrapped = wrapped;
    }

    @Override
    public Set<Map.Entry<String, Object>> entrySet() {
      return wrapped.entrySet().stream()
          .map(e -> Map.entry(e.getKey(), wrap(e.getValue())))
          .collect(toSet());
    }

    @Override
    public Object get(Object o) {
      // This method is implemented for efficiency; AbstractMap.get would work but be slower.
      if (o instanceof String key) {
        return wrap(wrapped.get(key));
      }
      return null;
    }
  }

  private static class WrappedJsonArray extends AbstractList<Object> {
    private final JsonArray wrapped;

    WrappedJsonArray(JsonArray wrapped) {
      this.wrapped = wrapped;
    }

    @Override
    public Object get(int index) {
      return wrap(wrapped.get(index));
    }

    @Override
    public int size() {
      return wrapped.size();
    }
  }

  public static void main(String[] args) throws Exception {
    JsonObject nested = new JsonObject();
    nested.add("quux", new JsonPrimitive(23));
    JsonArray array = new JsonArray();
    array.add(false);
    array.add(23);
    array.add(nested);
    JsonObject object = new JsonObject();
    object.add("bar", new JsonObject());
    object.add("baz", new JsonPrimitive("not an object"));
    object.add("array", array);
    Map<String, Object> vars1 = Map.of("foo", object, "bar", 23);
    Map<String, Object> vars2 =
        Maps.transformValues(vars1,
            value -> value instanceof JsonElement jsonElement ? wrap(jsonElement) : value);
    String templateString =
        """
        bar=$bar foo.bar=$foo.bar foo['bar']=$foo['bar']
        foo.array=$foo.array foo.array[2].quux=$foo.array[2].quux
        iterate over foo.array: #foreach ($x in $foo.array) $x #end
        """;
    Template template = Template.parseFrom(new StringReader(templateString));
    System.out.println(template.evaluate(vars2));
  }
}

When run, it produces this output:

bar=23 foo.bar={} foo['bar']={}
foo.array=[false, 23, {quux=23}] foo.array[2].quux=23
iterate over foo.array:  false  23  {quux=23} 

So access to nested objects and arrays, like foo.array[2].quux, works, by dynamically creating wrappers. There's no need for an up-front deep cloning.


The Gson use case happens to be solvable this way, but I agree that more general use cases might not be. It happens that we only need $array[i] (for which a wrapper implementing List works) and $object[key] and $object.key (for which a wrapper implementing Map works). But if we wanted to be able to do $object.method() there would be no trick for that. Maybe if we switched to using MethodHandle rather than reflection in some future version we might be able to pull something off, but it seems clumsy.

I'll also say that a major motivation for EscapeVelocity in the first place was the hugely complex API that Velocity has (247 public classes and interfaces in Velocity 1.7). My concern is that providing extensibility of the sort we're talking about would mean multiplying EscapeVelocity's API surface by a factor of 3 or 4. If we exposed EvaluationContext as suggested earlier, we'd probably also need to expose Macro and MethodFinder. It might also be difficult to add support later for Velocity features that EscapeVelocity currently doesn't support, such as #set ($array[$i] = 17). The smallest API change I can think of would look something like this:

  1. Add Template.evaluate(Map<String, Variable>, ResourceOpener). Unlike the current evaluate methods, the Map would not be copied, so it could implement dynamic lookup rather than requiring all variables to be defined initially.
  2. Define Variable something like this:
    public abstract class Variable {
      public abstract Object value(); // $foo
      public abstract Variable property(String name); // $foo.name
      public abstract Variable get(String key); // $foo[key]
      public abstract Variable get(int index); // $foo[index]
      public abstract Variable invoke(String name, List<Variable> params); // $foo.name(params)
    }
  3. Define DefaultVariable like this:
    public class DefaultVariable extends Variable {
        public DefaultVariable(Object wrapped) {...}
        ...
    }
    This class would implement the semantics that EscapeVelocity variables have currently (checking for Map and List and looking up methods using reflection).
  4. Users could subclass either Variable or DefaultVariable to create wrappers like the ones described earlier. We would expose the constructors of EvaluationException so those subclasses can throw it.
  5. If we later supported things like #set ($array[$index] = 17) then we could add the obvious methods, like public void set(int index, Variable value).

So as things stand I'm inclined to think the extra complexity isn't justified. I do think it would be good to document the wrapper techniques that I showed here.

from escapevelocity.

Related Issues (2)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.