    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.plastic;
    014
    015import java.lang.reflect.Method;
    016import java.util.Objects;
    017import java.util.Set;
    018import java.util.concurrent.atomic.AtomicLong;
    019
    020import org.apache.tapestry5.internal.plastic.PlasticInternalUtils;
    021import org.apache.tapestry5.internal.plastic.PrimitiveType;
    022
    023/**
    024 * Utilities for user code making use of Plastic.
    025 */
    026public class PlasticUtils
    027{
    028    private static final String SETTER_METHOD_NAME = "__propertyValueProvider__set";
    029
    030    private static final String GETTER_METHOD_NAME = "__propertyValueProvider__get";
    031
    032    /**
    033     * The {@code toString()} method inherited from Object.
    034     */
    035    public static final Method TO_STRING = getMethod(Object.class, "toString");
    036
    037    /**
    038     * The MethodDescription version of {@code toString()}.
    039     */
    040    public static final MethodDescription TO_STRING_DESCRIPTION = new MethodDescription(TO_STRING);
    041
    042    private static final AtomicLong UID_GENERATOR = new AtomicLong(System.nanoTime());
    043
    044    private static final MethodDescription PROPERTY_VALUE_PROVIDER_GETTER_METHOD_DESCRIPTION;
    045
    046    private static final MethodDescription PROPERTY_VALUE_PROVIDER_SETTER_METHOD_DESCRIPTION;
    047
    048    private static final MethodDescription FIELD_VALUE_PROVIDER_METHOD_DESCRIPTION;
    049
    050    static
    051    {
    052        try {
    053            PROPERTY_VALUE_PROVIDER_GETTER_METHOD_DESCRIPTION = new MethodDescription(PropertyValueProvider.class.getMethod(GETTER_METHOD_NAME, String.class));
    054            PROPERTY_VALUE_PROVIDER_SETTER_METHOD_DESCRIPTION = new MethodDescription(PropertyValueProvider.class.getMethod(SETTER_METHOD_NAME, String.class, Object.class));
    055            FIELD_VALUE_PROVIDER_METHOD_DESCRIPTION = new MethodDescription(FieldValueProvider.class.getMethod("__fieldValueProvider__get", String.class));
    056        } catch (Exception e) {
    057            throw new ExceptionInInitializerError(e);
    058        }
    059    }
    060
    061
    062    /**
    063     * Returns a string that can be used as part of a Java identifier and is unique
    064     * for this JVM. Currently returns a hexadecimal string and initialized by
    065     * System.nanoTime() (but both those details may change in the future).
    066     *
    067     * Note that the returned value may start with a numeric digit, so it should be used as a <em>suffix</em>, not
    068     * <em>prefix</em> of a Java identifier.
    069     *
    070     * @return unique id that can be used as part of a Java identifier
    071     */
    072    public static String nextUID()
    073    {
    074        return Long.toHexString(PlasticUtils.UID_GENERATOR.getAndIncrement());
    075    }
    076
    077    /**
    078     * Converts a type (including array and primitive types) to their type name (the way they are represented in Java
    079     * source files).
    080     */
    081    public static String toTypeName(Class type)
    082    {
    083        if (type.isArray())
    084            return toTypeName(type.getComponentType()) + "[]";
    085
    086        return type.getName();
    087    }
    088
    089    /** Converts a number of types (usually, arguments to a method or constructor) into their type names. */
    090    public static String[] toTypeNames(Class[] types)
    091    {
    092        String[] result = new String[types.length];
    093
    094        for (int i = 0; i < result.length; i++)
    095            result[i] = toTypeName(types[i]);
    096
    097        return result;
    098    }
    099
    100    /**
    101     * Gets the wrapper type for a given type (if primitive)
    102     *
    103     * @param type
    104     *            type to look up
    105     * @return the input type for non-primitive type, or corresponding wrapper type (Boolean.class for boolean.class,
    106     *         etc.)
    107     */
    108    public static Class toWrapperType(Class type)
    109    {
    110        assert type != null;
    111
    112        return type.isPrimitive() ? PrimitiveType.getByPrimitiveType(type).wrapperType : type;
    113    }
    114
    115    /**
    116     * Convenience for getting a method from a class.
    117     *
    118     * @param declaringClass
    119     *            containing class
    120     * @param name
    121     *            name of method
    122     * @param parameterTypes
    123     *            types of parameters
    124     * @return the Method
    125     * @throws RuntimeException
    126     *             if any error (such as method not found)
    127     */
    128    @SuppressWarnings("unchecked")
    129    public static Method getMethod(Class declaringClass, String name, Class... parameterTypes)
    130    {
    131        try
    132        {
    133            return declaringClass.getMethod(name, parameterTypes);
    134        }
    135        catch (Exception ex)
    136        {
    137            throw new RuntimeException(ex);
    138        }
    139    }
    140
    141    /**
    142     * Uses {@link #getMethod(Class, String, Class...)} and wraps the result as a {@link MethodDescription}.
    143     *
    144     * @param declaringClass
    145     *            containing class
    146     * @param name
    147     *            name of method
    148     * @param parameterTypes
    149     *            types of parameters
    150     * @return description for method
    151     * @throws RuntimeException
    152     *             if any error (such as method not found)
    153     */
    154    public static MethodDescription getMethodDescription(Class declaringClass, String name, Class... parameterTypes)
    155    {
    156        return new MethodDescription(getMethod(declaringClass, name, parameterTypes));
    157    }
    158
    159    /**
    160     * Determines if the provided type name is a primitive type.
    161     *
    162     * @param typeName Java type name, such as "boolean" or "java.lang.String"
    163     * @return true if primitive
    164     */
    165    public static boolean isPrimitive(String typeName)
    166    {
    167        return PrimitiveType.getByName(typeName) != null;
    168    }
    169
    170    /**
    171     * If the given class is an inner class, returns the enclosing class.
    172     * Otherwise, returns the class name unchanged.
    173     */
    174    public static String getEnclosingClassName(String className)
    175    {
    176        int index = className.indexOf('$');
    177        return index <= 0 ? className : className.substring(0, index);
    178    }
    179
    180    /**
    181     * Utility method for creating {@linkplain FieldInfo} instances.
    182     * @param field a {@linkplain PlasticField}.
    183     * @return a corresponding {@linkplain FieldInfo}.
    184     * @since 5.8.4
    185     */
    186    public static FieldInfo toFieldInfo(PlasticField field)
    187    {
    188        return new FieldInfo(field.getName(), field.getTypeName());
    189    }
    190
    191    /**
    192     * Transforms this {@linkplain PlasticClass} so it implements
    193     * {@linkplain FieldValueProvider} for the given set of field names.
    194     * Notice attempts to read a superclass' private field will result in
    195     * an {@linkplain IllegalAccessError}.
    196     *
    197     * @param plasticClass a {@linkplain PlasticClass} instance.
    198     * @param fieldInfos a {@linkplain Set} of {@linkplain String}s containing the field names.
    199     * @since 5.8.4
    200     */
    201    public static void implementFieldValueProvider(PlasticClass plasticClass, Set<FieldInfo> fieldInfos)
    202    {
    203
    204        final Set<PlasticMethod> methods = plasticClass.introduceInterface(FieldValueProvider.class);
    205
    206        if (!methods.isEmpty())
    207        {
    208            final PlasticMethod method = methods.iterator().next();
    209
    210            method.changeImplementation((builder) -> {
    211
    212                for (FieldInfo field : fieldInfos)
    213                {
    214                    builder.loadArgument(0);
    215                    builder.loadConstant(field.name);
    216                    builder.invokeVirtual(String.class.getName(), "boolean", "equals", Object.class.getName());
    217                    builder.when(Condition.NON_ZERO, ifBuilder -> {
    218                        ifBuilder.loadThis();
    219                        ifBuilder.getField(plasticClass.getClassName(), field.name, field.type);
    220                        ifBuilder.boxPrimitive(field.type);
    221                        ifBuilder.returnResult();
    222                    });
    223                }
    224
    225                builder.loadThis();
    226                builder.instanceOf(FieldValueProvider.class);
    227
    228                builder.when(Condition.NON_ZERO, ifBuilder -> {
    229                    builder.loadThis();
    230                    builder.loadArgument(0);
    231                    ifBuilder.invokeSpecial(
    232                            plasticClass.getSuperClassName(),
    233                            FIELD_VALUE_PROVIDER_METHOD_DESCRIPTION);
    234                    ifBuilder.returnResult();
    235                });
    236
    237                builder.throwException(RuntimeException.class, "Field not found or not supported");
    238
    239            });
    240
    241        }
    242
    243    }
    244
    245    /**
    246     * Transforms this {@linkplain PlasticClass} so it implements
    247     * {@linkplain PropertyValueProvider} for the given set of field names.
    248     * The implementation will use the fields' corresponding getters instead
    249     * of direct fields access.
    250     *
    251     * @param plasticClass a {@linkplain PlasticClass} instance.
    252     * @param fieldInfos a {@linkplain Set} of {@linkplain String}s containing the filed (i.e. property) names.
    253     * @since 5.8.4
    254     */
    255    public static void implementPropertyValueProvider(PlasticClass plasticClass, Set<FieldInfo> fieldInfos)
    256    {
    257
    258        final Set<PlasticMethod> methods = plasticClass.introduceInterface(PropertyValueProvider.class);
    259
    260        final InstructionBuilderCallback getterCallback = (builder) -> {
    261
    262            for (FieldInfo field : fieldInfos)
    263            {
    264                builder.loadArgument(0);
    265                builder.loadConstant(field.name);
    266                builder.invokeVirtual(String.class.getName(), "boolean", "equals", Object.class.getName());
    267                builder.when(Condition.NON_ZERO, ifBuilder ->
    268                {
    269                    final String prefix = field.type.equals("boolean") ? "is" : "get";
    270                    final String methodName = prefix + PlasticInternalUtils.capitalize(field.name);
    271
    272                    ifBuilder.loadThis();
    273                    builder.invokeVirtual(
    274                            plasticClass.getClassName(),
    275                            field.type,
    276                            methodName);
    277                    ifBuilder.boxPrimitive(field.type);
    278                    ifBuilder.returnResult();
    279                });
    280
    281            }
    282
    283            // Field/property not found, so let's try the superclass in case
    284            // it also implement
    285
    286            builder.loadThis();
    287            builder.instanceOf(PropertyValueProvider.class);
    288
    289            builder.when(Condition.NON_ZERO, ifBuilder -> {
    290                builder.loadThis();
    291                builder.loadArgument(0);
    292                ifBuilder.invokeSpecial(
    293                        plasticClass.getSuperClassName(),
    294                        PROPERTY_VALUE_PROVIDER_GETTER_METHOD_DESCRIPTION);
    295                ifBuilder.returnResult();
    296            });
    297
    298            // Giving up
    299
    300            builder.throwException(RuntimeException.class, "Property not found or not supported");
    301
    302        };
    303
    304        final InstructionBuilderCallback setterCallback = (builder) -> {
    305
    306            for (FieldInfo field : fieldInfos)
    307            {
    308                builder.loadArgument(0);
    309                builder.loadConstant(field.name);
    310                builder.invokeVirtual(String.class.getName(), "boolean", "equals", Object.class.getName());
    311                builder.when(Condition.NON_ZERO, ifBuilder ->
    312                {
    313                    final String methodName = "set" + PlasticInternalUtils.capitalize(field.name);
    314
    315                    ifBuilder.loadThis();
    316                    ifBuilder.loadArgument(1);
    317                    ifBuilder.castOrUnbox(field.type);
    318                    ifBuilder.invokeVirtual(
    319                            plasticClass.getClassName(),
    320                            void.class.getName(),
    321                            methodName,
    322                            field.type);
    323                    ifBuilder.returnResult();
    324                });
    325
    326            }
    327
    328            // Field/property not found, so let's try the superclass in case
    329            // it also implement
    330
    331            builder.loadThis();
    332            builder.instanceOf(PropertyValueProvider.class);
    333
    334            builder.when(Condition.NON_ZERO, ifBuilder -> {
    335                builder.loadThis();
    336                builder.loadArgument(0);
    337                builder.loadArgument(1);
    338                ifBuilder.invokeSpecial(
    339                        plasticClass.getSuperClassName(),
    340                        PROPERTY_VALUE_PROVIDER_SETTER_METHOD_DESCRIPTION);
    341                ifBuilder.returnResult();
    342            });
    343
    344            // Giving up
    345
    346            builder.throwException(RuntimeException.class, "Property not found or not supported");
    347
    348        };
    349
    350        final PlasticMethod getterMethod;
    351        final PlasticMethod setterMethod;
    352
    353        // Superclass has already defined this method, so we need to override it so
    354        // it can also find the subclasses' declared fields/properties.
    355        if (methods.isEmpty())
    356        {
    357            getterMethod = plasticClass.introduceMethod(PROPERTY_VALUE_PROVIDER_GETTER_METHOD_DESCRIPTION, getterCallback);
    358            setterMethod = plasticClass.introduceMethod(PROPERTY_VALUE_PROVIDER_SETTER_METHOD_DESCRIPTION, setterCallback);
    359        }
    360        else
    361        {
    362            getterMethod = methods.stream()
    363                    .filter(m -> m.getDescription().methodName.equals(GETTER_METHOD_NAME))
    364                    .findFirst()
    365                    .get();
    366            setterMethod = methods.stream()
    367                    .filter(m -> m.getDescription().methodName.equals(SETTER_METHOD_NAME))
    368                    .findFirst()
    369                    .get();
    370        }
    371
    372        getterMethod.changeImplementation(getterCallback);
    373        setterMethod.changeImplementation(setterCallback);
    374
    375    }
    376
    377    /**
    378     * Class used to represent a field name and its type for
    379     * {@linkplain PlasticUtils#implementFieldValueProvider(PlasticClass, Set)}.
    380     * It shouldn't be used directly. Use {@linkplain PlasticUtils#toFieldInfo(PlasticField)}
    381     * instead.
    382     * @see PlasticUtils#implementFieldValueProvider(PlasticClass, Set)
    383     * @since 5.8.4
    384     */
    385    public static class FieldInfo {
    386        final private String name;
    387        final private String type;
    388        public FieldInfo(String name, String type)
    389        {
    390            super();
    391            this.name = name;
    392            this.type = type;
    393        }
    394        @Override
    395        public int hashCode()
    396        {
    397            return Objects.hash(name);
    398        }
    399        @Override
    400        public boolean equals(Object obj)
    401        {
    402            if (this == obj)
    403            {
    404                return true;
    405            }
    406            if (!(obj instanceof FieldInfo))
    407            {
    408                return false;
    409            }
    410            FieldInfo other = (FieldInfo) obj;
    411            return Objects.equals(name, other.name);
    412        }
    413        @Override
    414        public String toString()
    415        {
    416            return "FieldInfo [name=" + name + ", type=" + type + "]";
    417        }
    418
    419    }
    420
    421}