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 }