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}