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.services.impl;
016
017import java.util.HashSet;
018import java.util.Iterator;
019import java.util.Map;
020import java.util.Set;
021
022import org.apache.commons.logging.Log;
023import org.apache.hivemind.ApplicationRuntimeException;
024import org.apache.hivemind.Location;
025import org.apache.tapestry.IBinding;
026import org.apache.tapestry.IComponent;
027import org.apache.tapestry.IRender;
028import org.apache.tapestry.IRequestCycle;
029import org.apache.tapestry.ITemplateComponent;
030import org.apache.tapestry.Tapestry;
031import org.apache.tapestry.binding.BindingConstants;
032import org.apache.tapestry.binding.BindingSource;
033import org.apache.tapestry.binding.LiteralBinding;
034import org.apache.tapestry.engine.IPageLoader;
035import org.apache.tapestry.parse.CloseToken;
036import org.apache.tapestry.parse.ComponentTemplate;
037import org.apache.tapestry.parse.LocalizationToken;
038import org.apache.tapestry.parse.OpenToken;
039import org.apache.tapestry.parse.TemplateToken;
040import org.apache.tapestry.parse.TextToken;
041import org.apache.tapestry.parse.TokenType;
042import org.apache.tapestry.services.TemplateSource;
043import org.apache.tapestry.spec.IComponentSpecification;
044import org.apache.tapestry.spec.IContainedComponent;
045import 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 */
055public 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}