    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    }