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