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            String bindingCheckedName = bindingFieldName + "Checked";
159    
160            op.addField(bindingFieldName, IBinding.class);
161            op.addField(bindingCheckedName, Boolean.TYPE);
162    
163            body.addln("if (!{0})", bindingCheckedName);
164            body.begin();
165            body.addln("{0} = getBinding(\"{1}\");", bindingFieldName, parameterName);
166            body.addln("{0} = true;", bindingCheckedName);
167            body.end();
168    
169            body.addln("return {0};", bindingFieldName);
170    
171            body.end();
172    
173            String methodName = EnhanceUtils.createAccessorMethodName(bindingFieldName);
174    
175            op.addMethod(Modifier.PUBLIC,
176                         new MethodSignature(IBinding.class, methodName, new Class[0], null),
177                         body.toString(), location);
178    
179            return methodName + "()";
180        }
181    
182        void extendCleanupAfterRender(EnhancementOperation op, String bindingFieldName,
183                                      String fieldName, String defaultFieldName, String cachedFieldName)
184        {
185            BodyBuilder cleanupBody = new BodyBuilder();
186    
187            // Cached is only set when the field is updated in the accessor or
188            // mutator.
189            // After rendering, we want to clear the cached value and cached flag
190            // unless the binding is invariant, in which case it can stick around
191            // for some future render.
192    
193            cleanupBody.addln("if ({0} && ! {1}.isInvariant())", cachedFieldName, bindingFieldName);
194            cleanupBody.begin();
195            cleanupBody.addln("{0} = false;", cachedFieldName);
196            cleanupBody.addln("{0} = {1};", fieldName, defaultFieldName);
197            cleanupBody.end();
198    
199            op.extendMethodImplementation(IComponent.class,
200                                          EnhanceUtils.CLEANUP_AFTER_RENDER_SIGNATURE, cleanupBody.toString());
201        }
202    
203        private void buildMutator(EnhancementOperation op, String parameterName,
204                                  String propertyName, Class propertyType, String fieldName,
205                                  String defaultFieldName, String cachedFieldName,
206                                  String bindingFieldName, Location location)
207        {
208            BodyBuilder builder = new BodyBuilder();
209            builder.begin();
210    
211            // The mutator method may be invoked from finishLoad(), in which
212            // case it changes the default value for the parameter property, if the
213            // parameter
214            // is not bound.
215    
216            builder.addln("if (! isInActiveState())");
217            builder.begin();
218            builder.addln("{0} = $1;", defaultFieldName);
219            builder.addln("return;");
220            builder.end();
221    
222            // In the normal state, we update the binding first - and it's an error
223            // if the parameter is not bound.
224    
225            builder.addln("if ({0} == null)", bindingFieldName);
226            builder.addln("  throw new {0}(\"Parameter ''{1}'' is not bound and can not be updated.\");",
227                          ApplicationRuntimeException.class.getName(), parameterName);
228    
229            // Always updated the binding first (which may fail with an exception).
230    
231            builder.addln("{0}.setObject(($w) $1);", bindingFieldName);
232    
233            // While rendering, we store the updated value for fast
234            // access again (while the component is still rendering).
235            // The property value will be reset to default by cleanupAfterRender().
236    
237            builder.addln("if (isRendering())");
238            builder.begin();
239            builder.addln("{0} = $1;", fieldName);
240            builder.addln("{0} = true;", cachedFieldName);
241            builder.end();
242    
243            builder.end();
244    
245            String mutatorMethodName = EnhanceUtils.createMutatorMethodName(propertyName);
246    
247            op.addMethod(Modifier.PUBLIC,
248                         new MethodSignature(void.class, mutatorMethodName, new Class[] { propertyType }, null),
249                         builder.toString(), location);
250        }
251    
252        // Package private for testing
253    
254        void buildAccessor(EnhancementOperation op, String propertyName, Class propertyType,
255                           String fieldName, String defaultFieldName, String cachedFieldName,
256                           String bindingFieldName, boolean cache, Location location)
257        {
258            BodyBuilder builder = new BodyBuilder();
259            builder.begin();
260    
261            builder.addln("if ({0}) return {1};", cachedFieldName, fieldName);
262            builder.addln("if ({0} == null) return {1};", bindingFieldName, defaultFieldName);
263    
264            String javaTypeName = ClassFabUtils.getJavaClassName(propertyType);
265    
266            builder.addln("{0} result = {1};", javaTypeName, EnhanceUtils.createUnwrapExpression(op, bindingFieldName, propertyType));
267    
268            // Values read via the binding are cached during the render of
269            // the component (if the parameter defines cache to be true, which
270            // is the default), or any time the binding is invariant
271            // (such as most bindings besides ExpressionBinding.
272    
273            String expression = cache ? "isRendering() || {0}.isInvariant()" : "{0}.isInvariant()";
274    
275            builder.addln("if (" + expression + ")", bindingFieldName);
276            builder.begin();
277            builder.addln("{0} = result;", fieldName);
278            builder.addln("{0} = true;", cachedFieldName);
279            builder.end();
280    
281            builder.addln("return result;");
282    
283            builder.end();
284    
285            String accessorMethodName = op.getAccessorMethodName(propertyName);
286    
287            op.addMethod(Modifier.PUBLIC, new MethodSignature(propertyType,
288                                                              accessorMethodName, null, null), builder.toString(), location);
289        }
290    
291        public void setErrorLog(ErrorLog errorLog)
292        {
293            _errorLog = errorLog;
294        }
295    }