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 }