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                                @Override
202                                public void run()
203                                {
204                                    load();
205                                }
206                            });
207                        }
208    
209                        private ParameterState getState()
210                        {
211                            ParameterState state = stateValue.get();
212    
213                            if (state == null)
214                            {
215                                state = new ParameterState();
216                                state.value = defaultValue;
217                                stateValue.set(state);
218                            }
219    
220                            return state;
221                        }
222    
223                        private boolean isLoaded()
224                        {
225                            return loaded;
226                        }
227    
228                        public void set(Object instance, InstanceContext context, Object newValue)
229                        {
230                            ParameterState state = getState();
231    
232                            // Assignments before the page is loaded ultimately exist to set the
233                            // default value for the field. Often this is from the (original)
234                            // constructor method, which is converted to a real method as part of the transformation.
235    
236                            if (!loaded)
237                            {
238                                state.value = newValue;
239                                defaultValue = newValue;
240                                return;
241                            }
242    
243                            // This will catch read-only or unbound parameters.
244    
245                            writeToBinding(newValue);
246    
247                            state.value = newValue;
248    
249                            // If caching is enabled for the parameter (the typical case) and the
250                            // component is currently rendering, then the result
251                            // can be cached in this ParameterConduit (until the component finishes
252                            // rendering).
253    
254                            state.cached = annotation.cache() && icr.isRendering();
255                        }
256    
257                        private Object readFromBinding()
258                        {
259                            Object result;
260    
261                            try
262                            {
263                                Object boundValue = parameterBinding.get();
264    
265                                result = typeCoercer.coerce(boundValue, fieldType);
266                            } catch (RuntimeException ex)
267                            {
268                                throw new TapestryException(String.format(
269                                        "Failure reading parameter '%s' of component %s: %s", parameterName,
270                                        icr.getCompleteId(), InternalUtils.toMessage(ex)), parameterBinding, ex);
271                            }
272    
273                            if (result == null && !allowNull)
274                            {
275                                throw new TapestryException(
276                                        String.format(
277                                                "Parameter '%s' of component %s is bound to null. This parameter is not allowed to be null.",
278                                                parameterName, icr.getCompleteId()), parameterBinding, null);
279                            }
280    
281                            return result;
282                        }
283    
284                        private void writeToBinding(Object newValue)
285                        {
286                            // An unbound parameter acts like a simple field
287                            // with no side effects.
288    
289                            if (parameterBinding == null)
290                            {
291                                return;
292                            }
293    
294                            try
295                            {
296                                Object coerced = typeCoercer.coerce(newValue, parameterBinding.getBindingType());
297    
298                                parameterBinding.set(coerced);
299                            } catch (RuntimeException ex)
300                            {
301                                throw new TapestryException(String.format(
302                                        "Failure writing parameter '%s' of component %s: %s", parameterName,
303                                        icr.getCompleteId(), InternalUtils.toMessage(ex)), icr, ex);
304                            }
305                        }
306    
307                        public void reset()
308                        {
309                            if (!invariant)
310                            {
311                                getState().reset(defaultValue);
312                            }
313                        }
314    
315                        public void load()
316                        {
317                            if (logger.isDebugEnabled())
318                            {
319                                logger.debug(String.format("%s loading parameter %s", icr.getCompleteId(), parameterName));
320                            }
321    
322                            // If it's bound at this point, that's because of an explicit binding
323                            // in the template or @Component annotation.
324    
325                            if (!icr.isBound(parameterName))
326                            {
327                                if (logger.isDebugEnabled())
328                                {
329                                    logger.debug(String.format("%s parameter %s not yet bound", icr.getCompleteId(),
330                                            parameterName));
331                                }
332    
333                                // Otherwise, construct a default binding, or use one provided from
334                                // the component.
335    
336                                Binding binding = getDefaultBindingForParameter();
337    
338                                if (logger.isDebugEnabled())
339                                {
340                                    logger.debug(String.format("%s parameter %s bound to default %s", icr.getCompleteId(),
341                                            parameterName, binding));
342                                }
343    
344                                if (binding != null)
345                                {
346                                    icr.bindParameter(parameterName, binding);
347                                }
348                            }
349    
350                            parameterBinding = icr.getBinding(parameterName);
351    
352                            loaded = true;
353    
354                            invariant = parameterBinding != null && parameterBinding.isInvariant();
355    
356                            getState().value = defaultValue;
357                        }
358    
359                        public boolean isBound()
360                        {
361                            return parameterBinding != null;
362                        }
363    
364                        public Object get(Object instance, InstanceContext context)
365                        {
366                            if (!isLoaded())
367                            {
368                                return defaultValue;
369                            }
370    
371                            ParameterState state = getState();
372    
373                            if (state.cached || !isBound())
374                            {
375                                return state.value;
376                            }
377    
378                            // Read the parameter's binding and cast it to the
379                            // field's type.
380    
381                            Object result = readFromBinding();
382    
383                            // If the value is invariant, we can cache it until at least the end of the request (before
384                            // 5.2, it would be cached forever in the pooled instance).
385                            // Otherwise, we we may want to cache it for the remainder of the component render (if the
386                            // component is currently rendering).
387    
388                            if (invariant || (annotation.cache() && icr.isRendering()))
389                            {
390                                state.value = result;
391                                state.cached = true;
392                            }
393    
394                            return result;
395                        }
396    
397                        private Binding getDefaultBindingForParameter()
398                        {
399                            if (InternalUtils.isNonBlank(annotation.value()))
400                            {
401                                return bindingSource.newBinding("default " + parameterName, icr,
402                                        annotation.defaultPrefix(), annotation.value());
403                            }
404    
405                            if (annotation.autoconnect())
406                            {
407                                return defaultProvider.defaultBinding(parameterName, icr);
408                            }
409    
410                            // Invoke the default method and install any value or Binding returned there.
411    
412                            invokeDefaultMethod();
413    
414                            return parameterBinding;
415                        }
416    
417                        private void invokeDefaultMethod()
418                        {
419                            if (defaultMethodHandle == null)
420                            {
421                                return;
422                            }
423    
424                            if (logger.isDebugEnabled())
425                            {
426                                logger.debug(String.format("%s invoking method %s to obtain default for parameter %s",
427                                        icr.getCompleteId(), defaultMethodHandle, parameterName));
428                            }
429    
430                            MethodInvocationResult result = defaultMethodHandle.invoke(icr.getComponent());
431    
432                            result.rethrow();
433    
434                            Object defaultValue = result.getReturnValue();
435    
436                            if (defaultValue == null)
437                            {
438                                return;
439                            }
440    
441                            if (defaultValue instanceof Binding)
442                            {
443                                parameterBinding = (Binding) defaultValue;
444                                return;
445                            }
446    
447                            parameterBinding = new LiteralBinding(null, "default " + parameterName, defaultValue);
448                        }
449    
450    
451                    };
452                }
453            };
454        }
455    
456        private static String getParameterName(String fieldName, String annotatedName)
457        {
458            if (InternalUtils.isNonBlank(annotatedName))
459            {
460                return annotatedName;
461            }
462    
463            return InternalUtils.stripMemberName(fieldName);
464        }
465    }