/* * 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.Math.max; import static java.lang.String.format; import static java.util.logging.Level.FINE; import static org.omnifaces.util.Facelets.getStringLiteral; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; 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; import org.omnifaces.util.MapWrapper; /** * <p> * The <code><o:importConstants></code> taghandler allows the developer to have a mapping of all constant field * values of the given fully qualified name of a type in the request scope. The constant field values are those public * static final fields. This works for classes, interfaces and enums. * * <h3>Usage</h3> * <p> * For example: * <pre> * public class Foo { * public static final String FOO1 = "foo1"; * public static final String FOO2 = "foo2"; * } * * public interface Bar { * public String BAR1 = "bar1"; * public String BAR2 = "bar2"; * } * * public enum Baz { * BAZ1, BAZ2; * } * </pre> * <p>The constant field values of the above types can be mapped into the request scope as follows: * <pre> * <o:importConstants type="com.example.Foo" /> * <o:importConstants type="com.example.Bar" /> * <o:importConstants type="com.example.Baz" var="Bazzz" /> * ... * #{Foo.FOO1}, #{Foo.FOO2}, #{Bar.BAR1}, #{Bar.BAR2}, #{Bazzz.BAZ1}, #{Bazzz.BAZ2} * </pre> * <p>The map is by default stored in the request scope by the simple name of the type as variable name. You can override * this by explicitly specifying the <code>var</code> attribute, as demonstrated for <code>com.example.Baz</code> in * the above example. * <p> * The resolved constants are by reference stored in the cache to improve retrieving performance. There is also a * runtime (no, not compiletime as that's just not possible in EL) check during retrieving the constant value. * If a constant value doesn't exist, then an <code>IllegalArgumentException</code> will be thrown. * * @author Bauke Scholtz */ public class ImportConstants extends TagHandler { // Constants ------------------------------------------------------------------------------------------------------ private static final Logger logger = Logger.getLogger(ImportConstants.class.getName()); private static final Map<String, Map<String, Object>> CONSTANTS_CACHE = new ConcurrentHashMap<>(); private static final String ERROR_MISSING_CLASS = "Cannot find type '%s' in classpath."; private static final String ERROR_FIELD_ACCESS = "Cannot access constant field '%s' of type '%s'."; private static final String ERROR_INVALID_CONSTANT = "Type '%s' does not have the constant '%s'."; // Variables ------------------------------------------------------------------------------------------------------ private String varValue; private TagAttribute typeAttribute; // Constructors --------------------------------------------------------------------------------------------------- /** * The tag constructor. * @param config The tag config. */ public ImportConstants(TagConfig config) { super(config); varValue = getStringLiteral(getAttribute("var"), "var"); typeAttribute = getRequiredAttribute("type"); } // Actions -------------------------------------------------------------------------------------------------------- /** * First obtain the constants of the class by its fully qualified name as specified in the <code>type</code> * attribute from the cache. If it hasn't been collected yet and is thus not present in the cache, then collect * them and store in cache. Finally set the constants in the request scope by the simple name of the type, or by the * name as specified in the <code>var</code> attribute, if any. */ @Override public void apply(FaceletContext context, UIComponent parent) throws IOException { String type = typeAttribute.getValue(context); Map<String, Object> constants = CONSTANTS_CACHE.get(type); if (constants == null) { constants = collectConstants(type); CONSTANTS_CACHE.put(type, constants); } String var = varValue; if (var == null) { int innerClass = type.lastIndexOf('$'); int outerClass = type.lastIndexOf('.'); var = type.substring(max(innerClass, outerClass) + 1); } context.setAttribute(var, constants); } // Helpers -------------------------------------------------------------------------------------------------------- /** * Collect constants of the given type. That are, all public static final fields of the given type. * @param type The fully qualified name of the type to collect constants for. * @return Constants of the given type. */ private static Map<String, Object> collectConstants(final String type) { Map<String, Object> constants = new LinkedHashMap<>(); for (Field field : toClass(type).getFields()) { if (isPublicStaticFinal(field)) { try { constants.put(field.getName(), field.get(null)); } catch (Exception e) { throw new IllegalArgumentException(format(ERROR_FIELD_ACCESS, type, field.getName()), e); } } } return new ConstantsMap(constants, type); } /** * Convert the given type, which should represent a fully qualified name, to a concrete {@link Class} instance. * @param type The fully qualified name of the class. * @return The concrete {@link Class} instance. * @throws IllegalArgumentException When it is missing in the classpath. */ static Class<?> toClass(String type) { // Package-private so that ImportFunctions can also use it. try { return Class.forName(type, true, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { // Perhaps it's an inner enum which is specified as com.example.SomeClass.SomeEnum. // Let's be lenient on that although the proper type notation should be com.example.SomeClass$SomeEnum. int i = type.lastIndexOf('.'); if (i > 0) { try { return toClass(new StringBuilder(type).replace(i, i + 1, "$").toString()); } catch (Exception ignore) { logger.log(FINE, "Ignoring thrown exception; previous exception will be rethrown instead.", ignore); // Just continue to IllegalArgumentException on original ClassNotFoundException. } } throw new IllegalArgumentException(format(ERROR_MISSING_CLASS, type), e); } } /** * Returns whether the given field is a constant field, that is when it is public, static and final. * @param field The field to be checked. * @return <code>true</code> if the given field is a constant field, otherwise <code>false</code>. */ private static boolean isPublicStaticFinal(Field field) { int modifiers = field.getModifiers(); return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); } // Nested classes ------------------------------------------------------------------------------------------------- /** * Specific map implementation which wraps the given map in {@link Collections#unmodifiableMap(Map)} and throws an * {@link IllegalArgumentException} in {@link ConstantsMap#get(Object)} method when the key doesn't exist at all. * * @author Bauke Scholtz */ private static class ConstantsMap extends MapWrapper<String, Object> { private static final long serialVersionUID = -7699617036767530156L; private String type; public ConstantsMap(Map<String, Object> map, String type) { super(Collections.unmodifiableMap(map)); this.type = type; } @Override public Object get(Object key) { if (!containsKey(key)) { throw new IllegalArgumentException(format(ERROR_INVALID_CONSTANT, type, key)); } return super.get(key); } @Override public boolean equals(Object object) { return super.equals(object) && type.equals(((ConstantsMap) object).type); } @Override public int hashCode() { return super.hashCode() + type.hashCode(); } } }