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    /**
029     * The {@code toString()} method inherited from Object.
030     */
031    public static final Method TO_STRING = getMethod(Object.class, "toString");
032
033    /**
034     * The MethodDescription version of {@code toString()}.
035     */
036    public static final MethodDescription TO_STRING_DESCRIPTION = new MethodDescription(TO_STRING);
037
038    private static final AtomicLong UID_GENERATOR = new AtomicLong(System.nanoTime());
039    
040    private static final MethodDescription PROPERTY_VALUE_PROVIDER_METHOD_DESCRIPTION;
041    
042    private static final MethodDescription FIELD_VALUE_PROVIDER_METHOD_DESCRIPTION;
043    
044    static
045    {
046        try {
047            PROPERTY_VALUE_PROVIDER_METHOD_DESCRIPTION = new MethodDescription(PropertyValueProvider.class.getMethod("__propertyValueProvider__get", String.class));
048            FIELD_VALUE_PROVIDER_METHOD_DESCRIPTION = new MethodDescription(FieldValueProvider.class.getMethod("__fieldValueProvider__get", String.class));
049        } catch (Exception e) {
050            throw new ExceptionInInitializerError(e);
051        }
052    }
053
054
055    /**
056     * Returns a string that can be used as part of a Java identifier and is unique
057     * for this JVM. Currently returns a hexadecimal string and initialized by
058     * System.nanoTime() (but both those details may change in the future).
059     *
060     * Note that the returned value may start with a numeric digit, so it should be used as a <em>suffix</em>, not
061     * <em>prefix</em> of a Java identifier.
062     * 
063     * @return unique id that can be used as part of a Java identifier
064     */
065    public static String nextUID()
066    {
067        return Long.toHexString(PlasticUtils.UID_GENERATOR.getAndIncrement());
068    }
069
070    /**
071     * Converts a type (including array and primitive types) to their type name (the way they are represented in Java
072     * source files).
073     */
074    public static String toTypeName(Class type)
075    {
076        if (type.isArray())
077            return toTypeName(type.getComponentType()) + "[]";
078
079        return type.getName();
080    }
081
082    /** Converts a number of types (usually, arguments to a method or constructor) into their type names. */
083    public static String[] toTypeNames(Class[] types)
084    {
085        String[] result = new String[types.length];
086
087        for (int i = 0; i < result.length; i++)
088            result[i] = toTypeName(types[i]);
089
090        return result;
091    }
092
093    /**
094     * Gets the wrapper type for a given type (if primitive)
095     * 
096     * @param type
097     *            type to look up
098     * @return the input type for non-primitive type, or corresponding wrapper type (Boolean.class for boolean.class,
099     *         etc.)
100     */
101    public static Class toWrapperType(Class type)
102    {
103        assert type != null;
104
105        return type.isPrimitive() ? PrimitiveType.getByPrimitiveType(type).wrapperType : type;
106    }
107
108    /**
109     * Convenience for getting a method from a class.
110     * 
111     * @param declaringClass
112     *            containing class
113     * @param name
114     *            name of method
115     * @param parameterTypes
116     *            types of parameters
117     * @return the Method
118     * @throws RuntimeException
119     *             if any error (such as method not found)
120     */
121    @SuppressWarnings("unchecked")
122    public static Method getMethod(Class declaringClass, String name, Class... parameterTypes)
123    {
124        try
125        {
126            return declaringClass.getMethod(name, parameterTypes);
127        }
128        catch (Exception ex)
129        {
130            throw new RuntimeException(ex);
131        }
132    }
133
134    /**
135     * Uses {@link #getMethod(Class, String, Class...)} and wraps the result as a {@link MethodDescription}.
136     * 
137     * @param declaringClass
138     *            containing class
139     * @param name
140     *            name of method
141     * @param parameterTypes
142     *            types of parameters
143     * @return description for method
144     * @throws RuntimeException
145     *             if any error (such as method not found)
146     */
147    public static MethodDescription getMethodDescription(Class declaringClass, String name, Class... parameterTypes)
148    {
149        return new MethodDescription(getMethod(declaringClass, name, parameterTypes));
150    }
151
152    /**
153     * Determines if the provided type name is a primitive type.
154     *
155     * @param typeName Java type name, such as "boolean" or "java.lang.String"
156     * @return true if primitive
157     */
158    public static boolean isPrimitive(String typeName)
159    {
160        return PrimitiveType.getByName(typeName) != null;
161    }
162
163    /**
164     * If the given class is an inner class, returns the enclosing class.
165     * Otherwise, returns the class name unchanged.
166     */
167    public static String getEnclosingClassName(String className)
168    {
169        int index = className.indexOf('$');
170        return index <= 0 ? className : className.substring(0, index);
171    }
172
173    /**
174     * Utility method for creating {@linkplain FieldInfo} instances.
175     * @param field a {@linkplain PlasticField}.
176     * @return a corresponding {@linkplain FieldInfo}.
177     * @since 5.8.4
178     */
179    public static FieldInfo toFieldInfo(PlasticField field)
180    {
181        return new FieldInfo(field.getName(), field.getTypeName());
182    }
183    
184    /**
185     * Transforms this {@linkplain PlasticClass} so it implements
186     * {@linkplain FieldValueProvider} for the given set of field names.
187     * Notice attempts to read a superclass' private field will result in 
188     * an {@linkplain IllegalAccessError}.
189     * 
190     * @param plasticClass a {@linkplain PlasticClass} instance.
191     * @param fieldInfos a {@linkplain Set} of {@linkplain String}s containing the field names.
192     * @since 5.8.4
193     */
194    public static void implementFieldValueProvider(PlasticClass plasticClass, Set<FieldInfo> fieldInfos)
195    {
196        
197        final Set<PlasticMethod> methods = plasticClass.introduceInterface(FieldValueProvider.class);
198        
199        if (!methods.isEmpty())
200        {
201            final PlasticMethod method = methods.iterator().next();
202            
203            method.changeImplementation((builder) -> {
204                
205                for (FieldInfo field : fieldInfos) 
206                {
207                    builder.loadArgument(0);
208                    builder.loadConstant(field.name);
209                    builder.invokeVirtual(String.class.getName(), "boolean", "equals", Object.class.getName());
210                    builder.when(Condition.NON_ZERO, ifBuilder -> {
211                        ifBuilder.loadThis();
212                        ifBuilder.getField(plasticClass.getClassName(), field.name, field.type);
213                        ifBuilder.boxPrimitive(field.type);
214                        ifBuilder.returnResult();
215                    });
216                }
217                
218                builder.loadThis();
219                builder.instanceOf(FieldValueProvider.class);
220                
221                builder.when(Condition.NON_ZERO, ifBuilder -> {
222                    builder.loadThis();
223                    builder.loadArgument(0);
224                    ifBuilder.invokeSpecial(
225                            plasticClass.getSuperClassName(), 
226                            FIELD_VALUE_PROVIDER_METHOD_DESCRIPTION);
227                    ifBuilder.returnResult();
228                });
229                
230                builder.throwException(RuntimeException.class, "Field not found or not supported");
231                
232            });
233            
234        }
235        
236    }
237    
238    /**
239     * Transforms this {@linkplain PlasticClass} so it implements
240     * {@linkplain PropertyValueProvider} for the given set of field names.
241     * The implementation will use the fields' corresponding getters instead
242     * of direct fields access.
243     * 
244     * @param plasticClass a {@linkplain PlasticClass} instance.
245     * @param fieldInfos a {@linkplain Set} of {@linkplain String}s containing the filed (i.e. property) names.
246     * @since 5.8.4
247     */
248    public static void implementPropertyValueProvider(PlasticClass plasticClass, Set<FieldInfo> fieldInfos)
249    {
250        
251        final Set<PlasticMethod> methods = plasticClass.introduceInterface(PropertyValueProvider.class);
252        
253        final InstructionBuilderCallback callback = (builder) -> {
254            
255            for (FieldInfo field : fieldInfos) 
256            {
257                builder.loadArgument(0);
258                builder.loadConstant(field.name);
259                builder.invokeVirtual(String.class.getName(), "boolean", "equals", Object.class.getName());
260                builder.when(Condition.NON_ZERO, ifBuilder -> 
261                {
262                    final String prefix = field.type.equals("boolean") ? "is" : "get";
263                    final String methodName = prefix + PlasticInternalUtils.capitalize(field.name);
264                    
265                    ifBuilder.loadThis();
266                    builder.invokeVirtual(
267                            plasticClass.getClassName(), 
268                            field.type, 
269                            methodName);
270                    ifBuilder.boxPrimitive(field.type);
271                    ifBuilder.returnResult();
272                });
273                
274            }
275            
276            builder.loadThis();
277            builder.instanceOf(PropertyValueProvider.class);
278            
279            builder.when(Condition.NON_ZERO, ifBuilder -> {
280                builder.loadThis();
281                builder.loadArgument(0);
282                ifBuilder.invokeSpecial(
283                        plasticClass.getSuperClassName(), 
284                        PROPERTY_VALUE_PROVIDER_METHOD_DESCRIPTION);
285                ifBuilder.returnResult();
286            });
287            
288            // Field/property not found, so let's try the superclass in case
289            // it also implement
290            
291            builder.throwException(RuntimeException.class, "Property not found or not supported");
292            
293        };
294        
295        final PlasticMethod method;
296        
297        // Superclass has already defined this method, so we need to override it so
298        // it can also find the subclasses' declared fields/properties.
299        if (methods.isEmpty())
300        {
301            method = plasticClass.introduceMethod(PROPERTY_VALUE_PROVIDER_METHOD_DESCRIPTION , callback);
302        }
303        else
304        {
305            method = methods.iterator().next();
306        }
307        
308        method.changeImplementation(callback);
309        
310    }
311
312    /**
313     * Class used to represent a field name and its type for 
314     * {@linkplain PlasticUtils#implementFieldValueProvider(PlasticClass, Set)}.
315     * It shouldn't be used directly. Use {@linkplain PlasticUtils#toFieldInfo(PlasticField)}
316     * instead.
317     * @see PlasticUtils#implementFieldValueProvider(PlasticClass, Set)
318     * @since 5.8.4
319     */
320    public static class FieldInfo {
321        final private String name;
322        final private String type;
323        public FieldInfo(String name, String type) 
324        {
325            super();
326            this.name = name;
327            this.type = type;
328        }
329        @Override
330        public int hashCode() 
331        {
332            return Objects.hash(name);
333        }
334        @Override
335        public boolean equals(Object obj) 
336        {
337            if (this == obj) 
338            {
339                return true;
340            }
341            if (!(obj instanceof FieldInfo)) 
342            {
343                return false;
344            }
345            FieldInfo other = (FieldInfo) obj;
346            return Objects.equals(name, other.name);
347        }
348        @Override
349        public String toString() 
350        {
351            return "FieldInfo [name=" + name + ", type=" + type + "]";
352        }
353        
354    }
355    
356}