001// Copyright 2009-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 017 018import org.apache.tapestry5.ComponentResources; 019import org.apache.tapestry5.annotations.BindParameter; 020import org.apache.tapestry5.commons.internal.util.TapestryException; 021import org.apache.tapestry5.commons.services.TypeCoercer; 022import org.apache.tapestry5.commons.util.AvailableValues; 023import org.apache.tapestry5.commons.util.CollectionFactory; 024import org.apache.tapestry5.commons.util.ExceptionUtils; 025import org.apache.tapestry5.commons.util.UnknownValueException; 026import org.apache.tapestry5.internal.InternalComponentResources; 027import org.apache.tapestry5.internal.services.ComponentClassCache; 028import org.apache.tapestry5.ioc.internal.util.InternalUtils; 029import org.apache.tapestry5.model.ComponentModel; 030import org.apache.tapestry5.model.EmbeddedComponentModel; 031import org.apache.tapestry5.model.MutableComponentModel; 032import org.apache.tapestry5.plastic.*; 033import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2; 034import org.apache.tapestry5.services.transform.TransformationSupport; 035 036import java.util.List; 037 038/** 039 * Responsible for identifying, via the {@link org.apache.tapestry5.annotations.BindParameter} annotation, mixin fields 040 * that should be bound to a core-component parameter value. 041 * 042 * @since 5.2.0 043 */ 044public class BindParameterWorker implements ComponentClassTransformWorker2 045{ 046 private final class BoundParameterFieldValueConduit implements FieldConduit<Object> 047 { 048 private final String containerParameterName; 049 050 private final InternalComponentResources containerResources; 051 052 private final Class fieldType; 053 054 // Guarded by this 055 private ParameterConduit conduit; 056 057 private BoundParameterFieldValueConduit(String containerParameterName, 058 InternalComponentResources containerResources, Class fieldType) 059 { 060 this.containerParameterName = containerParameterName; 061 this.containerResources = containerResources; 062 this.fieldType = fieldType; 063 } 064 065 /** 066 * Defer obtaining the conduit object until needed, to deal with the complex 067 * lifecycle of 068 * parameters. Perhaps this can be addressed by converting constructors into 069 * methods invoked 070 * from the page loaded lifecycle method? 071 */ 072 private synchronized ParameterConduit getParameterConduit() 073 { 074 if (conduit == null) 075 { 076 // if the parameter is not a formal parameter then it must be a published parameter 077 if (containerResources.getComponentModel().isFormalParameter(containerParameterName)) 078 conduit = containerResources.getParameterConduit(containerParameterName); 079 else 080 conduit = getEmbeddedComponentResourcesForPublishedParameter(containerResources, containerParameterName) 081 .getParameterConduit(containerParameterName); 082 } 083 084 return conduit; 085 } 086 087 088 public Object get(Object instance, InstanceContext context) 089 { 090 // For the moment, this results in two passes through the TypeCoercer; we'll look 091 // to optimize that in the future. The first pass is deep inside ParameterConduit (coercing 092 // to the component parameter field type), the second is here (usually the same type so no 093 // real coercion necessary). 094 095 Object result = getParameterConduit().get(instance, context); 096 097 return typeCoercer.coerce(result, fieldType); 098 } 099 100 public void set(Object instance, InstanceContext context, Object newValue) 101 { 102 getParameterConduit().set(instance, context, newValue); 103 104 } 105 } 106 107 private final TypeCoercer typeCoercer; 108 109 private final ComponentClassCache componentClassCache; 110 111 public BindParameterWorker(TypeCoercer typeCoercer, ComponentClassCache componentClassCache) 112 { 113 this.typeCoercer = typeCoercer; 114 this.componentClassCache = componentClassCache; 115 } 116 117 public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model) 118 { 119 for (PlasticField field : plasticClass.getFieldsWithAnnotation(BindParameter.class)) 120 { 121 convertFieldIntoContainerBoundParameter(field); 122 } 123 } 124 125 private void convertFieldIntoContainerBoundParameter(PlasticField field) 126 { 127 BindParameter annotation = field.getAnnotation(BindParameter.class); 128 129 field.claim(annotation); 130 131 final String[] possibleNames = annotation.value(); 132 133 final String fieldTypeName = field.getTypeName(); 134 135 final String fieldName = field.getName(); 136 137 ComputedValue<FieldConduit<Object>> computedConduit = new ComputedValue<FieldConduit<Object>>() 138 { 139 public FieldConduit<Object> get(InstanceContext context) 140 { 141 ComponentResources resources = context.get(ComponentResources.class); 142 143 try 144 { 145 return createConduit(resources, fieldTypeName, fieldName, possibleNames); 146 } catch (Exception ex) 147 { 148 throw new TapestryException(String.format( 149 "Failure binding parameter field '%s' of mixin %s (type %s): %s", fieldName, resources 150 .getCompleteId(), resources.getComponentModel().getComponentClassName(), 151 ExceptionUtils.toMessage(ex)), ex); 152 } 153 } 154 155 }; 156 157 field.setComputedConduit(computedConduit); 158 } 159 160 private FieldConduit<Object> createConduit(final ComponentResources resources, final String fieldTypeName, 161 final String fieldName, final String[] possibleNames) 162 { 163 if (!resources.isMixin()) 164 throw new TapestryException(String.format("@BindParameter was used on field '%s' of component class '%s', but @BindParameter should only be used in mixins.", fieldName, resources.getComponentModel() 165 .getComponentClassName()), null); 166 167 InternalComponentResources containerResources = (InternalComponentResources) resources.getContainerResources(); 168 169 // Evaluate this early so that we get a fast fail. 170 171 String containerParameterName = identifyParameterName(resources, InternalUtils.stripMemberName(fieldName), 172 possibleNames); 173 174 Class fieldType = componentClassCache.forName(fieldTypeName); 175 176 return new BoundParameterFieldValueConduit(containerParameterName, containerResources, fieldType); 177 } 178 179 private String identifyParameterName(ComponentResources resources, String firstGuess, String... otherGuesses) 180 { 181 ComponentModel model = resources.getContainerResources().getComponentModel(); 182 183 List<String> guesses = CollectionFactory.newList(); 184 guesses.add(firstGuess); 185 186 for (String name : otherGuesses) 187 { 188 guesses.add(name); 189 } 190 191 for (String name : guesses) 192 { 193 if (model.isFormalParameter(name)) 194 return name; 195 196 if (isPublishedParameter(model, name)) 197 return name; 198 } 199 200 String message = String.format("Containing component %s does not contain a formal parameter or a published parameter %s %s.", 201 202 model.getComponentClassName(), 203 204 guesses.size() == 1 ? "matching" : "matching any of", 205 206 InternalUtils.joinSorted(guesses)); 207 208 List<String> formalAndPublishedParameters = CollectionFactory.newList(model.getParameterNames()); 209 formalAndPublishedParameters.addAll(getPublishedParameters(model)); 210 211 throw new UnknownValueException(message, new AvailableValues("Formal and published parameters", formalAndPublishedParameters)); 212 } 213 214 /** 215 * Returns true if the parameter with the given parameterName is a published parameter 216 * of any of the embedded components for the component with the given model. 217 */ 218 private boolean isPublishedParameter(ComponentModel model, String parameterName) 219 { 220 for (String embeddedComponentId : model.getEmbeddedComponentIds()) 221 { 222 EmbeddedComponentModel embeddedComponentModel = model 223 .getEmbeddedComponentModel(embeddedComponentId); 224 if (embeddedComponentModel.getPublishedParameters().contains(parameterName)) return true; 225 } 226 227 return false; 228 } 229 230 private List<String> getPublishedParameters(ComponentModel model) 231 { 232 List<String> publishedParameters = CollectionFactory.newList(); 233 for (String embeddedComponentId : model.getEmbeddedComponentIds()) 234 { 235 EmbeddedComponentModel embeddedComponentModel = model.getEmbeddedComponentModel(embeddedComponentId); 236 publishedParameters.addAll(embeddedComponentModel.getPublishedParameters()); 237 } 238 return publishedParameters; 239 } 240 241 /** 242 * Returns the {@link InternalComponentResources} of an embeddedComponent that contains the published parameter 243 * publishedParameterName. This is basically a recursive search for published parameters. 244 */ 245 private InternalComponentResources getEmbeddedComponentResourcesForPublishedParameter(InternalComponentResources containerResources, 246 String publishedParameterName) 247 { 248 List<InternalComponentResources> embeddedComponentResourcesList = CollectionFactory.newList(); 249 250 embeddedComponentResourcesList.add(containerResources); 251 252 while (!embeddedComponentResourcesList.isEmpty()) 253 { 254 InternalComponentResources resources = embeddedComponentResourcesList.remove(0); 255 256 ComponentModel containerComponentModel = resources.getComponentModel(); 257 258 for (String embeddedComponentId : containerComponentModel.getEmbeddedComponentIds()) 259 { 260 EmbeddedComponentModel embeddedComponentModel = containerComponentModel 261 .getEmbeddedComponentModel(embeddedComponentId); 262 263 InternalComponentResources embeddedComponentResources = (InternalComponentResources) resources 264 .getEmbeddedComponent(embeddedComponentId).getComponentResources(); 265 /** 266 * If the parameter is not a formal parameter, then the parameter must be a published parameter 267 * of an embeddedComponent of the component we are currently examining. 268 */ 269 if (embeddedComponentModel.getPublishedParameters().contains(publishedParameterName) 270 && embeddedComponentResources.getComponentModel().isFormalParameter(publishedParameterName)) 271 { 272 return embeddedComponentResources; 273 } 274 275 embeddedComponentResourcesList.add(embeddedComponentResources); 276 } 277 } 278 279 return null; 280 } 281} 282