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    
118            Parameter annotation = field.getAnnotation(Parameter.class);
119    
120            String fieldType = field.getTypeName();
121    
122            String parameterName = getParameterName(field.getName(), annotation.name());
123    
124            field.claim(annotation);
125    
126            model.addParameter(parameterName, annotation.required(), annotation.allowNull(), annotation.defaultPrefix(),
127                    annotation.cache());
128    
129            MethodHandle defaultMethodHandle = findDefaultMethodHandle(plasticClass, parameterName);
130    
131            ComputedValue<FieldConduit<Object>> computedParameterConduit = createComputedParameterConduit(parameterName, fieldType,
132                    annotation, defaultMethodHandle);
133    
134            field.setComputedConduit(computedParameterConduit);
135        }
136    
137    
138        private MethodHandle findDefaultMethodHandle(PlasticClass plasticClass, String parameterName)
139        {
140            final String methodName = "default" + parameterName;
141    
142            Predicate<PlasticMethod> predicate = new Predicate<PlasticMethod>()
143            {
144                public boolean accept(PlasticMethod method)
145                {
146                    return method.getDescription().argumentTypes.length == 0
147                            && method.getDescription().methodName.equalsIgnoreCase(methodName);
148                }
149            };
150    
151            Flow<PlasticMethod> matches = F.flow(plasticClass.getMethods()).filter(predicate);
152    
153            // This will match exactly 0 or 1 (unless the user does something really silly)
154            // methods, and if it matches, we know the name of the method.
155    
156            return matches.isEmpty() ? null : matches.first().getHandle();
157        }
158    
159        @SuppressWarnings("all")
160        private ComputedValue<FieldConduit<Object>> createComputedParameterConduit(final String parameterName,
161                                                                                   final String fieldTypeName, final Parameter annotation,
162                                                                                   final MethodHandle defaultMethodHandle)
163        {
164            boolean primitive = PlasticUtils.isPrimitive(fieldTypeName);
165    
166            final boolean allowNull = annotation.allowNull() && !primitive;
167    
168            return new ComputedValue<FieldConduit<Object>>()
169            {
170                public ParameterConduit get(InstanceContext context)
171                {
172                    final InternalComponentResources icr = context.get(InternalComponentResources.class);
173    
174                    final Class fieldType = classCache.forName(fieldTypeName);
175    
176                    final PerThreadValue<ParameterState> stateValue = perThreadManager.createValue();
177    
178                    // Rely on some code generation in the component to set the default binding from
179                    // the field, or from a default method.
180    
181                    return new ParameterConduit()
182                    {
183                        // Default value for parameter, computed *once* at
184                        // page load time.
185    
186                        private Object defaultValue = classCache.defaultValueForType(fieldTypeName);
187    
188                        private Binding parameterBinding;
189    
190                        boolean loaded = false;
191    
192                        private boolean invariant = false;
193    
194                        {
195                            // Inform the ComponentResources about the parameter conduit, so it can be
196                            // shared with mixins.
197    
198                            icr.setParameterConduit(parameterName, this);
199                            icr.getPageLifecycleCallbackHub().addPageLoadedCallback(new Runnable()
200                            {
201                                public void run()
202                                {
203                                    load();
204                                }
205                            });
206                        }
207    
208                        private ParameterState getState()
209                        {
210                            ParameterState state = stateValue.get();
211    
212                            if (state == null)
213                            {
214                                state = new ParameterState();
215                                state.value = defaultValue;
216                                stateValue.set(state);
217                            }
218    
219                            return state;
220                        }
221    
222                        private boolean isLoaded()
223                        {
224                            return loaded;
225                        }
226    
227                        public void set(Object instance, InstanceContext context, Object newValue)
228                        {
229                            ParameterState state = getState();
230    
231                            // Assignments before the page is loaded ultimately exist to set the
232                            // default value for the field. Often this is from the (original)
233                            // constructor method, which is converted to a real method as part of the transformation.
234    
235                            if (!loaded)
236                            {
237                                state.value = newValue;
238                                defaultValue = newValue;
239                                return;
240                            }
241    
242                            // This will catch read-only or unbound parameters.
243    
244                            writeToBinding(newValue);
245    
246                            state.value = newValue;
247    
248                            // If caching is enabled for the parameter (the typical case) and the
249                            // component is currently rendering, then the result
250                            // can be cached in this ParameterConduit (until the component finishes
251                            // rendering).
252    
253                            state.cached = annotation.cache() && icr.isRendering();
254                        }
255    
256                        private Object readFromBinding()
257                        {
258                            Object result;
259    
260                            try
261                            {
262                                Object boundValue = parameterBinding.get();
263    
264                                result = typeCoercer.coerce(boundValue, fieldType);
265                            } catch (RuntimeException ex)
266                            {
267                                throw new TapestryException(String.format(
268                                        "Failure reading parameter '%s' of component %s: %s", parameterName,
269                                        icr.getCompleteId(), InternalUtils.toMessage(ex)), parameterBinding, ex);
270                            }
271    
272                            if (result == null && !allowNull)
273                            {
274                                throw new TapestryException(
275                                        String.format(
276                                                "Parameter '%s' of component %s is bound to null. This parameter is not allowed to be null.",
277                                                parameterName, icr.getCompleteId()), parameterBinding, null);
278                            }
279    
280                            return result;
281                        }
282    
283                        private void writeToBinding(Object newValue)
284                        {
285                            // An unbound parameter acts like a simple field
286                            // with no side effects.
287    
288                            if (parameterBinding == null)
289                            {
290                                return;
291                            }
292    
293                            try
294                            {
295                                Object coerced = typeCoercer.coerce(newValue, parameterBinding.getBindingType());
296    
297                                parameterBinding.set(coerced);
298                            } catch (RuntimeException ex)
299                            {
300                                throw new TapestryException(String.format(
301                                        "Failure writing parameter '%s' of component %s: %s", parameterName,
302                                        icr.getCompleteId(), InternalUtils.toMessage(ex)), icr, ex);
303                            }
304                        }
305    
306                        public void reset()
307                        {
308                            if (!invariant)
309                            {
310                                getState().reset(defaultValue);
311                            }
312                        }
313    
314                        public void load()
315                        {
316                            if (logger.isDebugEnabled())
317                            {
318                                logger.debug(String.format("%s loading parameter %s", icr.getCompleteId(), parameterName));
319                            }
320    
321                            // If it's bound at this point, that's because of an explicit binding
322                            // in the template or @Component annotation.
323    
324                            if (!icr.isBound(parameterName))
325                            {
326                                if (logger.isDebugEnabled())
327                                {
328                                    logger.debug(String.format("%s parameter %s not yet bound", icr.getCompleteId(),
329                                            parameterName));
330                                }
331    
332                                // Otherwise, construct a default binding, or use one provided from
333                                // the component.
334    
335                                Binding binding = getDefaultBindingForParameter();
336    
337                                if (logger.isDebugEnabled())
338                                {
339                                    logger.debug(String.format("%s parameter %s bound to default %s", icr.getCompleteId(),
340                                            parameterName, binding));
341                                }
342    
343                                if (binding != null)
344                                {
345                                    icr.bindParameter(parameterName, binding);
346                                }
347                            }
348    
349                            parameterBinding = icr.getBinding(parameterName);
350    
351                            loaded = true;
352    
353                            invariant = parameterBinding != null && parameterBinding.isInvariant();
354    
355                            getState().value = defaultValue;
356                        }
357    
358                        public boolean isBound()
359                        {
360                            return parameterBinding != null;
361                        }
362    
363                        public Object get(Object instance, InstanceContext context)
364                        {
365                            if (!isLoaded())
366                            {
367                                return defaultValue;
368                            }
369    
370                            ParameterState state = getState();
371    
372                            if (state.cached || !isBound())
373                            {
374                                return state.value;
375                            }
376    
377                            // Read the parameter's binding and cast it to the
378                            // field's type.
379    
380                            Object result = readFromBinding();
381    
382                            // If the value is invariant, we can cache it until at least the end of the request (before
383                            // 5.2, it would be cached forever in the pooled instance).
384                            // Otherwise, we we may want to cache it for the remainder of the component render (if the
385                            // component is currently rendering).
386    
387                            if (invariant || (annotation.cache() && icr.isRendering()))
388                            {
389                                state.value = result;
390                                state.cached = true;
391                            }
392    
393                            return result;
394                        }
395    
396                        private Binding getDefaultBindingForParameter()
397                        {
398                            if (InternalUtils.isNonBlank(annotation.value()))
399                            {
400                                return bindingSource.newBinding("default " + parameterName, icr,
401                                        annotation.defaultPrefix(), annotation.value());
402                            }
403    
404                            if (annotation.autoconnect())
405                            {
406                                return defaultProvider.defaultBinding(parameterName, icr);
407                            }
408    
409                            // Invoke the default method and install any value or Binding returned there.
410    
411                            invokeDefaultMethod();
412    
413                            return parameterBinding;
414                        }
415    
416                        private void invokeDefaultMethod()
417                        {
418                            if (defaultMethodHandle == null)
419                            {
420                                return;
421                            }
422    
423                            if (logger.isDebugEnabled())
424                            {
425                                logger.debug(String.format("%s invoking method %s to obtain default for parameter %s",
426                                        icr.getCompleteId(), defaultMethodHandle, parameterName));
427                            }
428    
429                            MethodInvocationResult result = defaultMethodHandle.invoke(icr.getComponent());
430    
431                            result.rethrow();
432    
433                            Object defaultValue = result.getReturnValue();
434    
435                            if (defaultValue == null)
436                            {
437                                return;
438                            }
439    
440                            if (defaultValue instanceof Binding)
441                            {
442                                parameterBinding = (Binding) defaultValue;
443                                return;
444                            }
445    
446                            parameterBinding = new LiteralBinding(null, "default " + parameterName, defaultValue);
447                        }
448    
449    
450                    };
451                }
452            };
453        }
454    
455        private static String getParameterName(String fieldName, String annotatedName)
456        {
457            if (InternalUtils.isNonBlank(annotatedName))
458            {
459                return annotatedName;
460            }
461    
462            return InternalUtils.stripMemberName(fieldName);
463        }
464    }