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