    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.commons.Location;
    022import org.apache.tapestry5.commons.Resource;
    023import org.apache.tapestry5.commons.internal.util.TapestryException;
    024import org.apache.tapestry5.commons.util.CollectionFactory;
    025import org.apache.tapestry5.commons.util.ExceptionUtils;
    026import org.apache.tapestry5.func.F;
    027import org.apache.tapestry5.func.Flow;
    028import org.apache.tapestry5.func.Mapper;
    029import org.apache.tapestry5.func.Worker;
    030import org.apache.tapestry5.internal.services.XMLTokenStream;
    031import org.apache.tapestry5.internal.services.XMLTokenType;
    032import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    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}