001    // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.transform;
016    
017    import org.apache.tapestry5.Binding;
018    import org.apache.tapestry5.annotations.Parameter;
019    import org.apache.tapestry5.func.F;
020    import org.apache.tapestry5.func.Flow;
021    import org.apache.tapestry5.func.Predicate;
022    import org.apache.tapestry5.internal.InternalComponentResources;
023    import org.apache.tapestry5.internal.bindings.LiteralBinding;
024    import org.apache.tapestry5.internal.services.ComponentClassCache;
025    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026    import org.apache.tapestry5.ioc.internal.util.TapestryException;
027    import org.apache.tapestry5.ioc.services.PerThreadValue;
028    import org.apache.tapestry5.ioc.services.PerthreadManager;
029    import org.apache.tapestry5.ioc.services.TypeCoercer;
030    import org.apache.tapestry5.model.MutableComponentModel;
031    import org.apache.tapestry5.plastic.*;
032    import org.apache.tapestry5.services.BindingSource;
033    import org.apache.tapestry5.services.ComponentDefaultProvider;
034    import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
035    import org.apache.tapestry5.services.transform.TransformationSupport;
036    import org.slf4j.Logger;
037    import org.slf4j.LoggerFactory;
038    
039    import java.util.Comparator;
040    
041    /**
042     * Responsible for identifying parameters via the {@link org.apache.tapestry5.annotations.Parameter} annotation on
043     * component fields. This is one of the most complex of the transformations.
044     */
045    public class ParameterWorker implements ComponentClassTransformWorker2
046    {
047        private final Logger logger = LoggerFactory.getLogger(ParameterWorker.class);
048    
049        /**
050         * Contains the per-thread state about a parameter, as stored (using
051         * a unique key) in the {@link PerthreadManager}. Externalizing such state
052         * is part of Tapestry 5.2's pool-less pages.
053         */
054        private final class ParameterState
055        {
056            boolean cached;
057    
058            Object value;
059    
060            void reset(Object defaultValue)
061            {
062                cached = false;
063                value = defaultValue;
064            }
065        }
066    
067        private final ComponentClassCache classCache;
068    
069        private final BindingSource bindingSource;
070    
071        private final ComponentDefaultProvider defaultProvider;
072    
073        private final TypeCoercer typeCoercer;
074    
075        private final PerthreadManager perThreadManager;
076    
077        public ParameterWorker(ComponentClassCache classCache, BindingSource bindingSource,
078                               ComponentDefaultProvider defaultProvider, TypeCoercer typeCoercer, PerthreadManager perThreadManager)
079        {
080            this.classCache = classCache;
081            this.bindingSource = bindingSource;
082            this.defaultProvider = defaultProvider;
083            this.typeCoercer = typeCoercer;
084            this.perThreadManager = perThreadManager;
085        }
086    
087        private final Comparator<PlasticField> byPrincipalThenName = new Comparator<PlasticField>()
088        {
089            public int compare(PlasticField o1, PlasticField o2)
090            {
091                boolean principal1 = o1.getAnnotation(Parameter.class).principal();
092                boolean principal2 = o2.getAnnotation(Parameter.class).principal();
093    
094                if (principal1 == principal2)
095                {
096                    return o1.getName().compareTo(o2.getName());
097                }
098    
099                return principal1 ? -1 : 1;
100            }
101        };
102    
103    
104        public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
105        {
106            Flow<PlasticField> parametersFields = F.flow(plasticClass.getFieldsWithAnnotation(Parameter.class)).sort(byPrincipalThenName);
107    
108            for (PlasticField field : parametersFields)
109            {
110                convertFieldIntoParameter(plasticClass, model, field);
111            }
112        }
113    
114        private void convertFieldIntoParameter(PlasticClass plasticClass, MutableComponentModel model,
115                                               PlasticField field)
116        {
117            Parameter annotation = field.getAnnotation(Parameter.class);
118    
119            String fieldType = field.getTypeName();
120    
121            String parameterName = getParameterName(field.getName(), annotation.name());
122    
123            field.claim(annotation);
124    
125            model.addParameter(parameterName, annotation.required(), annotation.allowNull(), annotation.defaultPrefix(),
126                    annotation.cache());
127    
128            MethodHandle defaultMethodHandle = findDefaultMethodHandle(plasticClass, parameterName);
129    
130            ComputedValue<FieldConduit<Object>> computedParameterConduit = createComputedParameterConduit(parameterName, fieldType,
131                    annotation, defaultMethodHandle);
132    
133            field.setComputedConduit(computedParameterConduit);
134        }
135    
136    
137        private MethodHandle findDefaultMethodHandle(PlasticClass plasticClass, String parameterName)
138        {
139            final String methodName = "default" + parameterName;
140    
141            Predicate<PlasticMethod> predicate = new Predicate<PlasticMethod>()
142            {
143                public boolean accept(PlasticMethod method)
144                {
145                    return method.getDescription().argumentTypes.length == 0
146                            && method.getDescription().methodName.equalsIgnoreCase(methodName);
147                }
148            };
149    
150            Flow<PlasticMethod> matches = F.flow(plasticClass.getMethods()).filter(predicate);
151    
152            // This will match exactly 0 or 1 (unless the user does something really silly)
153            // methods, and if it matches, we know the name of the method.
154    
155            return matches.isEmpty() ? null : matches.first().getHandle();
156        }
157    
158        @SuppressWarnings("all")
159        private ComputedValue<FieldConduit<Object>> createComputedParameterConduit(final String parameterName,
160                                                                                   final String fieldTypeName, final Parameter annotation,
161                                                                                   final MethodHandle defaultMethodHandle)
162        {
163            boolean primitive = PlasticUtils.isPrimitive(fieldTypeName);
164    
165            final boolean allowNull = annotation.allowNull() && !primitive;
166    
167            return new ComputedValue<FieldConduit<Object>>()
168            {
169                public ParameterConduit get(InstanceContext context)
170                {
171                    final InternalComponentResources icr = context.get(InternalComponentResources.class);
172    
173                    final Class fieldType = classCache.forName(fieldTypeName);
174    
175                    final PerThreadValue<ParameterState> stateValue = perThreadManager.createValue();
176    
177                    // Rely on some code generation in the component to set the default binding from
178                    // the field, or from a default method.
179    
180                    return new ParameterConduit()
181                    {
182                        // Default value for parameter, computed *once* at
183                        // page load time.
184    
185                        private Object defaultValue = classCache.defaultValueForType(fieldTypeName);
186    
187                        private Binding parameterBinding;
188    
189                        boolean loaded = false;
190    
191                        private boolean invariant = false;
192    
193                        {
194                            // Inform the ComponentResources about the parameter conduit, so it can be
195                            // shared with mixins.
196    
197                            icr.setParameterConduit(parameterName, this);
198                        }
199    
200                        private ParameterState getState()
201                        {
202                            ParameterState state = stateValue.get();
203    
204                            if (state == null)
205                            {
206                                state = new ParameterState();
207                                state.value = defaultValue;
208                                stateValue.set(state);
209                            }
210    
211                            return state;
212                        }
213    
214                        private boolean isLoaded()
215                        {
216                            return loaded;
217                        }
218    
219                        public void set(Object instance, InstanceContext context, Object newValue)
220                        {
221                            ParameterState state = getState();
222    
223                            // Assignments before the page is loaded ultimately exist to set the
224                            // default value for the field. Often this is from the (original)
225                            // constructor method, which is converted to a real method as part of the transformation.
226    
227                            if (!loaded)
228                            {
229                                state.value = newValue;
230                                defaultValue = newValue;
231                                return;
232                            }
233    
234                            // This will catch read-only or unbound parameters.
235    
236                            writeToBinding(newValue);
237    
238                            state.value = newValue;
239    
240                            // If caching is enabled for the parameter (the typical case) and the
241                            // component is currently rendering, then the result
242                            // can be cached in this ParameterConduit (until the component finishes
243                            // rendering).
244    
245                            state.cached = annotation.cache() && icr.isRendering();
246                        }
247    
248                        private Object readFromBinding()
249                        {
250                            Object result;
251    
252                            try
253                            {
254                                Object boundValue = parameterBinding.get();
255    
256                                result = typeCoercer.coerce(boundValue, fieldType);
257                            } catch (RuntimeException ex)
258                            {
259                                throw new TapestryException(String.format(
260                                        "Failure reading parameter '%s' of component %s: %s", parameterName,
261                                        icr.getCompleteId(), InternalUtils.toMessage(ex)), parameterBinding, ex);
262                            }
263    
264                            if (result == null && !allowNull)
265                            {
266                                throw new TapestryException(
267                                        String.format(
268                                                "Parameter '%s' of component %s is bound to null. This parameter is not allowed to be null.",
269                                                parameterName, icr.getCompleteId()), parameterBinding, null);
270                            }
271    
272                            return result;
273                        }
274    
275                        private void writeToBinding(Object newValue)
276                        {
277                            // An unbound parameter acts like a simple field
278                            // with no side effects.
279    
280                            if (parameterBinding == null)
281                            {
282                                return;
283                            }
284    
285                            try
286                            {
287                                Object coerced = typeCoercer.coerce(newValue, parameterBinding.getBindingType());
288    
289                                parameterBinding.set(coerced);
290                            } catch (RuntimeException ex)
291                            {
292                                throw new TapestryException(String.format(
293                                        "Failure writing parameter '%s' of component %s: %s", parameterName,
294                                        icr.getCompleteId(), InternalUtils.toMessage(ex)), icr, ex);
295                            }
296                        }
297    
298                        public void reset()
299                        {
300                            if (!invariant)
301                            {
302                                getState().reset(defaultValue);
303                            }
304                        }
305    
306                        public void load()
307                        {
308                            if (logger.isDebugEnabled())
309                            {
310                                logger.debug(String.format("%s loading parameter %s", icr.getCompleteId(), parameterName));
311                            }
312    
313                            // If it's bound at this point, that's because of an explicit binding
314                            // in the template or @Component annotation.
315    
316                            if (!icr.isBound(parameterName))
317                            {
318                                if (logger.isDebugEnabled())
319                                {
320                                    logger.debug(String.format("%s parameter %s not yet bound", icr.getCompleteId(),
321                                            parameterName));
322                                }
323    
324                                // Otherwise, construct a default binding, or use one provided from
325                                // the component.
326    
327                                Binding binding = getDefaultBindingForParameter();
328    
329                                if (logger.isDebugEnabled())
330                                {
331                                    logger.debug(String.format("%s parameter %s bound to default %s", icr.getCompleteId(),
332                                            parameterName, binding));
333                                }
334    
335                                if (binding != null)
336                                {
337                                    icr.bindParameter(parameterName, binding);
338                                }
339                            }
340    
341                            parameterBinding = icr.getBinding(parameterName);
342    
343                            loaded = true;
344    
345                            invariant = parameterBinding != null && parameterBinding.isInvariant();
346    
347                            getState().value = defaultValue;
348                        }
349    
350                        public boolean isBound()
351                        {
352                            return parameterBinding != null;
353                        }
354    
355                        public Object get(Object instance, InstanceContext context)
356                        {
357                            if (!isLoaded())
358                            {
359                                return defaultValue;
360                            }
361    
362                            ParameterState state = getState();
363    
364                            if (state.cached || !isBound())
365                            {
366                                return state.value;
367                            }
368    
369                            // Read the parameter's binding and cast it to the
370                            // field's type.
371    
372                            Object result = readFromBinding();
373    
374                            // If the value is invariant, we can cache it until at least the end of the request (before
375                            // 5.2, it would be cached forever in the pooled instance).
376                            // Otherwise, we we may want to cache it for the remainder of the component render (if the
377                            // component is currently rendering).
378    
379                            if (invariant || (annotation.cache() && icr.isRendering()))
380                            {
381                                state.value = result;
382                                state.cached = true;
383                            }
384    
385                            return result;
386                        }
387    
388                        private Binding getDefaultBindingForParameter()
389                        {
390                            if (InternalUtils.isNonBlank(annotation.value()))
391                            {
392                                return bindingSource.newBinding("default " + parameterName, icr,
393                                        annotation.defaultPrefix(), annotation.value());
394                            }
395    
396                            if (annotation.autoconnect())
397                            {
398                                return defaultProvider.defaultBinding(parameterName, icr);
399                            }
400    
401                            // Invoke the default method and install any value or Binding returned there.
402    
403                            invokeDefaultMethod();
404    
405                            return parameterBinding;
406                        }
407    
408                        private void invokeDefaultMethod()
409                        {
410                            if (defaultMethodHandle == null)
411                            {
412                                return;
413                            }
414    
415                            if (logger.isDebugEnabled())
416                            {
417                                logger.debug(String.format("%s invoking method %s to obtain default for parameter %s",
418                                        icr.getCompleteId(), defaultMethodHandle, parameterName));
419                            }
420    
421                            MethodInvocationResult result = defaultMethodHandle.invoke(icr.getComponent());
422    
423                            result.rethrow();
424    
425                            Object defaultValue = result.getReturnValue();
426    
427                            if (defaultValue == null)
428                            {
429                                return;
430                            }
431    
432                            if (defaultValue instanceof Binding)
433                            {
434                                parameterBinding = (Binding) defaultValue;
435                                return;
436                            }
437    
438                            parameterBinding = new LiteralBinding(null, "default " + parameterName, defaultValue);
439                        }
440    
441    
442                    };
443                }
444            };
445        }
446    
447        private static String getParameterName(String fieldName, String annotatedName)
448        {
449            if (InternalUtils.isNonBlank(annotatedName))
450            {
451                return annotatedName;
452            }
453    
454            return InternalUtils.stripMemberName(fieldName);
455        }
456    }