001// Copyright 2006-2013 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 015package org.apache.tapestry5.internal.transform; 016 017import org.apache.tapestry5.Binding; 018import org.apache.tapestry5.annotations.Parameter; 019import org.apache.tapestry5.func.F; 020import org.apache.tapestry5.func.Flow; 021import org.apache.tapestry5.func.Predicate; 022import org.apache.tapestry5.internal.InternalComponentResources; 023import org.apache.tapestry5.internal.bindings.LiteralBinding; 024import org.apache.tapestry5.internal.services.ComponentClassCache; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026import org.apache.tapestry5.ioc.internal.util.TapestryException; 027import org.apache.tapestry5.ioc.services.PerThreadValue; 028import org.apache.tapestry5.ioc.services.PerthreadManager; 029import org.apache.tapestry5.ioc.services.TypeCoercer; 030import org.apache.tapestry5.ioc.util.ExceptionUtils; 031import org.apache.tapestry5.model.MutableComponentModel; 032import org.apache.tapestry5.plastic.*; 033import org.apache.tapestry5.services.BindingSource; 034import org.apache.tapestry5.services.ComponentDefaultProvider; 035import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; 036import org.apache.tapestry5.services.transform.TransformationSupport; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import java.util.Comparator; 041 042/** 043 * Responsible for identifying parameters via the {@link org.apache.tapestry5.annotations.Parameter} annotation on 044 * component fields. This is one of the most complex of the transformations. 045 */ 046public class ParameterWorker implements ComponentClassTransformWorker2 047{ 048 private final Logger logger = LoggerFactory.getLogger(ParameterWorker.class); 049 050 /** 051 * Contains the per-thread state about a parameter, as stored (using 052 * a unique key) in the {@link PerthreadManager}. Externalizing such state 053 * is part of Tapestry 5.2's pool-less pages. 054 */ 055 private final class ParameterState 056 { 057 boolean cached; 058 059 Object value; 060 061 void reset(Object defaultValue) 062 { 063 cached = false; 064 value = defaultValue; 065 } 066 } 067 068 private final ComponentClassCache classCache; 069 070 private final BindingSource bindingSource; 071 072 private final ComponentDefaultProvider defaultProvider; 073 074 private final TypeCoercer typeCoercer; 075 076 private final PerthreadManager perThreadManager; 077 078 public ParameterWorker(ComponentClassCache classCache, BindingSource bindingSource, 079 ComponentDefaultProvider defaultProvider, TypeCoercer typeCoercer, PerthreadManager perThreadManager) 080 { 081 this.classCache = classCache; 082 this.bindingSource = bindingSource; 083 this.defaultProvider = defaultProvider; 084 this.typeCoercer = typeCoercer; 085 this.perThreadManager = perThreadManager; 086 } 087 088 private final Comparator<PlasticField> byPrincipalThenName = new Comparator<PlasticField>() 089 { 090 public int compare(PlasticField o1, PlasticField o2) 091 { 092 boolean principal1 = o1.getAnnotation(Parameter.class).principal(); 093 boolean principal2 = o2.getAnnotation(Parameter.class).principal(); 094 095 if (principal1 == principal2) 096 { 097 return o1.getName().compareTo(o2.getName()); 098 } 099 100 return principal1 ? -1 : 1; 101 } 102 }; 103 104 105 public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) 106 { 107 Flow<PlasticField> parametersFields = F.flow(plasticClass.getFieldsWithAnnotation(Parameter.class)).sort(byPrincipalThenName); 108 109 for (PlasticField field : parametersFields) 110 { 111 convertFieldIntoParameter(plasticClass, model, field); 112 } 113 } 114 115 private void convertFieldIntoParameter(PlasticClass plasticClass, MutableComponentModel model, 116 PlasticField field) 117 { 118 119 Parameter annotation = field.getAnnotation(Parameter.class); 120 121 String fieldType = field.getTypeName(); 122 123 String parameterName = getParameterName(field.getName(), annotation.name()); 124 125 field.claim(annotation); 126 127 model.addParameter(parameterName, annotation.required(), annotation.allowNull(), annotation.defaultPrefix(), 128 annotation.cache()); 129 130 MethodHandle defaultMethodHandle = findDefaultMethodHandle(plasticClass, parameterName); 131 132 ComputedValue<FieldConduit<Object>> computedParameterConduit = createComputedParameterConduit(parameterName, fieldType, 133 annotation, defaultMethodHandle); 134 135 field.setComputedConduit(computedParameterConduit); 136 } 137 138 139 private MethodHandle findDefaultMethodHandle(PlasticClass plasticClass, String parameterName) 140 { 141 final String methodName = "default" + parameterName; 142 143 Predicate<PlasticMethod> predicate = new Predicate<PlasticMethod>() 144 { 145 public boolean accept(PlasticMethod method) 146 { 147 return method.getDescription().argumentTypes.length == 0 148 && method.getDescription().methodName.equalsIgnoreCase(methodName); 149 } 150 }; 151 152 Flow<PlasticMethod> matches = F.flow(plasticClass.getMethods()).filter(predicate); 153 154 // This will match exactly 0 or 1 (unless the user does something really silly) 155 // methods, and if it matches, we know the name of the method. 156 157 return matches.isEmpty() ? null : matches.first().getHandle(); 158 } 159 160 @SuppressWarnings("all") 161 private ComputedValue<FieldConduit<Object>> createComputedParameterConduit(final String parameterName, 162 final String fieldTypeName, final Parameter annotation, 163 final MethodHandle defaultMethodHandle) 164 { 165 boolean primitive = PlasticUtils.isPrimitive(fieldTypeName); 166 167 final boolean allowNull = annotation.allowNull() && !primitive; 168 169 return new ComputedValue<FieldConduit<Object>>() 170 { 171 public ParameterConduit get(InstanceContext context) 172 { 173 final InternalComponentResources icr = context.get(InternalComponentResources.class); 174 175 final Class fieldType = classCache.forName(fieldTypeName); 176 177 final PerThreadValue<ParameterState> stateValue = perThreadManager.createValue(); 178 179 // Rely on some code generation in the component to set the default binding from 180 // the field, or from a default method. 181 182 return new ParameterConduit() 183 { 184 // Default value for parameter, computed *once* at 185 // page load time. 186 187 private Object defaultValue = classCache.defaultValueForType(fieldTypeName); 188 189 private Binding parameterBinding; 190 191 boolean loaded = false; 192 193 private boolean invariant = false; 194 195 { 196 // Inform the ComponentResources about the parameter conduit, so it can be 197 // shared with mixins. 198 199 icr.setParameterConduit(parameterName, this); 200 icr.getPageLifecycleCallbackHub().addPageLoadedCallback(new Runnable() 201 { 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(), ExceptionUtils.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(), ExceptionUtils.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}