com.sun.faces.application.ConverterPropertyEditorFactory Maven / Gradle / Ivy
Show all versions of jakarta.faces Show documentation
/*
* Copyright (c) 2023 Contributors to Eclipse Foundation.
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.faces.application;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINEST;
import static java.util.logging.Level.WARNING;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.sun.faces.util.FacesLogger;
/**
*
* Factory for dynamically generating PropertyEditor classes that extend {@link ConverterPropertyEditorBase} and replace
* any references to the target class from the template with a supplied target class.
*
*/
public class ConverterPropertyEditorFactory {
private static final Logger LOGGER = FacesLogger.APPLICATION.getLogger();
/**
*
* Capture information extracted from a "template" PropertyEditor class, and perform manipulation of the byte codes in
* order to generate the bytes for a new PropertyEditor class.
*
*
* The new class bytes are generated by identifying UTF8Info entries in the constant pool of the template class, and
* replacing them with new UTF8 constants to define a new class. The constants to be replaced are those for:
*
* - The name of the class itself (com/sun/faces/application/ConverterPropertyEditorFor_XXXX).
* - The class name as a type reference (Lcom/sun/faces/application/ConverterPropertyEditorFor_XXXX;).
* - The name of the target class that the editor will be manipulating (java/util/Date in the current
* template).
*
*
*/
private static class ClassTemplateInfo {
/**
* Capture details of the location of a UTF8Info entry in the constant pool of the template class.
*/
private static class Utf8InfoRef {
/**
* The position of the constant in the byte array that defines the template class.
*/
int index;
/**
* The number of bytes that the constant occupies in the byte array that defines the template class.
*/
int length;
public Utf8InfoRef(int index, int length) {
this.index = index;
this.length = length;
}
}
/**
* Capture details of a single substitution to be made in the template class while generating the new class. Implements
* {@link java.lang.Comparable} so that the replacements can be ordered according to the order they appear in the
* source.
*/
private static class Utf8InfoReplacement implements Comparable {
/**
* The utf8 constant reference from the template source.
*/
Utf8InfoRef ref;
/**
* The bytes to replace the constant with (must also be a valid utf8 constant pool entry).
*/
byte[] replacement;
public Utf8InfoReplacement(Utf8InfoRef ref, String replacement) {
this.ref = ref;
this.replacement = getUtf8InfoBytes(replacement);
}
/**
* Order by the index position of the source UTF8Info reference.
*/
@Override
public int compareTo(Utf8InfoReplacement rhs) {
return ref.index - rhs.ref.index;
}
}
// The source template class on which to base the definition of the new
// PropertyEditor classes.
private final Class extends ConverterPropertyEditorBase> templateClass;
// The bytes that define the source template class.
private byte[] templateBytes;
// The constant_pool_count from the template class bytecodes.
private int constant_pool_count;
// Reference to the class name utf8 constant
private Utf8InfoRef classNameConstant;
// Reference to the class name ref utf8 constant
private Utf8InfoRef classNameRefConstant;
// Reference to the target class name utf8 constant
private Utf8InfoRef targetClassConstant;
/**
* Default constructor uses the {@link ConverterPropertyEditorFor_XXXX} class as the source template.
*/
public ClassTemplateInfo() {
this(ConverterPropertyEditorFor_XXXX.class);
}
/**
* Construct a template info instance based on the supplied class.
*
* @param templateClass is a "template" class (but not in the java generics sense) which must extend
* {@link ConverterPropertyEditorBase} and override the {@link ConverterPropertyEditorBase#getTargetClass} method.
*/
public ClassTemplateInfo(Class extends ConverterPropertyEditorBase> templateClass) {
this.templateClass = templateClass;
try {
ConverterPropertyEditorBase tc = templateClass.getDeclaredConstructor().newInstance();
Class> templateTargetClass = tc.getTargetClass();
loadTemplateBytes();
classNameConstant = findConstant(getVMClassName(templateClass));
classNameRefConstant = findConstant(new StringBuilder(64).append('L').append(getVMClassName(templateClass)).append(';').toString());
targetClassConstant = findConstant(getVMClassName(templateTargetClass));
} catch (IllegalArgumentException | ReflectiveOperationException | SecurityException | IOException e) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Unexected exception ClassTemplateInfo", e);
}
}
}
/**
* Check whether the targetBytes
match the content of the templateBytes
at the given
* index
.
*
* @param targetBytes byte array to compare.
* @param index the index into templateBytes
at which to compare.
* @return true if the bytes from targetBytes
match the bytes from templateBytes
.
*/
private boolean matchAtIndex(byte[] targetBytes, int index) {
if (index < 0 || index + targetBytes.length > templateBytes.length) {
return false;
}
for (int i = 0; i < targetBytes.length; ++i) {
if (targetBytes[i] != templateBytes[index + i]) {
return false;
}
}
return true;
}
/**
* Find an instance of UTF8Info in the source class's constant pool where the text matches the given argument.
*
* @param text the text that the UTF8Info must contain.
* @return A {@link Utf8InfoRef} instance refering to the matched constant pool entry, or null
if no match
* was found.
*/
private Utf8InfoRef findConstant(String text) {
byte[] utf8InfoBytes = getUtf8InfoBytes(text);
assert utf8InfoBytes[0] == 1;
int off = 10;
for (int i = 1; i < constant_pool_count && off < templateBytes.length; ++i) {
if (matchAtIndex(utf8InfoBytes, off)) {
return new Utf8InfoRef(off, utf8InfoBytes.length);
}
switch (templateBytes[off]) {
case 1:// CONSTANT_Utf8
{
int len = (templateBytes[off + 1] & 0xff << 8) + (templateBytes[off + 2] & 0xff);
off += 3 + len;
break;
}
case 7:// CONSTANT_Class
case 8:// CONSTANT_String
off += 3;
break;
case 3:// CONSTANT_Integer
case 4:// CONSTANT_Float
case 9:// CONSTANT_Fieldref
case 10:// CONSTANT_Methodref
case 11:// CONSTANT_InterfaceMethodref
case 12:// CONSTANT_NameAndType
off += 5;
break;
case 5:// CONSTANT_Long
case 6:// CONSTANT_Double
off += 9;
break;
default:
throw new IllegalArgumentException("Unrecognized class file constant pool tag " + templateBytes[off]);
}
}
return null;
}
/**
* Obtain the bytes that define the given class by looking for the ".class" resource and loading the binary data.
*
* @throws IOException if an error occurs loading the binary data
*/
private void loadTemplateBytes() throws IOException {
String resourceName = '/' + templateClass.getName().replace('.', '/') + ".class";
try (InputStream in = ConverterPropertyEditorFactory.class.getResourceAsStream(resourceName)) {
if (in != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int more;
while ((more = in.read(buff)) > 0) {
baos.write(buff, 0, more);
}
templateBytes = baos.toByteArray();
// The bytes should start with the CAFEBABE "magic" header
// for class files.
assert templateBytes.length > 9;
assert templateBytes[0] == (byte) 0xCA;
assert templateBytes[1] == (byte) 0xFE;
assert templateBytes[2] == (byte) 0xBA;
assert templateBytes[3] == (byte) 0xBE;
constant_pool_count = ((templateBytes[8] & 0xff) << 8) + (templateBytes[9] & 0xff);
}
}
}
/**
* Generate a class name to use for the generated PropertyEditor class, based on the full name of the target class. This
* is done by replacing the "XXXX" in the template class name with a version of the target class name.
*
* @param targetClass The target class which the PropertyEditor will operate on.
* @param vmFormat If true, the package name components will be '/' separated. Otherwise they will be '.' separated.
* @return The full name to use for the generated PropertyEditor class.
*/
public String generateClassNameFor(Class> targetClass, boolean vmFormat) {
String name = targetClass.getName();
if (targetClass.isArray()) {
int idx = name.lastIndexOf('[');
int bracketCount = idx + 1;
int semiIdx = name.indexOf(';');
if (semiIdx == -1) {
// primitive array
name = PRIM_MAP.get(name.charAt(idx + 1));
} else {
// Object array
name = name.substring(idx + 2, semiIdx);
}
name += "Array" + bracketCount + 'd';
}
Matcher m = UNDERSCORE_PATTERN.matcher(name);
// Replace existing underscores with one extra underscore.
name = m.replaceAll("$0_");
// Replace existing dots with a single underscore.
name = name.replace('.', '_');
if (vmFormat) {
return getVMClassName(templateClass).replace("XXXX", name);
} else {
return templateClass.getName().replace("XXXX", name);
}
}
/**
* Extract the original target class name from the generated PropertyEditor class name. (This is the reverse of
* {@link #generateClassNameFor}).
*
* @param className name of the generated PropertyEditor class.
* @return the target class name, or null if the given className
was not a generated PropertyEditor name.
*/
public String getTargetClassName(String className) {
String prefix = templateClass.getName().replace("XXXX", "");
if (className.startsWith(prefix)) {
String name = className.substring(prefix.length());
name = SingleUnderscorePattern.matcher(name).replaceAll("$1.$2");
name = MultipleUnderscorePattern.matcher(name).replaceAll("$1");
return name;
}
return null;
}
/**
* Generate the bytes for a new class based on the templateBytes
, but with all the replacements in
* replacements
performed.
*
* @param replacements one or more Utf8InfoReplacments
* @return the bytes for the new class definition.
*/
private byte[] replaceInTemplate(Utf8InfoReplacement... replacements) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Sort the replacements, and weed out any that have no source match
TreeSet sorted = new TreeSet<>();
for (Utf8InfoReplacement r : replacements) {
if (r.ref != null && r.replacement != null) {
sorted.add(r);
}
}
// Now create the output bytes by applying the remaining
// replacements
int from = 0;
for (Utf8InfoReplacement r : sorted) {
baos.write(templateBytes, from, r.ref.index - from);
from = r.ref.index + r.ref.length;
baos.write(r.replacement, 0, r.replacement.length);
}
baos.write(templateBytes, from, templateBytes.length - from);
return baos.toByteArray();
}
/**
* @return the bytes for a new class with the given name and target class.
*
* @param newClassName the binary name of the new class.
* @param targetClassName the binary name of the PropertyEditor's target class.
*/
public byte[] generateClassBytesFor(String newClassName, String targetClassName) {
return replaceInTemplate(new Utf8InfoReplacement(classNameConstant, newClassName),
new Utf8InfoReplacement(classNameRefConstant, 'L' + newClassName + ';'),
new Utf8InfoReplacement(targetClassConstant, targetClassName));
}
}
/**
*
* A custom class loader for the definition of the generated classes. When the generated class is loaded, it will need
* to be able to resolve both the base class ({@link ConverterPropertyEditorBase}) which comes from
* myLoader
and the target class which comes from targetLoader
. This class loader defines only
* the generated class, and delegates to the above two loaders for the rest.
*
*
* The {@link ConverterPropertyEditorFactory} will keep a cache of these class loaders (via weak references), one for
* each class loader that the target classes come from. That way the target class loader (which is likely to be a webapp
* specific loader) can be disposed of and replaced when the webapp is removed or reinstalled.
*
*/
private class DisposableClassLoader extends ClassLoader {
// The class loader which loaded the target class.
private final ClassLoader targetLoader;
// The class loader which loaded the base class
private final ClassLoader myLoader;
public DisposableClassLoader(ClassLoader targetLoader) {
super(targetLoader);
this.targetLoader = targetLoader;
myLoader = ConverterPropertyEditorBase.class.getClassLoader();
}
/**
* Override class loading to enable possible delegation to the two class loaders, rather than just to the parent.
*/
@Override
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// First, check if the class has already been loaded
Class> clazz = findLoadedClass(name);
// Otherwise check if myLoader is able to load it ...
if (clazz == null && myLoader != null && myLoader != targetLoader) {
try {
clazz = myLoader.loadClass(name);
} catch (ClassNotFoundException ignored) {
if (LOGGER.isLoggable(FINEST)) {
LOGGER.log(FINEST, "Ignoring ClassNotFoundException, continuing with parent ClassLoader.", ignored);
}
}
}
// Otherwise go ahead with the targetLoader and with the dynamic
// class generation ...
if (clazz == null) {
clazz = super.loadClass(name, false);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
/**
* If super.loadClass
is unable to locate a class, it will call this method to define it. If the
* className
is a generated PropertyEditor class name, then create the new class. Otherwise call
* super.findClass
which will throw a {@link ClassNotFoundException}.
*/
@Override
protected Class> findClass(String className) throws ClassNotFoundException {
String targetClassName = getTemplateInfo().getTargetClassName(className);
if (targetClassName != null) {
// Need to generate an appropriate PropertyEditor class for the
// specified target class.
byte[] classBytes = getTemplateInfo().generateClassBytesFor(className.replace('.', '/'), targetClassName.replace('.', '/'));
Class> editorClass = defineClass(className, classBytes, 0, classBytes.length);
if (LOGGER.isLoggable(FINE)) {
LOGGER.fine("Defined editorClass " + editorClass);
}
return editorClass;
}
// This will just cause ClassNotFoundException to be thrown.
return super.findClass(className);
}
}
private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_+");
private static final Pattern SingleUnderscorePattern = Pattern.compile("([^_])_([^_])");
private static final Pattern MultipleUnderscorePattern = Pattern.compile("_(_+)");
private static ConverterPropertyEditorFactory defaultInstance;
// Template information extracted from the source template class.
private final ClassTemplateInfo templateInfo;
// Cache of DisposableClassLoaders keyed on the class loader of the target.
private Map> classLoaderCache;
private static final Map PRIM_MAP = new HashMap<>(8, 1.0f);
static {
PRIM_MAP.put('B', "byte");
PRIM_MAP.put('C', "char");
PRIM_MAP.put('S', "short");
PRIM_MAP.put('I', "int");
PRIM_MAP.put('F', "float");
PRIM_MAP.put('J', "long");
PRIM_MAP.put('D', "double");
PRIM_MAP.put('Z', "boolean");
}
/**
* Create a ConverterPropertyEditorFactory
that uses the default template class
* ({@link ConverterPropertyEditorFor_XXXX}).
*/
public ConverterPropertyEditorFactory() {
// Use the default template class
templateInfo = new ClassTemplateInfo();
}
/**
* Create a ConverterPropertyEditorFactory
that uses the specified template class.
*
* @param templateClass the template
*/
public ConverterPropertyEditorFactory(Class extends ConverterPropertyEditorBase> templateClass) {
templateInfo = new ClassTemplateInfo(templateClass);
}
/**
* @return the single default instance of this class (created with the default template class).
*/
public static synchronized ConverterPropertyEditorFactory getDefaultInstance() {
if (defaultInstance == null) {
defaultInstance = new ConverterPropertyEditorFactory();
}
return defaultInstance;
}
private ClassTemplateInfo getTemplateInfo() {
return templateInfo;
}
/**
* Return a PropertyEditor class appropriate for editing the given targetClass
. The new class will be
* defined from a DisposableClassLoader.
*
* @param targetClass the class of object that the returned property editor class will be editing.
* @return the dynamically generated PropertyEditor class.
*/
@SuppressWarnings("unchecked")
public Class extends ConverterPropertyEditorBase> definePropertyEditorClassFor(final Class> targetClass) {
try {
String className = getTemplateInfo().generateClassNameFor(targetClass, false);
if (classLoaderCache == null) {
// Use a WeakHashMap so as not to prevent the class loaders from
// being garbage collected.
classLoaderCache = new WeakHashMap<>();
}
DisposableClassLoader loader;
WeakReference loaderRef = classLoaderCache.get(targetClass.getClassLoader());
if (loaderRef == null || (loader = loaderRef.get()) == null) {
loader = new DisposableClassLoader(targetClass.getClassLoader());
classLoaderCache.put(targetClass.getClassLoader(), new WeakReference<>(loader));
}
return (Class extends ConverterPropertyEditorBase>) loader.loadClass(className);
} catch (ClassNotFoundException e) {
if (LOGGER.isLoggable(WARNING)) {
LOGGER.log(WARNING, "definePropertyEditorClassFor: ClassNotFoundException: " + e.getMessage(), e);
}
}
return null;
}
/**
* @param c the class to find the name of.
* @return the binary name of the class as used by the VM ('/' instead of '.' as a package name separator).
*/
private static String getVMClassName(Class> c) {
return c.getName().replace('.', '/');
}
/**
* Create a UTF8Info constant pool structure for the given text.
*
* @param text the text to create the UTF8 constant from.
* @return the bytes for the UTF8Info constant pool entry, including the tag, length, and utf8 content.
*/
private static byte[] getUtf8InfoBytes(String text) {
byte[] utf8 = text.getBytes(UTF_8);
byte[] info = new byte[utf8.length + 3];
info[0] = 1;
info[1] = (byte) (utf8.length >> 8 & 0xff);
info[2] = (byte) (utf8.length & 0xff);
System.arraycopy(utf8, 0, info, 3, utf8.length);
return info;
}
}