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