/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 freemarker.template;

import java.io.Serializable;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import freemarker.core._DelayedJQuote;
import freemarker.core._TemplateModelException;
import freemarker.ext.beans.BeansWrapper;

A simple implementation of the TemplateHashModelEx interface, using its own underlying Map or SortedMap for storing the hash entries. If you are wrapping an already existing Map, you should certainly use DefaultMapAdapter instead (see comparison below).

This class is thread-safe if you don't call modifying methods (like put(String, Object), remove(String), etc.) after you have made the object available for multiple threads (assuming you have published it safely to the other threads; see JSR-133 Java Memory Model). These methods aren't called by FreeMarker, so it's usually not a concern.

SimpleHash VS DefaultMapAdapter - Which to use when?

For a Map that exists regardless of FreeMarker, only you need to access it from templates, DefaultMapAdapter should be the default choice, as it reflects the exact behavior of the underlying Map (no surprises), can be unwrapped to the originally wrapped object (important when passing it to Java methods from the template), and has more predictable performance (no spikes).

For a hash that's made specifically to be used from templates, creating an empty SimpleHash then filling it with put(String, Object) is usually the way to go, as the resulting hash is significantly faster to read from templates than a DefaultMapAdapter (though it's somewhat slower to read from a plain Java method to which it had to be passed adapted to a Map).

It also matters if for how many times will the same Map entry be read from the template(s) later, on average. If, on average, you read each entry for more than 4 times, SimpleHash will be most certainly faster, but if for 2 times or less (and especially if not at all) then DefaultMapAdapter will be faster. Before choosing based on performance though, pay attention to the behavioral differences; SimpleHash will shallow-copy the original Map at construction time, so key order will be lost in some cases, and it won't reflect Map content changes after the SimpleHash construction, also SimpleHash can't be unwrapped to the original Map instance.

See Also:
/** * A simple implementation of the {@link TemplateHashModelEx} interface, using its own underlying {@link Map} or * {@link SortedMap} for storing the hash entries. If you are wrapping an already existing {@link Map}, you should * certainly use {@link DefaultMapAdapter} instead (see comparison below). * * <p> * This class is thread-safe if you don't call modifying methods (like {@link #put(String, Object)}, * {@link #remove(String)}, etc.) after you have made the object available for multiple threads (assuming you have * published it safely to the other threads; see JSR-133 Java Memory Model). These methods aren't called by FreeMarker, * so it's usually not a concern. * * <p> * <b>{@link SimpleHash} VS {@link DefaultMapAdapter} - Which to use when?</b> * * <p> * For a {@link Map} that exists regardless of FreeMarker, only you need to access it from templates, * {@link DefaultMapAdapter} should be the default choice, as it reflects the exact behavior of the underlying * {@link Map} (no surprises), can be unwrapped to the originally wrapped object (important when passing it to Java * methods from the template), and has more predictable performance (no spikes). * * <p> * For a hash that's made specifically to be used from templates, creating an empty {@link SimpleHash} then filling it * with {@link SimpleHash#put(String, Object)} is usually the way to go, as the resulting hash is significantly faster * to read from templates than a {@link DefaultMapAdapter} (though it's somewhat slower to read from a plain Java method * to which it had to be passed adapted to a {@link Map}). * * <p> * It also matters if for how many times will the <em>same</em> {@link Map} entry be read from the template(s) later, on * average. If, on average, you read each entry for more than 4 times, {@link SimpleHash} will be most certainly faster, * but if for 2 times or less (and especially if not at all) then {@link DefaultMapAdapter} will be faster. Before * choosing based on performance though, pay attention to the behavioral differences; {@link SimpleHash} will * shallow-copy the original {@link Map} at construction time, so key order will be lost in some cases, and it won't * reflect {@link Map} content changes after the {@link SimpleHash} construction, also {@link SimpleHash} can't be * unwrapped to the original {@link Map} instance. * * @see DefaultMapAdapter * @see TemplateHashModelEx */
public class SimpleHash extends WrappingTemplateModel implements TemplateHashModelEx2, Serializable { private final Map map; private boolean putFailed; private Map unwrappedMap;
Constructs an empty hash that uses the default wrapper set in WrappingTemplateModel.setDefaultObjectWrapper(ObjectWrapper).
Deprecated:Use SimpleHash(ObjectWrapper)
/** * Constructs an empty hash that uses the default wrapper set in * {@link WrappingTemplateModel#setDefaultObjectWrapper(ObjectWrapper)}. * * @deprecated Use {@link #SimpleHash(ObjectWrapper)} */
@Deprecated public SimpleHash() { this((ObjectWrapper) null); }
Creates a new simple hash with the copy of the underlying map and the default wrapper set in WrappingTemplateModel.setDefaultObjectWrapper(ObjectWrapper).
Params:
  • map – The Map to use for the key/value pairs. It makes a copy for internal use. If the map implements the SortedMap interface, the internal copy will be a TreeMap, otherwise it will be a HashMap.
Deprecated:Use SimpleHash(Map, ObjectWrapper)
/** * Creates a new simple hash with the copy of the underlying map and the * default wrapper set in * {@link WrappingTemplateModel#setDefaultObjectWrapper(ObjectWrapper)}. * @param map The Map to use for the key/value pairs. It makes a copy for * internal use. If the map implements the {@link SortedMap} interface, the * internal copy will be a {@link TreeMap}, otherwise it will be a * {@link HashMap}. * * @deprecated Use {@link #SimpleHash(Map, ObjectWrapper)} */
@Deprecated public SimpleHash(Map map) { this(map, null); }
Creates an empty simple hash using the specified object wrapper.
Params:
/** * Creates an empty simple hash using the specified object wrapper. * @param wrapper The object wrapper to use to wrap objects into * {@link TemplateModel} instances. If null, the default wrapper set in * {@link WrappingTemplateModel#setDefaultObjectWrapper(ObjectWrapper)} is * used. */
public SimpleHash(ObjectWrapper wrapper) { super(wrapper); map = new HashMap(); }
Creates a new hash by shallow-coping (possibly cloning) the underlying map; in many applications you should use DefaultMapAdapter instead.
Params:
/** * Creates a new hash by shallow-coping (possibly cloning) the underlying map; in many applications you should use * {@link DefaultMapAdapter} instead. * * @param map * The Map to use for the key/value pairs. It makes a copy for internal use. If the map implements the * {@link SortedMap} interface, the internal copy will be a {@link TreeMap}, otherwise it will be a * @param wrapper * The object wrapper to use to wrap contained objects into {@link TemplateModel} instances. Using * {@code null} is deprecated but allowed, in which case the deprecated default wrapper set in * {@link WrappingTemplateModel#setDefaultObjectWrapper(ObjectWrapper)} is used. */
public SimpleHash(Map map, ObjectWrapper wrapper) { super(wrapper); Map mapCopy; try { mapCopy = copyMap(map); } catch (ConcurrentModificationException cme) { //This will occur extremely rarely. //If it does, we just wait 5 ms and try again. If // the ConcurrentModificationException // is thrown again, we just let it bubble up this time. // TODO: Maybe we should log here. try { Thread.sleep(5); } catch (InterruptedException ie) { } synchronized (map) { mapCopy = copyMap(map); } } this.map = mapCopy; } protected Map copyMap(Map map) { if (map instanceof HashMap) { return (Map) ((HashMap) map).clone(); } if (map instanceof SortedMap) { if (map instanceof TreeMap) { return (Map) ((TreeMap) map).clone(); } else { return new TreeMap((SortedMap) map); } } return new HashMap(map); }
Adds a key-value entry to this hash.
Params:
  • key – The name by which the object is identified in the template.
  • value – The value to which the name will be associated. This will only be wrapped to TemplateModel lazily when it's first read.
/** * Adds a key-value entry to this hash. * * @param key * The name by which the object is identified in the template. * @param value * The value to which the name will be associated. This will only be wrapped to {@link TemplateModel} * lazily when it's first read. */
public void put(String key, Object value) { map.put(key, value); unwrappedMap = null; }
Puts a boolean in the map
Params:
  • key – the name by which the resulting TemplateModel is identified in the template.
  • b – the boolean to store.
/** * Puts a boolean in the map * * @param key the name by which the resulting <tt>TemplateModel</tt> * is identified in the template. * @param b the boolean to store. */
public void put(String key, boolean b) { put(key, b ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE); } public TemplateModel get(String key) throws TemplateModelException { Object result; try { result = map.get(key); } catch (ClassCastException e) { throw new _TemplateModelException(e, "ClassCastException while getting Map entry with String key ", new _DelayedJQuote(key)); } catch (NullPointerException e) { throw new _TemplateModelException(e, "NullPointerException while getting Map entry with String key ", new _DelayedJQuote(key)); } // The key to use for putting -- it's the key that already exists in // the map (either key or charKey below). This way, we'll never put a // new key in the map, avoiding spurious ConcurrentModificationException // from another thread iterating over the map, see bug #1939742 in // SourceForge tracker. Object putKey = null; if (result == null) { // Check for Character key if this is a single-character string. // In SortedMap-s, however, we can't do that safely, as it can cause ClassCastException. if (key.length() == 1 && !(map instanceof SortedMap)) { Character charKey = Character.valueOf(key.charAt(0)); try { result = map.get(charKey); if (result != null || map.containsKey(charKey)) { putKey = charKey; } } catch (ClassCastException e) { throw new _TemplateModelException(e, "ClassCastException while getting Map entry with Character key ", new _DelayedJQuote(key)); } catch (NullPointerException e) { throw new _TemplateModelException(e, "NullPointerException while getting Map entry with Character key ", new _DelayedJQuote(key)); } } if (putKey == null) { if (!map.containsKey(key)) { return null; } else { putKey = key; } } } else { putKey = key; } if (result instanceof TemplateModel) { return (TemplateModel) result; } TemplateModel tm = wrap(result); if (!putFailed) { try { map.put(putKey, tm); } catch (Exception e) { // If it's immutable or something, we just keep going. putFailed = true; } } return tm; }
Tells if the map contains a key or not, regardless if the associated value is null or not.
Since:2.3.20
/** * Tells if the map contains a key or not, regardless if the associated value is {@code null} or not. * @since 2.3.20 */
public boolean containsKey(String key) { return map.containsKey(key); }
Removes the given key from the underlying map.
Params:
  • key – the key to be removed
/** * Removes the given key from the underlying map. * * @param key the key to be removed */
public void remove(String key) { map.remove(key); }
Adds all the key/value entries in the map
Params:
  • m – the map with the entries to add, the keys are assumed to be strings.
/** * Adds all the key/value entries in the map * @param m the map with the entries to add, the keys are assumed to be strings. */
public void putAll(Map m) { for (Iterator it = m.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry) it.next(); this.put((String) entry.getKey(), entry.getValue()); } }
Note that this method creates and returns a deep-copy of the underlying hash used internally. This could be a gotcha for some people at some point who want to alter something in the data model, but we should maintain our immutability semantics (at least using default SimpleXXX wrappers) for the data model. It will recursively unwrap the stuff in the underlying container.
/** * Note that this method creates and returns a deep-copy of the underlying hash used * internally. This could be a gotcha for some people * at some point who want to alter something in the data model, * but we should maintain our immutability semantics (at least using default SimpleXXX wrappers) * for the data model. It will recursively unwrap the stuff in the underlying container. */
public Map toMap() throws TemplateModelException { if (unwrappedMap == null) { Class mapClass = this.map.getClass(); Map m = null; try { m = (Map) mapClass.newInstance(); } catch (Exception e) { throw new TemplateModelException("Error instantiating map of type " + mapClass.getName() + "\n" + e.getMessage()); } // Create a copy to maintain immutability semantics and // Do nested unwrapping of elements if necessary. BeansWrapper bw = BeansWrapper.getDefaultInstance(); for (Iterator it = map.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry) it.next(); Object key = entry.getKey(); Object value = entry.getValue(); if (value instanceof TemplateModel) { value = bw.unwrap((TemplateModel) value); } m.put(key, value); } unwrappedMap = m; } return unwrappedMap; }
Returns the toString() of the underlying Map.
/** * Returns the {@code toString()} of the underlying {@link Map}. */
@Override public String toString() { return map.toString(); } public int size() { return map.size(); } public boolean isEmpty() { return map == null || map.isEmpty(); } public TemplateCollectionModel keys() { return new SimpleCollection(map.keySet(), getObjectWrapper()); } public TemplateCollectionModel values() { return new SimpleCollection(map.values(), getObjectWrapper()); } public KeyValuePairIterator keyValuePairIterator() { return new MapKeyValuePairIterator(map, getObjectWrapper()); } public SimpleHash synchronizedWrapper() { return new SynchronizedHash(); } private class SynchronizedHash extends SimpleHash { @Override public boolean isEmpty() { synchronized (SimpleHash.this) { return SimpleHash.this.isEmpty(); } } @Override public void put(String key, Object obj) { synchronized (SimpleHash.this) { SimpleHash.this.put(key, obj); } } @Override public TemplateModel get(String key) throws TemplateModelException { synchronized (SimpleHash.this) { return SimpleHash.this.get(key); } } @Override public void remove(String key) { synchronized (SimpleHash.this) { SimpleHash.this.remove(key); } } @Override public int size() { synchronized (SimpleHash.this) { return SimpleHash.this.size(); } } @Override public TemplateCollectionModel keys() { synchronized (SimpleHash.this) { return SimpleHash.this.keys(); } } @Override public TemplateCollectionModel values() { synchronized (SimpleHash.this) { return SimpleHash.this.values(); } } @Override public KeyValuePairIterator keyValuePairIterator() { synchronized (SimpleHash.this) { return SimpleHash.this.keyValuePairIterator(); } } @Override public Map toMap() throws TemplateModelException { synchronized (SimpleHash.this) { return SimpleHash.this.toMap(); } } } }