com.sun.faces.application.ConverterPropertyEditorFactory Maven / Gradle / Ivy
Show all versions of jakarta.faces Show documentation
/*
* Copyright (c) 1997, 2018 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.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.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.security.AccessController;
import java.security.PrivilegedAction;
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) {
super();
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) {
super();
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 Class 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 templateClass) {
this.templateClass = templateClass;
try {
ConverterPropertyEditorBase tc = templateClass.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 (InstantiationException | IllegalAccessException | 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, new StringBuilder(32).append('L').append(newClassName).append(';').toString()),
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 ClassLoader targetLoader;
// The class loader which loaded the base class
private ClassLoader myLoader;
public DisposableClassLoader(ClassLoader targetLoader) {
super(targetLoader);
this.targetLoader = targetLoader;
this.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 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 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 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 = (DisposableClassLoader) AccessController.doPrivileged(new PrivilegedAction