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