/* * Copyright 2017 OmniFaces * * Licensed 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 org.omnifaces.taghandler; import static java.lang.String.format; import static org.omnifaces.taghandler.ImportConstants.toClass; import static org.omnifaces.util.Facelets.getStringLiteral; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Comparator; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import javax.el.FunctionMapper; import javax.faces.component.UIComponent; import javax.faces.view.facelets.FaceletContext; import javax.faces.view.facelets.TagAttribute; import javax.faces.view.facelets.TagConfig; import javax.faces.view.facelets.TagHandler; /** * <p> * The <code><o:importFunctions></code> taghandler allows the developer to have access to all functions of the * given fully qualified name of a type in the Facelet scope using the usual EL functions syntax without the need to * register them in <code>.taglib.xml</code> file. The functions are those <code>public static</code> methods with a * <strong>non</strong>-<code>void</code> return type. * * <h3>Usage</h3> * <p> * For example: * <pre> * <o:importFunctions type="java.lang.Math" var="m" /> * ... * #{m:abs(-10)} * #{m:max(bean.number1, bean.number2)} * </pre> * <p> * The functions prefix becomes by default the simple name of the type. You can override this by explicitly * specifying the <code>var</code> attribute. * <p> * The resolved functions are by reference stored in the cache to improve retrieving performance. * * <h3>Precaution as to multiple functions with exactly the same method name</h3> * <p> * EL functions does <strong>not</strong> support method overloading. It's therefore <strong>not</strong> possible to * provide overloaded methods like {@link Math#abs(int)}, {@link Math#abs(long)}, {@link Math#abs(float)} and * {@link Math#abs(double)} in four separate EL functions. * <p> * If there are multiple function methods discovered with exactly the same name, then the one with the least amount of * parameters will be used. If there are multiple function methods with exactly the same name and amount of parameters, * then the choice is unspecified (technically, JVM-dependent, the first one in the methods array as found by reflection * would be picked up) and should not be relied upon. So if you absolutely need to differentiate functions in such case, * give them each a different name. * * <h3>Design notes</h3> * <p> * Note that the colon <code>:</code> operator to invoke the method is as required by EL functions spec. It's by * design not easily possible to change it to the period <code>.</code> operator. Also note that in case of * <code>org.omnifaces.util.Faces</code> it's considered poor practice if the same functionality is already available * through the implicit EL objects <code>#{faces}</code>, <code>#{facesContext}</code>, <code>#{view}</code>, * <code>#{request}</code>, etc such as <code>#{faces.development}</code> or <code>#{request.contextPath}</code> which * should be preferred over <code>#{Faces:isDevelopment()}</code> or <code>#{Faces:getRequestContextPath()}</code>. * * @author Bauke Scholtz * @since 1.4 */ public class ImportFunctions extends TagHandler { // Constants ------------------------------------------------------------------------------------------------------ private static final Map<String, Method> FUNCTIONS_CACHE = new ConcurrentHashMap<>(); private static final String ERROR_INVALID_FUNCTION = "Type '%s' does not have the function '%s'."; // Variables ------------------------------------------------------------------------------------------------------ private String varValue; private TagAttribute typeAttribute; // Constructors --------------------------------------------------------------------------------------------------- /** * The tag constructor. * @param config The tag config. */ public ImportFunctions(TagConfig config) { super(config); varValue = getStringLiteral(getAttribute("var"), "var"); typeAttribute = getRequiredAttribute("type"); } // Actions -------------------------------------------------------------------------------------------------------- /** * Register a new {@link FunctionMapper} which checks if the given prefix matches our own <code>var</code> and then * find the associated method based on the given method name. */ @Override public void apply(FaceletContext context, UIComponent parent) throws IOException { String type = typeAttribute.getValue(context); String var = (varValue != null) ? varValue : type.substring(type.lastIndexOf('.') + 1); FunctionMapper originalFunctionMapper = context.getFunctionMapper(); context.setFunctionMapper(new ImportFunctionsMapper(originalFunctionMapper, var, toClass(type))); } // Nested classes ------------------------------------------------------------------------------------------------- private static class ImportFunctionsMapper extends FunctionMapper { private FunctionMapper originalFunctionMapper; private String var; private Class<?> type; public ImportFunctionsMapper(FunctionMapper originalFunctionMapper, String var, Class<?> type) { this.originalFunctionMapper = originalFunctionMapper; this.var = var; this.type = type; } @Override public Method resolveFunction(String prefix, String name) { if (var.equals(prefix)) { String key = type + "." + name; Method function = FUNCTIONS_CACHE.get(key); if (function == null) { function = findMethod(type, name); if (function == null) { throw new IllegalArgumentException(format(ERROR_INVALID_FUNCTION, type.getName(), name)); } FUNCTIONS_CACHE.put(key, function); } return function; } else { return originalFunctionMapper.resolveFunction(prefix, name); } } /** * Collect all public static methods of the given name in the given class, sort them by the amount of parameters * and return the first one. * @param cls The class to find the method in. * @param name The method name. * @return The found method, or <code>null</code> if none is found. */ private static Method findMethod(Class<?> cls, String name) { Set<Method> methods = new TreeSet<>(new Comparator<Method>() { @Override public int compare(Method m1, Method m2) { return Integer.valueOf(m1.getParameterTypes().length).compareTo(m2.getParameterTypes().length); } }); for (Method method : cls.getDeclaredMethods()) { if (method.getName().equals(name) && isPublicStaticNonVoid(method)) { methods.add(method); } } return methods.isEmpty() ? null : methods.iterator().next(); } /** * Returns whether the given method is an utility method, that is when it is public and static and returns a * non-void type. * @param method The method to be checked. * @return <code>true</code> if the given method is an utility method, otherwise <code>false</code>. */ private static boolean isPublicStaticNonVoid(Method method) { int modifiers = method.getModifiers(); return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && method.getReturnType() != void.class; } } }