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 }