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 }