From: Tim P. <ti...@pa...> - 2012-04-15 17:12:12
|
Hi Alex, On 11 April 2012 00:05, Alex Fiennes wrote: > All, > > I've been doing some more thinking about the problem defined below and I've > got some updates to the situation... > > On 24 November 2011 17:44, Alex Fiennes wrote: >> >> Dear all, > > > <snip> > >> >> First off, I've found a bug. Lets consider the following block of code: >> >> #templet $test { >> Original: $value <br /> >> #set $value = $value + 1 >> Incremented: $value <br /> >> } >> #eval $test using { "value": 1 } >> >> You run this in a servlet and it does what you would expect, ie: >> >> Original: 1 >> Incremented: 2 >> >> However if you reload the page shortly afterwards then you get the >> slightly surprising: >> >> Original: 2 >> Incremented: 3 >> >> which is much less what you would expect. >> >> I've been doing some initial poking around and from what I can see the >> problem is roughly along the lines of: >> >> the parser compiles the {"value": 1} into a Map and then caches this >> the EvalDirective passes this very same Map into the Context that is used >> to evaluate the $test templet >> the $test templet makes changes to it's Context which then changes the >> cached compiled version of {"value": 1} >> the next time we invoke $test, we pass in what we assume is the compiled >> version of {"value": 1}, but it actually now contains {"value": 2} >> >> If one leaves a pause between reloads and the caching engine discards the >> cached parsed templates then the value returns back to 1. >> >> Now obviously, the EvalDirective should be taking a shallow copy of the >> params before setting this as the Map that is backing the Context that is >> used to evaluate $test. >> I've mocked this up in user space so that I can invoke: >> >> #eval $test using $Vars.copyMap({ "value": 1 }) >> >> and I get the expected behaviour, ie the value doesn't increment between >> reloads. >> >> So I'm going to patch EvalDirective to sort this out which brings me onto >> my second point. > > > The problem actually isn't with EvalDirective because I think that it is > doing the right thing. I would like to be able to do something like this if > I wanted to: > > #set $params = { } > #eval $initParams1 using $params > #eval $initParams2 using $params > #eval $doWork using $params > > > and have $doWork get the populated version of $params that has been > customised by passing through the preceeding initialisation routines. > Making EvalDirective clone it's using params breaks this as the > initialisation never makes it out of the preceeding invocations. > > however, if I have a template that reads as follows: > > #set $b = { } > #if ($b.i == null) { #set $b.i = 0 } #else { #set $b.i = $b.i+1 } > Value: $b.i > > then I would expect the value of $b.i to be 0 because we initialise an empty > array and then put the value in. > however this is not the case. The parsed version of $b which is initially > an empty Map is cached along with the rest of the parsed template and > therefore the value of $b.i persists and is incremented each evaluation of > the template until the cache decides to expire it. This is definitely not > what we want to happen and therefore he cloning of the Map should occur when > we want to use the parsed version of "{ }" and therefore we will always have > a "fresh copy" of $b that reflects its state when it was declared. > > I've had a little look into MapBuilder and the code in question looks like > this: > > public Object build(BuildContext pc) > throws BuildException > { > Map<Object, Object> ret = new HashMap<Object, Object>(size()); > boolean isMacro = false; > for (Iterator<Map.Entry<Object, Object>> itr = entrySet().iterator(); > itr.hasNext();) { > Map.Entry<Object, Object> entry = itr.next(); > Object key = entry.getKey(); > Object value = entry.getValue(); > if (key instanceof Builder) > key = ((Builder) key).build(pc); > if (value instanceof Builder) > value = ((Builder) value).build(pc); > if (!isMacro) > isMacro = key instanceof Macro || value instanceof Macro; > ret.put(key, value); > } > if (isMacro) > return new MapMacro(ret); > else > return ret; > } > > What is interesting about this is that we have two different return types - > the Map itself, or the MapMacro wrapper which when evaluated creates a new > Map and populates it with the evaluated contents of the original parsed and > cached Map above. This would imply that if you Map definition is not > constant, ie contains Macros then we shouldn't be seeing the bug, and if we > extend our test case to: > > #set $var = "variable" > #set $b = { "var": $var } > #if ($b.i == null) { #set $b.i = 1 } #else { #set $b.i = $b.i+1 } > Value: $b.i > > > then we do in fact not see an incrementing $b.i because the parser is > evaluating the value of $var each time that $b is utilised and therefore the > problem goes away. > > I am therefore proposing that in cases where MapBuilder doesn't have any > Macros in then it returns a simpler wrapper like so: > > class MapClone > implements Macro > { > private final Map<Object, Object> __map; > > MapClone(Map<Object,Object> map) { > __map = map; > } > > @Override > public Object evaluate(Context context) > throws PropertyException > { > return new HashMap<Object,Object>(__map); > } > > @Override > public void write(FastWriter out, > Context context) > throws PropertyException, IOException > { > out.write(__map.toString()); > } > } > > > which just creates a clean copy of the cached Map every time it is asked for > it, and we return it as follows: > > if (isMacro) > return new MapMacro(ret); > else > return new MapClone(ret); > > > then it is no longer possible to reproduce any of the unexpected behaviour > and state is no longer preserved between re-invocations of the Template. > > If anyone can see any flaws in the above then let me know - otherwise I will > commit the updates and move forwards... > I cannot see a flaw, but that is not saying much. Are you going to commit a failing test first and then a fix for that? I do not currently use any of the fancy features of WM, only its most basic functionality, so am not a central user. Which repository are you committing to? (or are you still using CVS?) cheers Tim -- Tim Pizey http://pizey.net/~timp |