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