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}