/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2009, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.filter.function; import static org.geotools.filter.capability.FunctionNameImpl.parameter; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.filter.FunctionExpressionImpl; import org.geotools.filter.capability.FunctionNameImpl; import org.opengis.filter.capability.FunctionName; import org.opengis.filter.expression.Literal; /** * Provides a lookup table of named variables allowing externally defined * values to be access within a SLD document. * <p> * Example: in the application, prior to rendering... * <pre><code> * EnvFunction.setValue("foo", 42); * </code></pre> * Then, in the SLD document we can refer to this variable using * the "env" function * <pre><code> * ... * <FeatureTypeStyle> * <Rule> * <Filter> * <PropertyIsEqualTo> * <PropertyName>FooValue</PropertyName> * <Function name="env"> * <literal>foo</literal> * </Function> * </PropertyIsEqualTo> * </Filter> * ... * </code></pre> * The function provides a lookup table that is local to the active thread * so that a given variable can hold different values in different threads. * There is also a global lookup table, accessible from all threads. * When the function is given a variable to look up it first searches the * thread's local table and then, if the variable was not found, the global * table. All lookups are case-insensitive. * <p> * Setting a fallback value is not supported in accordance with SLD 1.1 specification. * However, you can provide a default value when calling the function as in * these examples: * <pre><code> * <!-- Here, if variable foo is not set the function returns null --> * <Function name="env"> * <Literal>foo</Literal> * </Function> * * <!-- Here, a second argument is provided. If foo is not set the --> * <!-- function will return 0. --> * <Function name="env"> * <Literal>foo</Literal> * <Literal>0</Literal> * </Function> * </code></pre> * The same approach can be used programmatically... * <pre><code> * // set argument to set a default return value of 0 * FilterFactory ff = ... * ff.function("env", ff.literal("foo"), ff.literal(0)); * </code></pre> * * @author Andrea Aime * @author Michael Bedward * @since 2.6 * * @source $URL$ * @version $Id $ */ public class EnvFunction extends FunctionExpressionImpl { /** * Provides a lookup table that is local to each thread. */ private static class LocalLookup extends ThreadLocal<Map<String, Object>> { @Override protected Map<String, Object> initialValue() { return new LinkedHashMap<String, Object>(); } /** * Get the table of lookup values. * Defined to make code using this class more obvious. * * @return the table of lookup values */ public Map<String, Object> getTable() { return super.get(); } }; private static final LocalLookup localLookup = new LocalLookup(); /** * A global lookup table */ private static ConcurrentMap<String, Object> globalLookup = new ConcurrentHashMap<String, Object>(); //public static FunctionName NAME = new FunctionNameImpl("env","variable"); public static FunctionName NAME = new FunctionNameImpl("env", parameter("value", Object.class), parameter("variable", String.class)); /** * Create a new instance of this function. */ public EnvFunction() { super(NAME); } /** * Set the local (to this thread) table of lookup values, deleting any previously * set table. The input {@code Map} is copied. * * @param values the lookup table; if {@code null} the existing lookup * table will be cleared. */ public static void setLocalValues(Map<String, Object> values) { Map<String, Object> table = localLookup.getTable(); table.clear(); if (values != null) { for (Entry<String, Object> e : values.entrySet()) { table.put(e.getKey().toUpperCase(), e.getValue()); } } } /** * Clear all values from the local (to this thread) lookup table. */ public static void clearLocalValues() { localLookup.getTable().clear(); localLookup.remove(); } /** * Set the table of global lookup values that is accessible from any * thread, replacing the previously set table. The input {@code Map} is copied. * * @param values the lookup table; if {@code null} the existing lookup * table will be cleared. */ public static void setGlobalValues(Map<String, Object> values) { globalLookup.clear(); if (values != null) { for (Entry<String, Object> e : values.entrySet()) { globalLookup.put(e.getKey().toUpperCase(), e.getValue()); } } } /** * Clear all values from the global (accessible from any thread) lookup table. */ public static void clearGlobalValues() { globalLookup.clear(); } /** * Add a named value to the local (to this thread) lookup table. If the name is * already present in the table it will be assigned the new value. * * @param name the name * @param value the value */ public static void setLocalValue(String name, Object value) { localLookup.getTable().put(name.toUpperCase(), value); } /** * Add a named value to the global (accessible from any thread) lookup table. * If the name is already present in the table it will be assigned the new value. * * @param name the name * @param value the value */ public static void setGlobalValue(String name, Object value) { globalLookup.put(name.toUpperCase(), value); } /** * {@inheritDoc} * @return Always returns 1 */ @Override public int getArgCount() { return 1; } /** * {@inheritDoc} * The variable name to search for is provided as the single argument to * this function. The active thread's local lookup table is searched first. * If the name is not found there the global table is searched. * * @return the variable value or {@code null} if the variable was not found */ @Override public Object evaluate(Object feature) { String varName = getExpression(0).evaluate(feature, String.class); Object value = localLookup.getTable().get(varName.toUpperCase()); // No result - check the global lookup table if (value == null) { value = globalLookup.get(varName.toUpperCase()); } // Still no result - check if there is a default if (value == null) { final int paramSize = getParameters().size(); if (paramSize > getArgCount()) { value = getExpression(paramSize - 1).evaluate(feature); } } return value; } /** * {@inheritDoc} * This method is overriden to allow for either a single parameter * (variable name) or two parameters (variable name plus default value). */ @Override public void setParameters(List params) { if(params == null){ throw new NullPointerException("params can't be null"); } final int argCount = getArgCount(); final int paramsSize = params.size(); if(paramsSize < argCount || paramsSize > argCount + 1){ throw new IllegalArgumentException( String.format("Function %s expected %d or %d arguments but got %d", name, argCount, argCount+1, paramsSize)); } this.params = new ArrayList(params); } /** * {@inheritDoc} * This method is overriden to ignore the fallback value and log * a warning message. If you want to set a default value it can be provided * as a second argument when calling the function. See the class description * for details. */ @Override public void setFallbackValue(Literal fallback) { Logger logger = Logger.getLogger(EnvFunction.class.getName()); logger.log(Level.WARNING, "The setFallbackValue is not supported by this function." + "Use a second argument when calling the function to provide " + "a default value."); } }