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 }