Comments (5)
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.
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.
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.
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.
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:
- Add
Template.evaluate(Map<String, Variable>, ResourceOpener)
. Unlike the currentevaluate
methods, theMap
would not be copied, so it could implement dynamic lookup rather than requiring all variables to be defined initially. - 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) }
- Define
DefaultVariable
like this:This class would implement the semantics that EscapeVelocity variables have currently (checking forpublic class DefaultVariable extends Variable { public DefaultVariable(Object wrapped) {...} ... }
Map
andList
and looking up methods using reflection). - Users could subclass either
Variable
orDefaultVariable
to create wrappers like the ones described earlier. We would expose the constructors ofEvaluationException
so those subclasses can throw it. - If we later supported things like
#set ($array[$index] = 17)
then we could add the obvious methods, likepublic 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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from escapevelocity.