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.services.impl;
016    
017    import org.apache.commons.logging.Log;
018    import org.apache.hivemind.ApplicationRuntimeException;
019    import org.apache.hivemind.Location;
020    import org.apache.tapestry.*;
021    import org.apache.tapestry.binding.BindingConstants;
022    import org.apache.tapestry.binding.BindingSource;
023    import org.apache.tapestry.binding.LiteralBinding;
024    import org.apache.tapestry.engine.IPageLoader;
025    import org.apache.tapestry.parse.*;
026    import org.apache.tapestry.services.TemplateSource;
027    import org.apache.tapestry.spec.IComponentSpecification;
028    import org.apache.tapestry.spec.IContainedComponent;
029    import org.apache.tapestry.spec.IParameterSpecification;
030    
031    import java.util.HashSet;
032    import java.util.Iterator;
033    import java.util.Map;
034    import java.util.Set;
035    
036    /**
037     * Contains the logic from {@link org.apache.tapestry.services.impl.ComponentTemplateLoaderImpl},
038     * which creates one of these instances to process the request. This is necessary because the
039     * service must be re-entrant (because templates can contain components that have templates).
040     *
041     */
042    public class ComponentTemplateLoaderLogic
043    {
044        private Log _log;
045    
046        private IPageLoader _pageLoader;
047    
048        private IRequestCycle _requestCycle;
049    
050        private ITemplateComponent _loadComponent;
051    
052        private BindingSource _bindingSource;
053    
054        private IComponent[] _stack;
055    
056        private int _stackx;
057    
058        private IComponent _activeComponent = null;
059    
060        private Set _seenIds = new HashSet();
061    
062        public ComponentTemplateLoaderLogic(Log log, IPageLoader pageLoader, BindingSource bindingSource)
063        {
064            _log = log;
065            _pageLoader = pageLoader;
066            _bindingSource = bindingSource;
067        }
068    
069        public void loadTemplate(IRequestCycle requestCycle, ITemplateComponent loadComponent,
070                                 ComponentTemplate template)
071        {
072            _requestCycle = requestCycle;
073            _loadComponent = loadComponent;
074    
075            process(template);
076        }
077    
078        private void process(ComponentTemplate template)
079        {
080            int count = template.getTokenCount();
081    
082            _stack = new IComponent[count];
083    
084            for (int i = 0; i < count; i++)
085            {
086                TemplateToken token = template.getToken(i);
087    
088                TokenType type = token.getType();
089    
090                if (type == TokenType.TEXT)
091                {
092                    process((TextToken) token);
093                    continue;
094                }
095    
096                if (type == TokenType.OPEN)
097                {
098                    process((OpenToken) token);
099                    continue;
100                }
101    
102                if (type == TokenType.CLOSE)
103                {
104                    process((CloseToken) token);
105                    continue;
106                }
107    
108                if (type == TokenType.LOCALIZATION)
109                {
110                    process((LocalizationToken) token);
111                }
112            }
113    
114            // This is also pretty much unreachable, and the message is kind of out
115            // of date, too.
116    
117            if (_stackx != 0)
118                throw new ApplicationRuntimeException(Tapestry.getMessage("BaseComponent.unbalance-open-tags"),
119                                                      _loadComponent, null, null);
120    
121            checkAllComponentsReferenced();
122        }
123    
124        /**
125         * Adds the token (which implements {@link IRender}) to the active component (using
126         * {@link IComponent#addBody(IRender)}), or to this component
127         * {@link org.apache.tapestry.BaseComponent#addOuter(IRender)}.
128         * <p>
129         * A check is made that the active component allows a body.
130         */
131    
132        private void process(TextToken token)
133        {
134            if (_activeComponent == null)
135            {
136                _loadComponent.addOuter(token);
137                return;
138            }
139    
140            if (!_activeComponent.getSpecification().getAllowBody())
141                throw createBodylessComponentException(_activeComponent);
142    
143            _activeComponent.addBody(token);
144        }
145    
146        private void process(OpenToken token)
147        {
148            String id = token.getId();
149            IComponent component = null;
150            String componentType = token.getComponentType();
151    
152            if (componentType == null)
153                component = getEmbeddedComponent(id);
154            else
155            {
156                checkForDuplicateId(id, token.getLocation());
157    
158                component = createImplicitComponent(id, componentType, token.getLocation());
159            }
160    
161            // Make sure the template contains each component only once.
162    
163            if (_seenIds.contains(id))
164                throw new ApplicationRuntimeException(ImplMessages.multipleComponentReferences(_loadComponent,id),
165                                                      _loadComponent, token.getLocation(), null);
166    
167            _seenIds.add(id);
168    
169            if (_activeComponent == null)
170                _loadComponent.addOuter(component);
171            else
172            {
173                // Note: this code may no longer be reachable (because the
174                // template parser does this check first).
175    
176                if (!_activeComponent.getSpecification().getAllowBody())
177                    throw createBodylessComponentException(_activeComponent);
178    
179                _activeComponent.addBody(component);
180            }
181    
182            addTemplateBindings(component, token);
183    
184            _stack[_stackx++] = _activeComponent;
185    
186            _activeComponent = component;
187        }
188    
189        private void checkForDuplicateId(String id, Location location)
190        {
191            if (id == null)
192                return;
193    
194            IContainedComponent cc = _loadComponent.getSpecification().getComponent(id);
195    
196            if (cc != null)
197                throw new ApplicationRuntimeException(ImplMessages.dupeComponentId(id, cc),
198                                                      _loadComponent, location, null);
199        }
200    
201        private IComponent createImplicitComponent(String id, String componentType, Location location)
202        {
203            return _pageLoader.createImplicitComponent(
204                    _requestCycle,
205                    _loadComponent,
206                    id,
207                    componentType,
208                    location);
209        }
210    
211        private IComponent getEmbeddedComponent(String id)
212        {
213            return _loadComponent.getComponent(id);
214        }
215    
216        private void process(CloseToken token)
217        {
218            // Again, this is pretty much impossible to reach because
219            // the template parser does a great job.
220    
221            if (_stackx <= 0)
222                throw new ApplicationRuntimeException(ImplMessages.unbalancedCloseTags(),
223                                                      _loadComponent, token.getLocation(), null);
224    
225            // Null and forget the top element on the stack.
226    
227            _stack[_stackx--] = null;
228    
229            _activeComponent = _stack[_stackx];
230        }
231    
232        private void process(LocalizationToken token)
233        {
234            IRender render = new LocalizedStringRender(_loadComponent, token);
235    
236            if (_activeComponent == null)
237                _loadComponent.addOuter(render);
238            else
239                _activeComponent.addBody(render);
240        }
241    
242        /**
243         * Adds bindings based on attributes in the template.
244         */
245    
246        void addTemplateBindings(IComponent component, OpenToken token)
247        {
248            // sets the html tag name used to specify the component
249    
250            component.setTemplateTagName(token.getTag());
251    
252            IComponentSpecification spec = component.getSpecification();
253    
254            Map attributes = token.getAttributesMap();
255    
256            if (attributes != null)
257            {
258                Iterator i = attributes.entrySet().iterator();
259    
260                while (i.hasNext())
261                {
262                    Map.Entry entry = (Map.Entry) i.next();
263    
264                    String attributeName = (String) entry.getKey();
265                    String value = (String) entry.getValue();
266    
267                    IParameterSpecification pspec = spec.getParameter(attributeName);
268                    String parameterName = pspec == null ? attributeName : pspec.getParameterName();
269    
270                    if (!attributeName.equals(parameterName))
271                        _log.warn(ImplMessages.usedTemplateParameterAlias(token, attributeName, parameterName));
272    
273                    String description = ImplMessages.templateParameterName(parameterName);
274    
275                    // Values in a template are always literal, unless prefixed.
276    
277                    IBinding binding = _bindingSource.createBinding(
278                            _loadComponent,
279                            pspec,
280                            description,
281                            value,
282                            BindingConstants.LITERAL_PREFIX,
283                            token.getLocation());
284    
285                    addBinding(component, spec, parameterName, binding);
286                }
287            }
288    
289            // if the component defines a templateTag parameter and
290            // there is no established binding for that parameter,
291            // add a static binding carrying the template tag
292    
293            if (spec.getParameter(TemplateSource.TEMPLATE_TAG_PARAMETER_NAME) != null
294                && component.getBinding(TemplateSource.TEMPLATE_TAG_PARAMETER_NAME) == null)
295            {
296                IBinding binding = _bindingSource.createBinding(
297                        component,
298                        TemplateSource.TEMPLATE_TAG_PARAMETER_NAME,
299                        token.getTag(),
300                        BindingConstants.LITERAL_PREFIX,
301                        token.getLocation());
302    
303                addBinding(component, spec, TemplateSource.TEMPLATE_TAG_PARAMETER_NAME, binding);
304            }
305        }
306    
307        /**
308         * Adds an expression binding, checking for errors related to reserved and informal parameters.
309         * <p>
310         * It is an error to specify expression bindings in both the specification and the template.
311         */
312    
313        private void addBinding(IComponent component, IComponentSpecification spec,
314                                String name, IBinding binding)
315        {
316    
317            // If matches a formal parameter name, allow it to be set
318            // unless there's already a binding.
319    
320            boolean valid = validate(component, spec, name, binding);
321    
322            if (valid)
323                component.setBinding(name, binding);
324        }
325    
326        private boolean validate(IComponent component, IComponentSpecification spec,
327                                 String name, IBinding binding)
328        {
329            // TODO: This is ugly! Need a better/smarter way, even if we have to extend BindingSource
330            // to tell us.
331    
332            boolean isLiteral = binding instanceof LiteralBinding;
333            boolean isBound = component.getBinding(name) != null;
334            boolean isFormal = spec.getParameter(name) != null;
335    
336            if (!isFormal)
337            {
338                if (!spec.getAllowInformalParameters())
339                {
340                    // Again; if informal parameters are disallowed, ignore literal bindings, as they
341                    // are there as placeholders or for WYSIWYG.
342    
343                    if (isLiteral)
344                        return false;
345    
346                    throw new ApplicationRuntimeException(ImplMessages.templateBindingForInformalParameter(_loadComponent, name, component),
347                                                          component, binding.getLocation(), null);
348                }
349    
350                // If the name is reserved (matches a formal parameter
351                // or reserved name, caselessly), then skip it.
352    
353                if (spec.isReservedParameterName(name))
354                {
355                    // Final case for literals: if they conflict with a reserved name, they are ignored.
356                    // Again, there for WYSIWYG.
357    
358                    if (isLiteral)
359                        return false;
360    
361                    throw new ApplicationRuntimeException(ImplMessages.templateBindingForReservedParameter(_loadComponent, name, component),
362                                                          component, binding.getLocation(), null);
363                }
364            }
365    
366            // So, at this point it doesn't matter if the parameter is a formal parameter or
367            // an informal parameter. The binding (if any) in the specification takes precendence
368            // over the template. Literal bindings that conflict are considered to be there for WYSIWYG
369            // purposes. Non-literal bindings that conflict with a specification binding are an
370            // error.
371    
372            if (isBound)
373            {
374                // Literal bindings in the template that conflict with bound parameters
375                // from the spec are silently ignored.
376    
377                if (isLiteral)
378                    return false;
379    
380                throw new ApplicationRuntimeException(ImplMessages.dupeTemplateBinding(
381                        name,
382                        component,
383                        _loadComponent), component, binding.getLocation(), null);
384            }
385    
386            return true;
387    
388        }
389    
390        private void checkAllComponentsReferenced()
391        {
392            // First, contruct a modifiable copy of the ids of all expected components
393            // (that is, components declared in the specification).
394    
395            Map components = _loadComponent.getComponents();
396    
397            Set ids = components.keySet();
398    
399            // If the seen ids ... ids referenced in the template, matches
400            // all the ids in the specification then we're fine.
401    
402            if (_seenIds.containsAll(ids))
403                return;
404    
405            // Create a modifiable copy. Remove the ids that are referenced in
406            // the template. The remainder are worthy of note.
407    
408            ids = new HashSet(ids);
409            ids.removeAll(_seenIds);
410    
411            _log.warn(ImplMessages.missingComponentSpec(_loadComponent, ids));
412    
413        }
414    
415        private ApplicationRuntimeException createBodylessComponentException(IComponent component)
416        {
417            return new ApplicationRuntimeException(ImplMessages.bodylessComponent(), component, null, null);
418        }
419    }