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    
015    package org.apache.tapestry5.internal.dynamic;
016    
017    import org.apache.tapestry5.Binding;
018    import org.apache.tapestry5.BindingConstants;
019    import org.apache.tapestry5.Block;
020    import org.apache.tapestry5.MarkupWriter;
021    import org.apache.tapestry5.func.F;
022    import org.apache.tapestry5.func.Flow;
023    import org.apache.tapestry5.func.Mapper;
024    import org.apache.tapestry5.func.Worker;
025    import org.apache.tapestry5.internal.services.XMLTokenStream;
026    import org.apache.tapestry5.internal.services.XMLTokenType;
027    import org.apache.tapestry5.ioc.Location;
028    import org.apache.tapestry5.ioc.Resource;
029    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
030    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
031    import org.apache.tapestry5.ioc.internal.util.TapestryException;
032    import org.apache.tapestry5.runtime.RenderCommand;
033    import org.apache.tapestry5.runtime.RenderQueue;
034    import org.apache.tapestry5.services.BindingSource;
035    import org.apache.tapestry5.services.dynamic.DynamicDelegate;
036    import org.apache.tapestry5.services.dynamic.DynamicTemplate;
037    
038    import javax.xml.namespace.QName;
039    import java.net.URL;
040    import java.util.List;
041    import java.util.Map;
042    import java.util.regex.Matcher;
043    import java.util.regex.Pattern;
044    
045    /**
046     * Does the heavy lifting for {@link DynamicTemplateParserImpl}.
047     */
048    public 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    }