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 }