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
015package org.apache.tapestry5.internal.transform;
016
017import org.apache.tapestry5.Binding;
018import org.apache.tapestry5.annotations.Parameter;
019import org.apache.tapestry5.func.F;
020import org.apache.tapestry5.func.Flow;
021import org.apache.tapestry5.func.Predicate;
022import org.apache.tapestry5.internal.InternalComponentResources;
023import org.apache.tapestry5.internal.bindings.LiteralBinding;
024import org.apache.tapestry5.internal.services.ComponentClassCache;
025import org.apache.tapestry5.ioc.internal.util.InternalUtils;
026import org.apache.tapestry5.ioc.internal.util.TapestryException;
027import org.apache.tapestry5.ioc.services.PerThreadValue;
028import org.apache.tapestry5.ioc.services.PerthreadManager;
029import org.apache.tapestry5.ioc.services.TypeCoercer;
030import org.apache.tapestry5.model.MutableComponentModel;
031import org.apache.tapestry5.plastic.*;
032import org.apache.tapestry5.services.BindingSource;
033import org.apache.tapestry5.services.ComponentDefaultProvider;
034import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
035import org.apache.tapestry5.services.transform.TransformationSupport;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import 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 */
045public 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}