001    // Copyright 2004, 2005 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.tapestry.enhance;
016    
017    import org.apache.hivemind.ApplicationRuntimeException;
018    import org.apache.hivemind.ErrorLog;
019    import org.apache.hivemind.Location;
020    import org.apache.hivemind.service.BodyBuilder;
021    import org.apache.hivemind.service.ClassFabUtils;
022    import org.apache.hivemind.service.MethodSignature;
023    import org.apache.hivemind.util.Defense;
024    import org.apache.tapestry.IBinding;
025    import org.apache.tapestry.IComponent;
026    import org.apache.tapestry.spec.IComponentSpecification;
027    import org.apache.tapestry.spec.IParameterSpecification;
028    
029    import java.lang.reflect.Modifier;
030    import java.util.Iterator;
031    
032    /**
033     * Responsible for creating properties for connected parameters.
034     *
035     * @author Howard M. Lewis Ship
036     * @since 4.0
037     */
038    public class ParameterPropertyWorker implements EnhancementWorker
039    {
040    
041        private ErrorLog _errorLog;
042    
043        public void performEnhancement(EnhancementOperation op, IComponentSpecification spec)
044        {
045            Iterator i = spec.getParameterNames().iterator();
046            while(i.hasNext())
047            {
048                String name = (String) i.next();
049    
050                IParameterSpecification ps = spec.getParameter(name);
051    
052                try
053                {
054                    performEnhancement(op, name, ps);
055                }
056                catch (RuntimeException ex)
057                {
058                    _errorLog.error(EnhanceMessages.errorAddingProperty(ps.getPropertyName(), op.getBaseClass(), ex),
059                                    ps.getLocation(), ex);
060                }
061            }
062        }
063    
064        /**
065         * Performs the enhancement for a single parameter; this is about to change
066         * radically in release 4.0 but for the moment we're emulating 3.0 behavior.
067         *
068         * @param op
069         *          Enhancement operation service.
070         * @param parameterName
071         *          Name of the parameter being enhanced.
072         * @param ps
073         *          Specification of parameter.
074         */
075    
076        private void performEnhancement(EnhancementOperation op, String parameterName, IParameterSpecification ps)
077        {
078            // If the parameter name doesn't match, its because this is an alias
079            // for a true parameter; we ignore aliases.
080    
081            if (!parameterName.equals(ps.getParameterName()))
082                return;
083    
084            String propertyName = ps.getPropertyName();
085            String specifiedType = ps.getType();
086            boolean cache = ps.getCache();
087    
088            addParameter(op, parameterName, propertyName, specifiedType, cache, ps.getLocation());
089        }
090    
091        /**
092         * Adds a parameter as a (very smart) property.
093         *
094         * @param op
095         *            the enhancement operation
096         * @param parameterName
097         *            the name of the parameter (used to access the binding)
098         * @param propertyName
099         *            the name of the property to create (usually, but not always,
100         *            matches the parameterName)
101         * @param specifiedType
102         *            the type declared in the DTD (only 3.0 DTD supports this), may
103         *            be null (always null for 4.0 DTD)
104         * @param cache
105         *            if true, then the value should be cached while the component
106         *            renders; false (a much less common case) means that every
107         *            access will work through binding object.
108         * @param location
109         *            Used for reporting line-precise errors in binding resolution / setting / etc..
110         */
111    
112        public void addParameter(EnhancementOperation op, String parameterName,
113                                 String propertyName, String specifiedType, boolean cache,
114                                 Location location)
115        {
116            Defense.notNull(op, "op");
117            Defense.notNull(parameterName, "parameterName");
118            Defense.notNull(propertyName, "propertyName");
119    
120            Class propertyType = EnhanceUtils.extractPropertyType(op, propertyName, specifiedType);
121    
122            // 3.0 would allow connected parameter properties to be fully
123            // implemented
124            // in the component class. This is not supported in 4.0 and an existing
125            // property will be overwritten in the subclass.
126    
127            op.claimProperty(propertyName);
128    
129            // 3.0 used to support a property for the binding itself. That's
130            // no longer the case.
131    
132            String fieldName = "_$" + propertyName;
133            String defaultFieldName = fieldName + "$Default";
134            String cachedFieldName = fieldName + "$Cached";
135    
136            op.addField(fieldName, propertyType);
137            op.addField(defaultFieldName, propertyType);
138            op.addField(cachedFieldName, boolean.class);
139    
140            String bindingFieldName = buildBindingAccessor(op, fieldName, parameterName, location);
141    
142            buildAccessor(op, propertyName, propertyType, fieldName,
143                          defaultFieldName, cachedFieldName, bindingFieldName,
144                          cache, location);
145    
146            buildMutator(op, parameterName, propertyName, propertyType, fieldName,
147                         defaultFieldName, cachedFieldName, bindingFieldName, location);
148    
149            extendCleanupAfterRender(op, bindingFieldName, fieldName, defaultFieldName, cachedFieldName);
150        }
151    
152        String buildBindingAccessor(EnhancementOperation op, String fieldName, String parameterName, Location location)
153        {
154            BodyBuilder body = new BodyBuilder();
155            body.begin();
156            
157            String bindingFieldName = fieldName + "$Binding";
158    
159            op.addField(bindingFieldName, IBinding.class);
160    
161            body.addln("if ({0} == null)", bindingFieldName);
162            body.begin();
163            body.addln("{0} = getBinding(\"{1}\");", bindingFieldName, parameterName);
164            body.end();
165    
166            body.addln("return {0};", bindingFieldName);
167    
168            body.end();
169    
170            String methodName = EnhanceUtils.createAccessorMethodName(bindingFieldName);
171    
172            op.addMethod(Modifier.PUBLIC,
173                         new MethodSignature(IBinding.class, methodName, new Class[0], null),
174                         body.toString(), location);
175    
176            return methodName + "()";
177        }
178    
179        void extendCleanupAfterRender(EnhancementOperation op, String bindingFieldName,
180                                      String fieldName, String defaultFieldName, String cachedFieldName)
181        {
182            BodyBuilder cleanupBody = new BodyBuilder();
183    
184            // Cached is only set when the field is updated in the accessor or
185            // mutator.
186            // After rendering, we want to clear the cached value and cached flag
187            // unless the binding is invariant, in which case it can stick around
188            // for some future render.
189    
190            cleanupBody.addln("if ({0} && ! {1}.isInvariant())", cachedFieldName, bindingFieldName);
191            cleanupBody.begin();
192            cleanupBody.addln("{0} = false;", cachedFieldName);
193            cleanupBody.addln("{0} = {1};", fieldName, defaultFieldName);
194            cleanupBody.end();
195    
196            op.extendMethodImplementation(IComponent.class,
197                                          EnhanceUtils.CLEANUP_AFTER_RENDER_SIGNATURE, cleanupBody.toString());
198        }
199    
200        private void buildMutator(EnhancementOperation op, String parameterName,
201                                  String propertyName, Class propertyType, String fieldName,
202                                  String defaultFieldName, String cachedFieldName,
203                                  String bindingFieldName, Location location)
204        {
205            BodyBuilder builder = new BodyBuilder();
206            builder.begin();
207    
208            // The mutator method may be invoked from finishLoad(), in which
209            // case it changes the default value for the parameter property, if the
210            // parameter
211            // is not bound.
212    
213            builder.addln("if (! isInActiveState())");
214            builder.begin();
215            builder.addln("{0} = $1;", defaultFieldName);
216            builder.addln("return;");
217            builder.end();
218    
219            // In the normal state, we update the binding first - and it's an error
220            // if the parameter is not bound.
221    
222            builder.addln("if ({0} == null)", bindingFieldName);
223            builder.addln("  throw new {0}(\"Parameter ''{1}'' is not bound and can not be updated.\");",
224                          ApplicationRuntimeException.class.getName(), parameterName);
225    
226            // Always updated the binding first (which may fail with an exception).
227    
228            builder.addln("{0}.setObject(($w) $1);", bindingFieldName);
229    
230            // While rendering, we store the updated value for fast
231            // access again (while the component is still rendering).
232            // The property value will be reset to default by cleanupAfterRender().
233    
234            builder.addln("if (isRendering())");
235            builder.begin();
236            builder.addln("{0} = $1;", fieldName);
237            builder.addln("{0} = true;", cachedFieldName);
238            builder.end();
239    
240            builder.end();
241    
242            String mutatorMethodName = EnhanceUtils.createMutatorMethodName(propertyName);
243    
244            op.addMethod(Modifier.PUBLIC,
245                         new MethodSignature(void.class, mutatorMethodName, new Class[] { propertyType }, null),
246                         builder.toString(), location);
247        }
248    
249        // Package private for testing
250    
251        void buildAccessor(EnhancementOperation op, String propertyName, Class propertyType,
252                           String fieldName, String defaultFieldName, String cachedFieldName,
253                           String bindingFieldName, boolean cache, Location location)
254        {
255            BodyBuilder builder = new BodyBuilder();
256            builder.begin();
257    
258            builder.addln("if ({0}) return {1};", cachedFieldName, fieldName);
259            builder.addln("if ({0} == null) return {1};", bindingFieldName, defaultFieldName);
260    
261            String javaTypeName = ClassFabUtils.getJavaClassName(propertyType);
262    
263            builder.addln("{0} result = {1};", javaTypeName, EnhanceUtils.createUnwrapExpression(op, bindingFieldName, propertyType));
264    
265            // Values read via the binding are cached during the render of
266            // the component (if the parameter defines cache to be true, which
267            // is the default), or any time the binding is invariant
268            // (such as most bindings besides ExpressionBinding.
269    
270            String expression = cache ? "isRendering() || {0}.isInvariant()" : "{0}.isInvariant()";
271    
272            builder.addln("if (" + expression + ")", bindingFieldName);
273            builder.begin();
274            builder.addln("{0} = result;", fieldName);
275            builder.addln("{0} = true;", cachedFieldName);
276            builder.end();
277    
278            builder.addln("return result;");
279    
280            builder.end();
281    
282            String accessorMethodName = op.getAccessorMethodName(propertyName);
283    
284            op.addMethod(Modifier.PUBLIC, new MethodSignature(propertyType,
285                                                              accessorMethodName, null, null), builder.toString(), location);
286        }
287    
288        public void setErrorLog(ErrorLog errorLog)
289        {
290            _errorLog = errorLog;
291        }
292    }