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 }