001// Copyright 2011 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.tapestry5.internal.dynamic;
016
017import org.apache.tapestry5.Binding;
018import org.apache.tapestry5.BindingConstants;
019import org.apache.tapestry5.Block;
020import org.apache.tapestry5.MarkupWriter;
021import org.apache.tapestry5.func.F;
022import org.apache.tapestry5.func.Flow;
023import org.apache.tapestry5.func.Mapper;
024import org.apache.tapestry5.func.Worker;
025import org.apache.tapestry5.internal.services.XMLTokenStream;
026import org.apache.tapestry5.internal.services.XMLTokenType;
027import org.apache.tapestry5.ioc.Location;
028import org.apache.tapestry5.ioc.Resource;
029import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
030import org.apache.tapestry5.ioc.internal.util.InternalUtils;
031import org.apache.tapestry5.ioc.internal.util.TapestryException;
032import org.apache.tapestry5.runtime.RenderCommand;
033import org.apache.tapestry5.runtime.RenderQueue;
034import org.apache.tapestry5.services.BindingSource;
035import org.apache.tapestry5.services.dynamic.DynamicDelegate;
036import org.apache.tapestry5.services.dynamic.DynamicTemplate;
037
038import javax.xml.namespace.QName;
039import java.net.URL;
040import java.util.List;
041import java.util.Map;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044
045/**
046 * Does the heavy lifting for {@link DynamicTemplateParserImpl}.
047 */
048public class DynamicTemplateSaxParser
049{
050    private final Resource resource;
051
052    private final BindingSource bindingSource;
053
054    private final XMLTokenStream tokenStream;
055
056    private static final Pattern PARAM_ID_PATTERN = Pattern.compile("^param:(\\p{Alpha}\\w*)$",
057            Pattern.CASE_INSENSITIVE);
058
059    private static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{\\s*(.*?)\\s*}");
060
061    private static final DynamicTemplateElement END = new DynamicTemplateElement()
062    {
063        public void render(MarkupWriter writer, RenderQueue queue, DynamicDelegate delegate)
064        {
065            // End the previously started element
066            writer.end();
067        }
068    };
069
070    public DynamicTemplateSaxParser(Resource resource, BindingSource bindingSource, Map<String, URL> publicIdToURL)
071    {
072        this.resource = resource;
073        this.bindingSource = bindingSource;
074
075        this.tokenStream = new XMLTokenStream(resource, publicIdToURL);
076    }
077
078    public DynamicTemplate parse()
079    {
080        try
081        {
082            tokenStream.parse();
083
084            return toDynamicTemplate(root());
085        } catch (Exception ex)
086        {
087            throw new TapestryException(String.format("Failure parsing dynamic template %s: %s", resource,
088                    InternalUtils.toMessage(ex)), tokenStream.getLocation(), ex);
089        }
090    }
091
092    // Note the use of static methods; otherwise the compiler sets this$0 to point to the DynamicTemplateSaxParser,
093    // creating an unwanted reference that keeps the parser from being GCed.
094
095    private static DynamicTemplate toDynamicTemplate(List<DynamicTemplateElement> elements)
096    {
097        final Flow<DynamicTemplateElement> flow = F.flow(elements).reverse();
098
099        return new DynamicTemplate()
100        {
101            public RenderCommand createRenderCommand(final DynamicDelegate delegate)
102            {
103                final Mapper<DynamicTemplateElement, RenderCommand> toRenderCommand = createToRenderCommandMapper(delegate);
104
105                return new RenderCommand()
106                {
107                    public void render(MarkupWriter writer, RenderQueue queue)
108                    {
109                        Worker<RenderCommand> pushOnQueue = createQueueRenderCommand(queue);
110
111                        flow.map(toRenderCommand).each(pushOnQueue);
112                    }
113                };
114            }
115        };
116    }
117
118    private List<DynamicTemplateElement> root()
119    {
120        List<DynamicTemplateElement> result = CollectionFactory.newList();
121
122        while (tokenStream.hasNext())
123        {
124            switch (tokenStream.next())
125            {
126                case START_ELEMENT:
127                    result.add(element());
128                    break;
129
130                case END_DOCUMENT:
131                    // Ignore it.
132                    break;
133
134                default:
135                    addTextContent(result);
136            }
137        }
138
139        return result;
140    }
141
142    private DynamicTemplateElement element()
143    {
144        String elementURI = tokenStream.getNamespaceURI();
145        String elementName = tokenStream.getLocalName();
146
147        String blockId = null;
148
149        int count = tokenStream.getAttributeCount();
150
151        List<DynamicTemplateAttribute> attributes = CollectionFactory.newList();
152
153        Location location = getLocation();
154
155        for (int i = 0; i < count; i++)
156        {
157            QName qname = tokenStream.getAttributeName(i);
158
159            // The name will be blank for an xmlns: attribute
160
161            String localName = qname.getLocalPart();
162
163            if (InternalUtils.isBlank(localName))
164                continue;
165
166            String uri = qname.getNamespaceURI();
167
168            String value = tokenStream.getAttributeValue(i);
169
170            if (localName.equals("id"))
171            {
172                Matcher matcher = PARAM_ID_PATTERN.matcher(value);
173
174                if (matcher.matches())
175                {
176                    blockId = matcher.group(1);
177                    continue;
178                }
179            }
180
181            Mapper<DynamicDelegate, String> attributeValueExtractor = createCompositeExtractorFromText(value, location);
182
183            attributes.add(new DynamicTemplateAttribute(uri, localName, attributeValueExtractor));
184        }
185
186        if (blockId != null)
187            return block(blockId);
188
189        List<DynamicTemplateElement> body = CollectionFactory.newList();
190
191        boolean atEnd = false;
192        while (!atEnd)
193        {
194            switch (tokenStream.next())
195            {
196                case START_ELEMENT:
197
198                    // Recurse into this new element
199                    body.add(element());
200
201                    break;
202
203                case END_ELEMENT:
204                    body.add(END);
205                    atEnd = true;
206
207                    break;
208
209                default:
210
211                    addTextContent(body);
212            }
213        }
214
215        return createElementWriterElement(elementURI, elementName, attributes, body);
216    }
217
218    private static DynamicTemplateElement createElementWriterElement(final String elementURI, final String elementName,
219                                                                     final List<DynamicTemplateAttribute> attributes, List<DynamicTemplateElement> body)
220    {
221        final Flow<DynamicTemplateElement> bodyFlow = F.flow(body).reverse();
222
223        return new DynamicTemplateElement()
224        {
225            public void render(MarkupWriter writer, RenderQueue queue, DynamicDelegate delegate)
226            {
227                // Write the element ...
228
229                writer.elementNS(elementURI, elementName);
230
231                // ... and the attributes
232
233                for (DynamicTemplateAttribute attribute : attributes)
234                {
235                    attribute.write(writer, delegate);
236                }
237
238                // And convert the DTEs for the direct children of this element into RenderCommands and push them onto
239                // the queue. This includes the child that will end the started element.
240
241                Mapper<DynamicTemplateElement, RenderCommand> toRenderCommand = createToRenderCommandMapper(delegate);
242                Worker<RenderCommand> pushOnQueue = createQueueRenderCommand(queue);
243
244                bodyFlow.map(toRenderCommand).each(pushOnQueue);
245            }
246        };
247    }
248
249    private DynamicTemplateElement block(final String blockId)
250    {
251        Location location = getLocation();
252
253        removeContent();
254
255        return createBlockElement(blockId, location);
256    }
257
258    private static DynamicTemplateElement createBlockElement(final String blockId, final Location location)
259    {
260        return new DynamicTemplateElement()
261        {
262            public void render(MarkupWriter writer, RenderQueue queue, DynamicDelegate delegate)
263            {
264                try
265                {
266                    Block block = delegate.getBlock(blockId);
267
268                    queue.push((RenderCommand) block);
269                } catch (Exception ex)
270                {
271                    throw new TapestryException(String.format(
272                            "Exception rendering block '%s' as part of dynamic template: %s", blockId,
273                            InternalUtils.toMessage(ex)), location, ex);
274                }
275            }
276        };
277    }
278
279    private Location getLocation()
280    {
281        return tokenStream.getLocation();
282    }
283
284    private void removeContent()
285    {
286        int depth = 1;
287
288        while (true)
289        {
290            switch (tokenStream.next())
291            {
292                case START_ELEMENT:
293                    depth++;
294                    break;
295
296                // The matching end element.
297
298                case END_ELEMENT:
299                    depth--;
300
301                    if (depth == 0)
302                        return;
303
304                    break;
305
306                default:
307                    // Ignore anything else (text, comments, etc.)
308            }
309        }
310    }
311
312    void addTextContent(List<DynamicTemplateElement> elements)
313    {
314        switch (tokenStream.getEventType())
315        {
316            case COMMENT:
317                elements.add(comment());
318                break;
319
320            case CHARACTERS:
321            case SPACE:
322                addTokensForText(elements);
323                break;
324
325            default:
326                unexpectedEventType();
327        }
328    }
329
330    private void addTokensForText(List<DynamicTemplateElement> elements)
331    {
332        Mapper<DynamicDelegate, String> composite = createCompositeExtractorFromText(tokenStream.getText(),
333                tokenStream.getLocation());
334
335        elements.add(createTextWriterElement(composite));
336    }
337
338    private static DynamicTemplateElement createTextWriterElement(final Mapper<DynamicDelegate, String> composite)
339    {
340        return new DynamicTemplateElement()
341        {
342            public void render(MarkupWriter writer, RenderQueue queue, DynamicDelegate delegate)
343            {
344                String value = composite.map(delegate);
345
346                writer.write(value);
347            }
348        };
349    }
350
351    private Mapper<DynamicDelegate, String> createCompositeExtractorFromText(String text, Location location)
352    {
353        Matcher matcher = EXPANSION_PATTERN.matcher(text);
354
355        List<Mapper<DynamicDelegate, String>> extractors = CollectionFactory.newList();
356
357        int startx = 0;
358
359        while (matcher.find())
360        {
361            int matchStart = matcher.start();
362
363            if (matchStart != startx)
364            {
365                String prefix = text.substring(startx, matchStart);
366
367                extractors.add(createTextExtractor(prefix));
368            }
369
370            // Group 1 includes the real text of the expansion, with whitespace
371            // around the
372            // expression (but inside the curly braces) excluded.
373
374            String expression = matcher.group(1);
375
376            extractors.add(createExpansionExtractor(expression, location, bindingSource));
377
378            startx = matcher.end();
379        }
380
381        // Catch anything after the final regexp match.
382
383        if (startx < text.length())
384            extractors.add(createTextExtractor(text.substring(startx, text.length())));
385
386        if (extractors.size() == 1)
387            return extractors.get(0);
388
389        return creatCompositeExtractor(extractors);
390    }
391
392    private static Mapper<DynamicDelegate, String> creatCompositeExtractor(
393            final List<Mapper<DynamicDelegate, String>> extractors)
394    {
395        return new Mapper<DynamicDelegate, String>()
396        {
397            public String map(final DynamicDelegate delegate)
398            {
399                StringBuilder builder = new StringBuilder();
400
401                for (Mapper<DynamicDelegate, String> extractor : extractors)
402                {
403                    String value = extractor.map(delegate);
404
405                    if (value != null)
406                        builder.append(value);
407                }
408
409                return builder.toString();
410            }
411        };
412    }
413
414    private DynamicTemplateElement comment()
415    {
416        return createCommentElement(tokenStream.getText());
417    }
418
419    private static DynamicTemplateElement createCommentElement(final String content)
420    {
421        return new DynamicTemplateElement()
422        {
423            public void render(MarkupWriter writer, RenderQueue queue, DynamicDelegate delegate)
424            {
425                writer.comment(content);
426            }
427        };
428    }
429
430    private static Mapper<DynamicDelegate, String> createTextExtractor(final String content)
431    {
432        return new Mapper<DynamicDelegate, String>()
433        {
434            public String map(DynamicDelegate delegate)
435            {
436                return content;
437            }
438        };
439    }
440
441    private static Mapper<DynamicDelegate, String> createExpansionExtractor(final String expression,
442                                                                            final Location location, final BindingSource bindingSource)
443    {
444        return new Mapper<DynamicDelegate, String>()
445        {
446            public String map(DynamicDelegate delegate)
447            {
448                try
449                {
450                    Binding binding = bindingSource.newBinding("dynamic template binding", delegate
451                            .getComponentResources().getContainerResources(), delegate.getComponentResources(),
452                            BindingConstants.PROP, expression, location);
453
454                    Object boundValue = binding.get();
455
456                    return boundValue == null ? null : boundValue.toString();
457                } catch (Throwable t)
458                {
459                    throw new TapestryException(InternalUtils.toMessage(t), location, t);
460                }
461            }
462        };
463    }
464
465    private <T> T unexpectedEventType()
466    {
467        XMLTokenType eventType = tokenStream.getEventType();
468
469        throw new IllegalStateException(String.format("Unexpected XML parse event %s.", eventType.name()));
470    }
471
472    private static Worker<RenderCommand> createQueueRenderCommand(final RenderQueue queue)
473    {
474        return new Worker<RenderCommand>()
475        {
476            public void work(RenderCommand value)
477            {
478                queue.push(value);
479            }
480        };
481    }
482
483    private static RenderCommand toRenderCommand(final DynamicTemplateElement value, final DynamicDelegate delegate)
484    {
485        return new RenderCommand()
486        {
487            public void render(MarkupWriter writer, RenderQueue queue)
488            {
489                value.render(writer, queue, delegate);
490            }
491        };
492    }
493
494    private static Mapper<DynamicTemplateElement, RenderCommand> createToRenderCommandMapper(
495            final DynamicDelegate delegate)
496    {
497        return new Mapper<DynamicTemplateElement, RenderCommand>()
498        {
499            public RenderCommand map(final DynamicTemplateElement value)
500            {
501                return toRenderCommand(value, delegate);
502            }
503        };
504    }
505}