1 /*
2 * Copyright (c) 2003 The Visigoth Software Society. All rights
3 * reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The end-user documentation included with the redistribution, if
18 * any, must include the following acknowledgement:
19 * "This product includes software developed by the
20 * Visigoth Software Society (http://www.visigoths.org/)."
21 * Alternately, this acknowledgement may appear in the software itself,
22 * if and wherever such third-party acknowledgements normally appear.
23 *
24 * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
25 * project contributors may be used to endorse or promote products derived
26 * from this software without prior written permission. For written
27 * permission, please contact visigoths@visigoths.org.
28 *
29 * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
30 * nor may "FreeMarker" or "Visigoth" appear in their names
31 * without prior written permission of the Visigoth Software Society.
32 *
33 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
34 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
36 * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
37 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
40 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
41 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
42 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
43 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
44 * SUCH DAMAGE.
45 * ====================================================================
46 *
47 * This software consists of voluntary contributions made by many
48 * individuals on behalf of the Visigoth Software Society. For more
49 * information on the Visigoth Software Society, please see
50 * http://www.visigoths.org/
51 */
52
53 package freemarker.template;
54
55 import java.io;
56 import java.util;
57 import javax.swing.tree.TreePath;
58 import freemarker.core;
59
60 import freemarker.debug.impl.DebuggerService;
61
62 /**
63 * <p>A core FreeMarker API that represents a compiled template.
64 * Typically, you will use a {@link Configuration} object to instantiate a template.
65 *
66 * <PRE>
67 Configuration cfg = new Configuration();
68 ...
69 Template myTemplate = cfg.getTemplate("myTemplate.html");
70 </PRE>
71 *
72 * <P>However, you can also construct a template directly by passing in to
73 * the appropriate constructor a java.io.Reader instance that is set to
74 * read the raw template text. The compiled template is
75 * stored in an an efficient data structure for later use.
76 *
77 * <p>To render the template, i.e. to merge it with a data model, and
78 * thus produce "cooked" output, call the <tt>process</tt> method.
79 *
80 * <p>Any error messages from exceptions thrown during compilation will be
81 * included in the output stream and thrown back to the calling code.
82 * To change this behavior, you can install custom exception handlers using
83 * {@link Configurable#setTemplateExceptionHandler(TemplateExceptionHandler)} on
84 * a Configuration object (for all templates belonging to a configuration) or on
85 * a Template object (for a single template).
86 *
87 * <p>It's not legal to modify the values of FreeMarker settings: a) while the
88 * template is executing; b) if the template object is already accessible from
89 * multiple threads.
90 *
91 * @version $Id: Template.java,v 1.216.2.3 2006/03/10 17:49:02 revusky Exp $
92 */
93
94 public class Template extends Configurable {
95 public static final String DEFAULT_NAMESPACE_PREFIX = "D";
96 public static final String NO_NS_PREFIX = "N";
97
98 private Map macros = new HashMap();
99 private List imports = new Vector();
100 private TemplateElement rootElement;
101 private String encoding, defaultNS;
102 private final String name;
103 private final ArrayList lines = new ArrayList();
104 private Map prefixToNamespaceURILookup = new HashMap();
105 private Map namespaceURIToPrefixLookup = new HashMap();
106
107 /**
108 * A prime constructor to which all other constructors should
109 * delegate directly or indirectly.
110 */
111 private Template(String name, Configuration cfg)
112 {
113 super(cfg != null ? cfg : Configuration.getDefaultConfiguration());
114 this.name = name;
115 }
116
117 /**
118 * Constructs a template from a character stream.
119 *
120 * @param name the path of the template file relative to the directory what you use to store
121 * the templates. See {@link #getName} for more details.
122 * @param reader the character stream to read from. It will always be closed (Reader.close()).
123 * @param cfg the Configuration object that this Template is associated with.
124 * If this is null, the "default" {@link Configuration} object is used,
125 * which is highly discouraged, because it can easily lead to
126 * erroneous, unpredictable behaviour.
127 * (See more {@link Configuration#getDefaultConfiguration() here...})
128 * @param encoding This is the encoding that we are supposed to be using. If this is
129 * non-null (It's not actually necessary because we are using a Reader) then it is
130 * checked against the encoding specified in the FTL header -- assuming that is specified,
131 * and if they don't match a WrongEncodingException is thrown.
132 */
133 public Template(String name, Reader reader, Configuration cfg, String encoding)
134 throws IOException
135 {
136 this(name, cfg);
137 this.encoding = encoding;
138
139 if (!(reader instanceof BufferedReader)) {
140 reader = new BufferedReader(reader, 0x1000);
141 }
142 LineTableBuilder ltb = new LineTableBuilder(reader);
143 try {
144 try {
145 FMParser parser = new FMParser(this, ltb,
146 getConfiguration().getStrictSyntaxMode(),
147 getConfiguration().getWhitespaceStripping(),
148 getConfiguration().getTagSyntax());
149 this.rootElement = parser.Root();
150 }
151 catch (TokenMgrError exc) {
152 throw new ParseException("Token manager error: " + exc, 0, 0);
153 }
154 }
155 catch(ParseException e) {
156 e.setTemplateName(name);
157 throw e;
158 }
159 finally {
160 ltb.close();
161 }
162 DebuggerService.registerTemplate(this);
163 namespaceURIToPrefixLookup = Collections.unmodifiableMap(namespaceURIToPrefixLookup);
164 prefixToNamespaceURILookup = Collections.unmodifiableMap(prefixToNamespaceURILookup);
165 }
166
167 /**
168 * This is equivalent to Template(name, reader, cfg, null)
169 */
170
171 public Template(String name, Reader reader, Configuration cfg) throws IOException {
172 this(name, reader, cfg, null);
173 }
174
175
176 /**
177 * Constructs a template from a character stream.
178 *
179 * This is the same as the 3 parameter version when you pass null
180 * as the cfg parameter.
181 *
182 * @deprecated This constructor uses the "default" {@link Configuration}
183 * instance, which can easily lead to erroneous, unpredictable behaviour.
184 * See more {@link Configuration#getDefaultConfiguration() here...}.
185 */
186 public Template(String name, Reader reader) throws IOException {
187 this(name, reader, null);
188 }
189
190 /**
191 * This constructor is only used internally.
192 */
193 Template(String name, TemplateElement root, Configuration config) {
194 this(name, config);
195 this.rootElement = root;
196 DebuggerService.registerTemplate(this);
197 }
198
199 /**
200 * Returns a trivial template, one that is just a single block of
201 * plain text, no dynamic content. (Used by the cache module to create
202 * unparsed templates.)
203 * @param name the path of the template file relative to the directory what you use to store
204 * the templates. See {@link #getName} for more details.
205 * @param content the block of text that this template represents
206 * @param config the configuration to which this template belongs
207 */
208 static public Template getPlainTextTemplate(String name, String content, Configuration config) {
209 Template template = new Template(name, config);
210 TextBlock block = new TextBlock(content);
211 template.rootElement = block;
212 DebuggerService.registerTemplate(template);
213 return template;
214 }
215
216 /**
217 * Processes the template, using data from the map, and outputs
218 * the resulting text to the supplied <tt>Writer</tt> The elements of the
219 * map are converted to template models using the default object wrapper
220 * returned by the {@link Configuration#getObjectWrapper() getObjectWrapper()}
221 * method of the <tt>Configuration</tt>.
222 * @param rootMap the root node of the data model. If null, an
223 * empty data model is used. Can be any object that the effective object
224 * wrapper can turn into a <tt>TemplateHashModel</tt>. Basically, simple and
225 * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
226 * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as
227 * any object that implements <tt>__getitem__</tt> into a template hash.
228 * Naturally, you can pass any object directly implementing
229 * <tt>TemplateHashModel</tt> as well.
230 * @param out a <tt>Writer</tt> to output the text to.
231 * @throws TemplateException if an exception occurs during template processing
232 * @throws IOException if an I/O exception occurs during writing to the writer.
233 */
234 public void process(Object rootMap, Writer out)
235 throws TemplateException, IOException
236 {
237 createProcessingEnvironment(rootMap, out, null).process();
238 }
239
240 /**
241 * Processes the template, using data from the root map object, and outputs
242 * the resulting text to the supplied writer, using the supplied
243 * object wrapper to convert map elements to template models.
244 * @param rootMap the root node of the data model. If null, an
245 * empty data model is used. Can be any object that the effective object
246 * wrapper can turn into a <tt>TemplateHashModel</tt> Basically, simple and
247 * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
248 * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as any
249 * object that implements <tt>__getitem__</tt> into a template hash.
250 * Naturally, you can pass any object directly implementing
251 * <tt>TemplateHashModel</tt> as well.
252 * @param wrapper The object wrapper to use to wrap objects into
253 * {@link TemplateModel} instances. If null, the default wrapper retrieved
254 * by {@link Configurable#getObjectWrapper()} is used.
255 * @param out the writer to output the text to.
256 * @param rootNode The root node for recursive processing, this may be null.
257 *
258 * @throws TemplateException if an exception occurs during template processing
259 * @throws IOException if an I/O exception occurs during writing to the writer.
260 */
261 public void process(Object rootMap, Writer out, ObjectWrapper wrapper, TemplateNodeModel rootNode)
262 throws TemplateException, IOException
263 {
264 Environment env = createProcessingEnvironment(rootMap, out, wrapper);
265 if (rootNode != null) {
266 env.setCurrentVisitorNode(rootNode);
267 }
268 env.process();
269 }
270
271 /**
272 * Processes the template, using data from the root map object, and outputs
273 * the resulting text to the supplied writer, using the supplied
274 * object wrapper to convert map elements to template models.
275 * @param rootMap the root node of the data model. If null, an
276 * empty data model is used. Can be any object that the effective object
277 * wrapper can turn into a <tt>TemplateHashModel</tt> Basically, simple and
278 * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
279 * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as any
280 * object that implements <tt>__getitem__</tt> into a template hash.
281 * Naturally, you can pass any object directly implementing
282 * <tt>TemplateHashModel</tt> as well.
283 * @param wrapper The object wrapper to use to wrap objects into
284 * {@link TemplateModel} instances. If null, the default wrapper retrieved
285 * by {@link Configurable#getObjectWrapper()} is used.
286 * @param out the writer to output the text to.
287 *
288 * @throws TemplateException if an exception occurs during template processing
289 * @throws IOException if an I/O exception occurs during writing to the writer.
290 */
291 public void process(Object rootMap, Writer out, ObjectWrapper wrapper)
292 throws TemplateException, IOException
293 {
294 process(rootMap, out, wrapper, null);
295 }
296
297 /**
298 * Creates a {@link freemarker.core.Environment Environment} object,
299 * using this template, the data model provided as the root map object, and
300 * the supplied object wrapper to convert map elements to template models.
301 * You can then call Environment.process() on the returned environment
302 * to set off the actual rendering.
303 * Use this method if you want to do some special initialization on the environment
304 * before template processing, or if you want to read the environment after template
305 * processing.
306 *
307 * <p>Example:
308 *
309 * <p>This:
310 * <pre>
311 * Environment env = myTemplate.createProcessingEnvironment(root, out, null);
312 * env.process();
313 * </pre>
314 * is equivalent with this:
315 * <pre>
316 * myTemplate.process(root, out);
317 * </pre>
318 * But with <tt>createProcessingEnvironment</tt>, you can manipulate the environment
319 * before and after the processing:
320 * <pre>
321 * Environment env = myTemplate.createProcessingEnvironment(root, out);
322 * env.include("include/common.ftl", null, true); // before processing
323 * env.process();
324 * TemplateModel x = env.getVariable("x"); // after processing
325 * </pre>
326 *
327 * @param rootMap the root node of the data model. If null, an
328 * empty data model is used. Can be any object that the effective object
329 * wrapper can turn into a <tt>TemplateHashModel</tt> Basically, simple and
330 * beans wrapper can turn <tt>java.util.Map</tt> objects into hashes
331 * and the Jython wrapper can turn both a <tt>PyDictionary</tt> as well as any
332 * object that implements <tt>__getitem__</tt> into a template hash.
333 * Naturally, you can pass any object directly implementing
334 * <tt>TemplateHashModel</tt> as well.
335 * @param wrapper The object wrapper to use to wrap objects into
336 * {@link TemplateModel} instances. If null, the default wrapper retrieved
337 * by {@link Configurable#getObjectWrapper()} is used.
338 * @param out the writer to output the text to.
339 * @return the {@link freemarker.core.Environment Environment} object created for processing
340 * @throws TemplateException if an exception occurs while setting up the Environment object.
341 * @throws IOException if an exception occurs doing any auto-imports
342 */
343 public Environment createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)
344 throws TemplateException, IOException
345 {
346 TemplateHashModel root = null;
347 if(rootMap instanceof TemplateHashModel) {
348 root = (TemplateHashModel)rootMap;
349 }
350 else {
351 if(wrapper == null) {
352 wrapper = getObjectWrapper();
353 }
354
355 try {
356 root = rootMap != null
357 ? (TemplateHashModel)wrapper.wrap(rootMap)
358 : new SimpleHash(wrapper);
359 if(root == null) {
360 throw new IllegalArgumentException(wrapper.getClass().getName() + " converted " + rootMap.getClass().getName() + " to null.");
361 }
362 }
363 catch(ClassCastException e) {
364 throw new IllegalArgumentException(wrapper.getClass().getName() + " could not convert " + rootMap.getClass().getName() + " to a TemplateHashModel.");
365 }
366 }
367 Environment env = new Environment(this, root, out);
368 getConfiguration().doAutoImports(env);
369 getConfiguration().doAutoIncludes(env);
370 return env;
371 }
372
373 /**
374 * Same as <code>createProcessingEnvironment(rootMap, out, null)</code>.
375 * @see #createProcessingEnvironment(Object rootMap, Writer out, ObjectWrapper wrapper)
376 */
377 public Environment createProcessingEnvironment(Object rootMap, Writer out)
378 throws TemplateException, IOException
379 {
380 return createProcessingEnvironment(rootMap, out, null);
381 }
382
383 /**
384 * Returns a string representing the raw template
385 * text in canonical form.
386 */
387 public String toString() {
388 StringWriter sw = new StringWriter();
389 try {
390 dump(sw);
391 } catch (IOException ioe) {
392 throw new RuntimeException(ioe.getMessage());
393 }
394 return sw.toString();
395 }
396
397
398 /**
399 * The path of the template file relative to the directory what you use to store the templates.
400 * For example, if the real path of template is <tt>"/www/templates/community/forum.fm"</tt>,
401 * and you use "<tt>"/www/templates"</tt> as
402 * {@link Configuration#setDirectoryForTemplateLoading "directoryForTemplateLoading"},
403 * then <tt>name</tt> should be <tt>"community/forum.fm"</tt>. The <tt>name</tt> is used for example when you
404 * use <tt><include ...></tt> and you give a path that is relative to the current
405 * template, or in error messages when FreeMarker logs an error while it processes the template.
406 */
407 public String getName() {
408 return name;
409 }
410
411 /**
412 * Returns the Configuration object associated with this template.
413 */
414 public Configuration getConfiguration() {
415 return (Configuration) getParent();
416 }
417
418 /**
419 * Sets the character encoding to use for
420 * included files. Usually you don't set this value manually,
421 * instead it is assigned to the template upon loading.
422 */
423
424 public void setEncoding(String encoding) {
425 this.encoding = encoding;
426 }
427
428 /**
429 * Returns the character encoding used for reading included files.
430 */
431 public String getEncoding() {
432 return this.encoding;
433 }
434
435 /**
436 * Dump the raw template in canonical form.
437 */
438 public void dump(PrintStream ps) {
439 ps.print(rootElement.getCanonicalForm());
440 }
441
442 /**
443 * Dump the raw template in canonical form.
444 */
445 public void dump(Writer out) throws IOException {
446 out.write(rootElement.getCanonicalForm());
447 }
448
449 /**
450 * Called by code internally to maintain
451 * a table of macros
452 */
453 public void addMacro(Macro macro) {
454 macros.put(macro.getName(), macro);
455 }
456
457 /**
458 * Called by code internally to maintain
459 * a list of imports
460 */
461 public void addImport(LibraryLoad ll) {
462 imports.add(ll);
463 }
464
465 /**
466 * Returns the template source at the location
467 * specified by the coordinates given.
468 * @param beginColumn the first column of the requested source, 1-based
469 * @param beginLine the first line of the requested source, 1-based
470 * @param endColumn the last column of the requested source, 1-based
471 * @param endLine the last line of the requested source, 1-based
472 * @see freemarker.core.TemplateObject#getSource()
473 */
474 public String getSource(int beginColumn,
475 int beginLine,
476 int endColumn,
477 int endLine)
478 {
479 // Our container is zero-based.
480 --beginLine;
481 --beginColumn;
482 --endColumn;
483 --endLine;
484 StringBuffer buf = new StringBuffer();
485 for (int i = beginLine ; i<=endLine; i++) {
486 if (i < lines.size()) {
487 buf.append(lines.get(i));
488 }
489 }
490 int lastLineLength = lines.get(endLine).toString().length();
491 int trailingCharsToDelete = lastLineLength - endColumn -1;
492 buf.delete(0, beginColumn);
493 buf.delete(buf.length() - trailingCharsToDelete, buf.length());
494 return buf.toString();
495 }
496
497 /**
498 * This is a helper class that builds up the line table
499 * info for us.
500 */
501 private class LineTableBuilder extends FilterReader {
502
503 StringBuffer lineBuf = new StringBuffer();
504 int lastChar;
505
506 /**
507 * @param r the character stream to wrap
508 */
509 LineTableBuilder(Reader r) {
510 super(r);
511 }
512
513 public int read() throws IOException {
514 int c = in.read();
515 handleChar(c);
516 return c;
517 }
518
519 public int read(char cbuf[], int off, int len) throws IOException {
520 int numchars = in.read(cbuf, off, len);
521 for (int i=off; i < off+numchars; i++) {
522 char c = cbuf[i];
523 handleChar(c);
524 }
525 return numchars;
526 }
527
528 public void close() throws IOException {
529 if (lineBuf.length() >0) {
530 lines.add(lineBuf.toString());
531 lineBuf.setLength(0);
532 }
533 super.close();
534 }
535
536 private void handleChar(int c) {
537 if (c == '\n' || c == '\r') {
538 if (lastChar == '\r' && c == '\n') { // CRLF under Windoze
539 int lastIndex = lines.size() -1;
540 String lastLine = (String) lines.get(lastIndex);
541 lines.set(lastIndex, lastLine + '\n');
542 } else {
543 lineBuf.append((char) c);
544 lines.add(lineBuf.toString());
545 lineBuf.setLength(0);
546 }
547 }
548 else if (c == '\t') {
549 int numSpaces = 8 - (lineBuf.length() %8);
550 for (int i=0; i<numSpaces; i++) {
551 lineBuf.append(' ');
552 }
553 }
554 else {
555 lineBuf.append((char) c);
556 }
557 lastChar = c;
558 }
559 }
560
561 /**
562 * @return the root TemplateElement object.
563 */
564 public TemplateElement getRootTreeNode() {
565 return rootElement;
566 }
567
568 public Map getMacros() {
569 return macros;
570 }
571
572 public List getImports() {
573 return imports;
574 }
575
576 /**
577 * This is used internally.
578 */
579 public void addPrefixNSMapping(String prefix, String nsURI) {
580 if (nsURI.length() == 0) {
581 throw new IllegalArgumentException("Cannot map empty string URI");
582 }
583 if (prefix.length() == 0) {
584 throw new IllegalArgumentException("Cannot map empty string prefix");
585 }
586 if (prefix.equals(NO_NS_PREFIX)) {
587 throw new IllegalArgumentException("The prefix: " + prefix + " cannot be registered, it is reserved for special internal use.");
588 }
589 if (prefixToNamespaceURILookup.containsKey(prefix)) {
590 throw new IllegalArgumentException("The prefix: '" + prefix + "' was repeated. This is illegal.");
591 }
592 if (namespaceURIToPrefixLookup.containsKey(nsURI)) {
593 throw new IllegalArgumentException("The namespace URI: " + nsURI + " cannot be mapped to 2 different prefixes.");
594 }
595 if (prefix.equals(DEFAULT_NAMESPACE_PREFIX)) {
596 this.defaultNS = nsURI;
597 } else {
598 prefixToNamespaceURILookup.put(prefix, nsURI);
599 namespaceURIToPrefixLookup.put(nsURI, prefix);
600 }
601 }
602
603 public String getDefaultNS() {
604 return this.defaultNS;
605 }
606
607 /**
608 * @return the NamespaceUri mapped to this prefix in this template. (Or null if there is none.)
609 */
610 public String getNamespaceForPrefix(String prefix) {
611 if (prefix.equals("")) {
612 return defaultNS == null ? "" : defaultNS;
613 }
614 return (String) prefixToNamespaceURILookup.get(prefix);
615 }
616
617 /**
618 * @return the prefix mapped to this nsURI in this template. (Or null if there is none.)
619 */
620 public String getPrefixForNamespace(String nsURI) {
621 if (nsURI == null) {
622 return null;
623 }
624 if (nsURI.length() == 0) {
625 return defaultNS == null ? "" : NO_NS_PREFIX;
626 }
627 if (nsURI.equals(defaultNS)) {
628 return "";
629 }
630 return (String) namespaceURIToPrefixLookup.get(nsURI);
631 }
632
633 /**
634 * @return the prefixed name, based on the ns_prefixes defined
635 * in this template's header for the local name and node namespace
636 * passed in as parameters.
637 */
638 public String getPrefixedName(String localName, String nsURI) {
639 if (nsURI == null || nsURI.length() == 0) {
640 if (defaultNS != null) {
641 return NO_NS_PREFIX + ":" + localName;
642 } else {
643 return localName;
644 }
645 }
646 if (nsURI.equals(defaultNS)) {
647 return localName;
648 }
649 String prefix = getPrefixForNamespace(nsURI);
650 if (prefix == null) {
651 return null;
652 }
653 return prefix + ":" + localName;
654 }
655
656 /**
657 * @return an array of the elements containing the given column and line numbers.
658 * @param column the column
659 * @param line the line
660 */
661 public TreePath containingElements(int column, int line) {
662 ArrayList elements = new ArrayList();
663 TemplateElement element = rootElement;
664 mainloop:
665 while (element.contains(column, line)) {
666 elements.add(element);
667 for (Enumeration enumeration = element.children(); enumeration.hasMoreElements();) {
668 TemplateElement elem = (TemplateElement) enumeration.nextElement();
669 if (elem.contains(column, line)) {
670 element = elem;
671 continue mainloop;
672 }
673 }
674 break;
675 }
676 if (elements == null || elements.isEmpty()) {
677 return null;
678 }
679 return new TreePath(elements.toArray());
680 }
681
682 static public class WrongEncodingException extends ParseException {
683
684 public String specifiedEncoding;
685
686 public WrongEncodingException(String specifiedEncoding) {
687 this.specifiedEncoding = specifiedEncoding;
688 }
689
690 }
691 }
692