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 java.lang.reflect.Modifier;
018    import java.util.Iterator;
019    
020    import org.apache.hivemind.ApplicationRuntimeException;
021    import org.apache.hivemind.ErrorLog;
022    import org.apache.hivemind.Location;
023    import org.apache.hivemind.service.BodyBuilder;
024    import org.apache.hivemind.service.ClassFabUtils;
025    import org.apache.hivemind.service.MethodSignature;
026    import org.apache.hivemind.util.Defense;
027    import org.apache.tapestry.IBinding;
028    import org.apache.tapestry.IComponent;
029    import org.apache.tapestry.spec.IComponentSpecification;
030    import 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     */
038    public 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    }