001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.core;
054:
055: import java.io.IOException;
056: import java.io.InputStream;
057: import java.util.*;
058:
059: import freemarker.template.*;
060: import freemarker.template.utility.ClassUtil;
061: import freemarker.template.utility.StringUtil;
062: import freemarker.ext.beans.BeansWrapper;
063:
064: /**
065: * This is a common superclass of {@link freemarker.template.Configuration},
066: * {@link freemarker.template.Template}, and {@link Environment} classes.
067: * It provides settings that are common to each of them. FreeMarker
068: * uses a three-level setting hierarchy - the return value of every setting
069: * getter method on <code>Configurable</code> objects inherits its value from its parent
070: * <code>Configurable</code> object, unless explicitly overridden by a call to a
071: * corresponding setter method on the object itself. The parent of an
072: * <code>Environment</code> object is a <code>Template</code> object, the
073: * parent of a <code>Template</code> object is a <code>Configuration</code>
074: * object.
075: *
076: * @version $Id: Configurable.java,v 1.23.2.2 2007/04/02 13:46:43 szegedia Exp $
077: * @author Attila Szegedi
078: */
079: public class Configurable {
080: public static final String LOCALE_KEY = "locale";
081: public static final String NUMBER_FORMAT_KEY = "number_format";
082: public static final String TIME_FORMAT_KEY = "time_format";
083: public static final String DATE_FORMAT_KEY = "date_format";
084: public static final String DATETIME_FORMAT_KEY = "datetime_format";
085: public static final String TIME_ZONE_KEY = "time_zone";
086: public static final String CLASSIC_COMPATIBLE_KEY = "classic_compatible";
087: public static final String TEMPLATE_EXCEPTION_HANDLER_KEY = "template_exception_handler";
088: public static final String ARITHMETIC_ENGINE_KEY = "arithmetic_engine";
089: public static final String OBJECT_WRAPPER_KEY = "object_wrapper";
090: public static final String BOOLEAN_FORMAT_KEY = "boolean_format";
091: public static final String OUTPUT_ENCODING_KEY = "output_encoding";
092: public static final String URL_ESCAPING_CHARSET_KEY = "url_escaping_charset";
093: public static final String STRICT_BEAN_MODELS = "strict_bean_models";
094:
095: private static final char COMMA = ',';
096:
097: private Configurable parent;
098: private Properties properties;
099: private HashMap customAttributes;
100:
101: private Locale locale;
102: private String numberFormat;
103: private String timeFormat;
104: private String dateFormat;
105: private String dateTimeFormat;
106: private TimeZone timeZone;
107: private String trueFormat;
108: private String falseFormat;
109: private Boolean classicCompatible;
110: private TemplateExceptionHandler templateExceptionHandler;
111: private ArithmeticEngine arithmeticEngine;
112: private ObjectWrapper objectWrapper;
113: private String outputEncoding;
114: private boolean outputEncodingSet;
115: private String urlEscapingCharset;
116: private boolean urlEscapingCharsetSet;
117:
118: public Configurable() {
119: parent = null;
120: locale = Locale.getDefault();
121: timeZone = TimeZone.getDefault();
122: numberFormat = "number";
123: timeFormat = "";
124: dateFormat = "";
125: dateTimeFormat = "";
126: trueFormat = "true";
127: falseFormat = "false";
128: classicCompatible = Boolean.FALSE;
129: templateExceptionHandler = TemplateExceptionHandler.DEBUG_HANDLER;
130: arithmeticEngine = ArithmeticEngine.BIGDECIMAL_ENGINE;
131: objectWrapper = ObjectWrapper.DEFAULT_WRAPPER;
132: // outputEncoding and urlEscapingCharset defaults to null,
133: // which means "not specified"
134:
135: properties = new Properties();
136: properties.setProperty(LOCALE_KEY, locale.toString());
137: properties.setProperty(TIME_FORMAT_KEY, timeFormat);
138: properties.setProperty(DATE_FORMAT_KEY, dateFormat);
139: properties.setProperty(DATETIME_FORMAT_KEY, dateTimeFormat);
140: properties.setProperty(TIME_ZONE_KEY, timeZone.getID());
141: properties.setProperty(NUMBER_FORMAT_KEY, numberFormat);
142: properties.setProperty(CLASSIC_COMPATIBLE_KEY,
143: classicCompatible.toString());
144: properties.setProperty(TEMPLATE_EXCEPTION_HANDLER_KEY,
145: templateExceptionHandler.getClass().getName());
146: properties.setProperty(ARITHMETIC_ENGINE_KEY, arithmeticEngine
147: .getClass().getName());
148: properties.setProperty(BOOLEAN_FORMAT_KEY, "true,false");
149: // as outputEncoding and urlEscapingCharset defaults to null,
150: // they are not set
151:
152: customAttributes = new HashMap();
153: }
154:
155: /**
156: * Creates a new instance. Normally you do not need to use this constructor,
157: * as you don't use <code>Configurable</code> directly, but its subclasses.
158: */
159: public Configurable(Configurable parent) {
160: this .parent = parent;
161: locale = null;
162: numberFormat = null;
163: trueFormat = null;
164: falseFormat = null;
165: classicCompatible = null;
166: templateExceptionHandler = null;
167: properties = new Properties(parent.properties);
168: customAttributes = new HashMap();
169: }
170:
171: protected Object clone() throws CloneNotSupportedException {
172: Configurable copy = (Configurable) super .clone();
173: copy.properties = new Properties(properties);
174: copy.customAttributes = (HashMap) customAttributes.clone();
175: return copy;
176: }
177:
178: /**
179: * Returns the parent <tt>Configurable</tt> object of this object.
180: * The parent stores the default values for this configurable. For example,
181: * the parent of the {@link freemarker.template.Template} object is the
182: * {@link freemarker.template.Configuration} object, so setting values not
183: * specfied on template level are specified by the confuration object.
184: *
185: * @return the parent <tt>Configurable</tt> object, or null, if this is
186: * the root <tt>Configurable</tt> object.
187: */
188: public final Configurable getParent() {
189: return parent;
190: }
191:
192: /**
193: * Reparenting support. This is used by Environment when it includes a
194: * template - the included template becomes the parent configurable during
195: * its evaluation.
196: */
197: final void setParent(Configurable parent) {
198: this .parent = parent;
199: }
200:
201: /**
202: * Toggles the "Classic Compatibile" mode. For a comprehensive description
203: * of this mode, see {@link #isClassicCompatible()}.
204: */
205: public void setClassicCompatible(boolean classicCompatibility) {
206: this .classicCompatible = classicCompatibility ? Boolean.TRUE
207: : Boolean.FALSE;
208: properties.setProperty(CLASSIC_COMPATIBLE_KEY,
209: classicCompatible.toString());
210: }
211:
212: /**
213: * Returns whether the engine runs in the "Classic Compatibile" mode.
214: * When this mode is active, the engine behavior is altered in following
215: * way: (these resemble the behavior of the 1.7.x line of FreeMarker engine,
216: * now named "FreeMarker Classic", hence the name).
217: * <ul>
218: * <li>handle undefined expressions gracefully. Namely when an expression
219: * "expr" evaluates to null:
220: * <ul>
221: * <li>as argument of the <tt><assign varname=expr></tt> directive,
222: * <tt>${expr}</tt> directive, <tt>otherexpr == expr</tt> or
223: * <tt>otherexpr != expr</tt> conditional expressions, or
224: * <tt>hash[expr]</tt> expression, then it is treated as empty string.
225: * </li>
226: * <li>as argument of <tt><list expr as item></tt> or
227: * <tt><foreach item in expr></tt>, the loop body is not executed
228: * (as if it were a 0-length list)
229: * </li>
230: * <li>as argument of <tt><if></tt> directive, or otherwise where a
231: * boolean expression is expected, it is treated as false
232: * </li>
233: * </ul>
234: * </li>
235: * <li>Non-boolean models are accepted in <tt><if></tt> directive,
236: * or as operands of logical operators. "Empty" models (zero-length string,
237: * empty sequence or hash) are evaluated as false, all others are evaluated as
238: * true.</li>
239: * <li>When boolean value is treated as a string (i.e. output in
240: * <tt>${...}</tt> directive, or concatenated with other string), true
241: * values are converted to string "true", false values are converted to
242: * empty string.
243: * </li>
244: * <li>Scalar models supplied to <tt><list></tt> and
245: * <tt><foreach></tt> are treated as a one-element list consisting
246: * of the passed model.
247: * </li>
248: * <li>Paths parameter of <tt><include></tt> will be interpreted as
249: * absolute path.
250: * </li>
251: * </ul>
252: * In all other aspects, the engine is a 2.1 engine even in compatibility
253: * mode - you don't lose any of the new functionality by enabling it.
254: */
255: public boolean isClassicCompatible() {
256: return classicCompatible != null ? classicCompatible
257: .booleanValue() : parent.isClassicCompatible();
258: }
259:
260: /**
261: * Sets the locale to assume when searching for template files with no
262: * explicit requested locale.
263: */
264: public void setLocale(Locale locale) {
265: if (locale == null)
266: throw new IllegalArgumentException(
267: "Setting \"locale\" can't be null");
268: this .locale = locale;
269: properties.setProperty(LOCALE_KEY, locale.toString());
270: }
271:
272: /**
273: * Returns the time zone to use when formatting time values. Defaults to
274: * system time zone.
275: */
276: public TimeZone getTimeZone() {
277: return timeZone != null ? timeZone : parent.getTimeZone();
278: }
279:
280: /**
281: * Sets the time zone to use when formatting time values.
282: */
283: public void setTimeZone(TimeZone timeZone) {
284: if (timeZone == null)
285: throw new IllegalArgumentException(
286: "Setting \"time_zone\" can't be null");
287: this .timeZone = timeZone;
288: properties.setProperty(TIME_ZONE_KEY, timeZone.getID());
289: }
290:
291: /**
292: * Returns the assumed locale when searching for template files with no
293: * explicit requested locale. Defaults to system locale.
294: */
295: public Locale getLocale() {
296: return locale != null ? locale : parent.getLocale();
297: }
298:
299: /**
300: * Sets the number format used to convert numbers to strings.
301: */
302: public void setNumberFormat(String numberFormat) {
303: if (numberFormat == null)
304: throw new IllegalArgumentException(
305: "Setting \"number_format\" can't be null");
306: this .numberFormat = numberFormat;
307: properties.setProperty(NUMBER_FORMAT_KEY, numberFormat);
308: }
309:
310: /**
311: * Returns the default number format used to convert numbers to strings.
312: * Defaults to <tt>"number"</tt>
313: */
314: public String getNumberFormat() {
315: return numberFormat != null ? numberFormat : parent
316: .getNumberFormat();
317: }
318:
319: public void setBooleanFormat(String booleanFormat) {
320: if (booleanFormat == null) {
321: throw new IllegalArgumentException(
322: "Setting \"boolean_format\" can't be null");
323: }
324: int comma = booleanFormat.indexOf(COMMA);
325: if (comma == -1) {
326: throw new IllegalArgumentException(
327: "Setting \"boolean_format\" must consist of two comma-separated values for true and false respectively");
328: }
329: trueFormat = booleanFormat.substring(0, comma);
330: falseFormat = booleanFormat.substring(comma + 1);
331: properties.setProperty(BOOLEAN_FORMAT_KEY, booleanFormat);
332: }
333:
334: public String getBooleanFormat() {
335: if (trueFormat == null) {
336: return parent.getBooleanFormat();
337: }
338: return trueFormat + COMMA + falseFormat;
339: }
340:
341: String getBooleanFormat(boolean value) {
342: return value ? getTrueFormat() : getFalseFormat();
343: }
344:
345: private String getTrueFormat() {
346: return trueFormat != null ? trueFormat : parent.getTrueFormat();
347: }
348:
349: private String getFalseFormat() {
350: return falseFormat != null ? falseFormat : parent
351: .getFalseFormat();
352: }
353:
354: /**
355: * Sets the date format used to convert date models representing time-only
356: * values to strings.
357: */
358: public void setTimeFormat(String timeFormat) {
359: if (timeFormat == null)
360: throw new IllegalArgumentException(
361: "Setting \"time_format\" can't be null");
362: this .timeFormat = timeFormat;
363: properties.setProperty(TIME_FORMAT_KEY, timeFormat);
364: }
365:
366: /**
367: * Returns the date format used to convert date models representing
368: * time-only dates to strings.
369: * Defaults to <tt>"time"</tt>
370: */
371: public String getTimeFormat() {
372: return timeFormat != null ? timeFormat : parent.getTimeFormat();
373: }
374:
375: /**
376: * Sets the date format used to convert date models representing date-only
377: * dates to strings.
378: */
379: public void setDateFormat(String dateFormat) {
380: if (dateFormat == null)
381: throw new IllegalArgumentException(
382: "Setting \"date_format\" can't be null");
383: this .dateFormat = dateFormat;
384: properties.setProperty(DATE_FORMAT_KEY, dateFormat);
385: }
386:
387: /**
388: * Returns the date format used to convert date models representing
389: * date-only dates to strings.
390: * Defaults to <tt>"date"</tt>
391: */
392: public String getDateFormat() {
393: return dateFormat != null ? dateFormat : parent.getDateFormat();
394: }
395:
396: /**
397: * Sets the date format used to convert date models representing datetime
398: * dates to strings.
399: */
400: public void setDateTimeFormat(String dateTimeFormat) {
401: if (dateTimeFormat == null)
402: throw new IllegalArgumentException(
403: "Setting \"datetime_format\" can't be null");
404: this .dateTimeFormat = dateTimeFormat;
405: properties.setProperty(DATETIME_FORMAT_KEY, dateTimeFormat);
406: }
407:
408: /**
409: * Returns the date format used to convert date models representing datetime
410: * dates to strings.
411: * Defaults to <tt>"datetime"</tt>
412: */
413: public String getDateTimeFormat() {
414: return dateTimeFormat != null ? dateTimeFormat : parent
415: .getDateTimeFormat();
416: }
417:
418: /**
419: * Sets the exception handler used to handle template exceptions.
420: *
421: * @param templateExceptionHandler the template exception handler to use for
422: * handling {@link TemplateException}s. By default,
423: * {@link TemplateExceptionHandler#HTML_DEBUG_HANDLER} is used.
424: */
425: public void setTemplateExceptionHandler(
426: TemplateExceptionHandler templateExceptionHandler) {
427: if (templateExceptionHandler == null)
428: throw new IllegalArgumentException(
429: "Setting \"template_exception_handler\" can't be null");
430: this .templateExceptionHandler = templateExceptionHandler;
431: properties.setProperty(TEMPLATE_EXCEPTION_HANDLER_KEY,
432: templateExceptionHandler.getClass().getName());
433: }
434:
435: /**
436: * Retrieves the exception handler used to handle template exceptions.
437: */
438: public TemplateExceptionHandler getTemplateExceptionHandler() {
439: return templateExceptionHandler != null ? templateExceptionHandler
440: : parent.getTemplateExceptionHandler();
441: }
442:
443: /**
444: * Sets the arithmetic engine used to perform arithmetic operations.
445: *
446: * @param arithmeticEngine the arithmetic engine used to perform arithmetic
447: * operations.By default, {@link ArithmeticEngine#BIGDECIMAL_ENGINE} is
448: * used.
449: */
450: public void setArithmeticEngine(ArithmeticEngine arithmeticEngine) {
451: if (arithmeticEngine == null)
452: throw new IllegalArgumentException(
453: "Setting \"arithmetic_engine\" can't be null");
454: this .arithmeticEngine = arithmeticEngine;
455: properties.setProperty(ARITHMETIC_ENGINE_KEY, arithmeticEngine
456: .getClass().getName());
457: }
458:
459: /**
460: * Retrieves the arithmetic engine used to perform arithmetic operations.
461: */
462: public ArithmeticEngine getArithmeticEngine() {
463: return arithmeticEngine != null ? arithmeticEngine : parent
464: .getArithmeticEngine();
465: }
466:
467: /**
468: * Sets the object wrapper used to wrap objects to template models.
469: *
470: * @param objectWrapper the object wrapper used to wrap objects to template
471: * models.By default, {@link ObjectWrapper#DEFAULT_WRAPPER} is used.
472: */
473: public void setObjectWrapper(ObjectWrapper objectWrapper) {
474: if (objectWrapper == null)
475: throw new IllegalArgumentException(
476: "Setting \"object_wrapper\" can't be null");
477: this .objectWrapper = objectWrapper;
478: properties.setProperty(OBJECT_WRAPPER_KEY, objectWrapper
479: .getClass().getName());
480: }
481:
482: /**
483: * Retrieves the object wrapper used to wrap objects to template models.
484: */
485: public ObjectWrapper getObjectWrapper() {
486: return objectWrapper != null ? objectWrapper : parent
487: .getObjectWrapper();
488: }
489:
490: /**
491: * Sets the output encoding. Allows <code>null</code>, which means that the
492: * output encoding is not known.
493: */
494: public void setOutputEncoding(String outputEncoding) {
495: this .outputEncoding = outputEncoding;
496: // java.util.Properties doesn't allow null value!
497: if (outputEncoding != null) {
498: properties.setProperty(OUTPUT_ENCODING_KEY, outputEncoding);
499: } else {
500: properties.remove(OUTPUT_ENCODING_KEY);
501: }
502: outputEncodingSet = true;
503: }
504:
505: public String getOutputEncoding() {
506: return outputEncodingSet ? outputEncoding
507: : (parent != null ? parent.getOutputEncoding() : null);
508: }
509:
510: /**
511: * Sets the URL escaping charset. Allows <code>null</code>, which means that the
512: * output encoding will be used for URL escaping.
513: */
514: public void setURLEscapingCharset(String urlEscapingCharset) {
515: this .urlEscapingCharset = urlEscapingCharset;
516: // java.util.Properties doesn't allow null value!
517: if (urlEscapingCharset != null) {
518: properties.setProperty(URL_ESCAPING_CHARSET_KEY,
519: urlEscapingCharset);
520: } else {
521: properties.remove(URL_ESCAPING_CHARSET_KEY);
522: }
523: urlEscapingCharsetSet = true;
524: }
525:
526: public String getURLEscapingCharset() {
527: return urlEscapingCharsetSet ? urlEscapingCharset
528: : (parent != null ? parent.getURLEscapingCharset()
529: : null);
530: }
531:
532: /**
533: * Sets a setting by a name and string value.
534: *
535: * <p>List of supported names and their valid values:
536: * <ul>
537: * <li><code>"locale"</code>: local codes with the usual format, such as <code>"en_US"</code>.
538: * <li><code>"classic_compatible"</code>:
539: * <code>"true"</code>, <code>"false"</code>, <code>"yes"</code>, <code>"no"</code>,
540: * <code>"t"</code>, <code>"f"</code>, <code>"y"</code>, <code>"n"</code>.
541: * Case insensitive.
542: * <li><code>"template_exception_handler"</code>: If the value contains dot, then it is
543: * interpreted as class name, and the object will be created with
544: * its parameterless constructor. If the value does not contain dot,
545: * then it must be one of these special values:
546: * <code>"rethrow"</code>, <code>"debug"</code>,
547: * <code>"html_debug"</code>, <code>"ignore"</code> (case insensitive).
548: * <li><code>"arithmetic_engine"</code>: If the value contains dot, then it is
549: * interpreted as class name, and the object will be created with
550: * its parameterless constructor. If the value does not contain dot,
551: * then it must be one of these special values:
552: * <code>"bigdecimal"</code>, <code>"conservative"</code> (case insensitive).
553: * <li><code>"object_wrapper"</code>: If the value contains dot, then it is
554: * interpreted as class name, and the object will be created with
555: * its parameterless constructor. If the value does not contain dot,
556: * then it must be one of these special values:
557: * <code>"simple"</code>, <code>"beans"</code>, <code>"jython"</code> (case insensitive).
558: * <li><code>"number_format"</code>: pattern as <code>java.text.DecimalFormat</code> defines.
559: * <li><code>"boolean_format"</code>: the textual value for boolean true and false,
560: * separated with comma. For example <code>"yes,no"</code>.
561: * <li><code>"date_format", "time_format", "datetime_format"</code>: patterns as
562: * <code>java.text.SimpleDateFormat</code> defines.
563: * <li><code>"time_zone"</code>: time zone, with the format as
564: * <code>java.util.TimeZone.getTimeZone</code> defines. For example <code>"GMT-8:00"</code> or
565: * <code>"America/Los_Angeles"</code>
566: * <li><code>"output_encoding"</code>: Informs FreeMarker about the charset
567: * used for the output. As FreeMarker outputs character stream (not
568: * byte stream), it is not aware of the output charset unless the
569: * software that encloses it tells it explicitly with this setting.
570: * Some templates may use FreeMarker features that require this.</code>
571: * <li><code>"url_escaping_charset"</code>: If this setting is set, then it
572: * overrides the value of the <code>"output_encoding"</code> setting when
573: * FreeMarker does URL encoding.
574: * </ul>
575: *
576: * @param key the name of the setting.
577: * @param value the string that describes the new value of the setting.
578: *
579: * @throws UnknownSettingException if the key is wrong.
580: * @throws TemplateException if the new value of the setting can't be set
581: * for any other reasons.
582: */
583: public void setSetting(String key, String value)
584: throws TemplateException {
585: try {
586: if (LOCALE_KEY.equals(key)) {
587: setLocale(StringUtil.deduceLocale(value));
588: } else if (NUMBER_FORMAT_KEY.equals(key)) {
589: setNumberFormat(value);
590: } else if (TIME_FORMAT_KEY.equals(key)) {
591: setTimeFormat(value);
592: } else if (DATE_FORMAT_KEY.equals(key)) {
593: setDateFormat(value);
594: } else if (DATETIME_FORMAT_KEY.equals(key)) {
595: setDateTimeFormat(value);
596: } else if (TIME_ZONE_KEY.equals(key)) {
597: setTimeZone(TimeZone.getTimeZone(value));
598: } else if (CLASSIC_COMPATIBLE_KEY.equals(key)) {
599: setClassicCompatible(StringUtil.getYesNo(value));
600: } else if (TEMPLATE_EXCEPTION_HANDLER_KEY.equals(key)) {
601: if (value.indexOf('.') == -1) {
602: if ("debug".equalsIgnoreCase(value)) {
603: setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
604: } else if ("html_debug".equalsIgnoreCase(value)) {
605: setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
606: } else if ("ignore".equalsIgnoreCase(value)) {
607: setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
608: } else if ("rethrow".equalsIgnoreCase(value)) {
609: setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
610: } else {
611: throw invalidSettingValueException(key, value);
612: }
613: } else {
614: setTemplateExceptionHandler((TemplateExceptionHandler) ClassUtil
615: .forName(value).newInstance());
616: }
617: } else if (ARITHMETIC_ENGINE_KEY.equals(key)) {
618: if (value.indexOf('.') == -1) {
619: if ("bigdecimal".equalsIgnoreCase(value)) {
620: setArithmeticEngine(ArithmeticEngine.BIGDECIMAL_ENGINE);
621: } else if ("conservative".equalsIgnoreCase(value)) {
622: setArithmeticEngine(ArithmeticEngine.CONSERVATIVE_ENGINE);
623: } else {
624: throw invalidSettingValueException(key, value);
625: }
626: } else {
627: setArithmeticEngine((ArithmeticEngine) ClassUtil
628: .forName(value).newInstance());
629: }
630: } else if (OBJECT_WRAPPER_KEY.equals(key)) {
631: if (value.indexOf('.') == -1) {
632: if ("default".equalsIgnoreCase(value)) {
633: setObjectWrapper(ObjectWrapper.DEFAULT_WRAPPER);
634: } else if ("simple".equalsIgnoreCase(value)) {
635: setObjectWrapper(ObjectWrapper.SIMPLE_WRAPPER);
636: } else if ("beans".equalsIgnoreCase(value)) {
637: setObjectWrapper(ObjectWrapper.BEANS_WRAPPER);
638: } else if ("jython".equalsIgnoreCase(value)) {
639: Class clazz = Class
640: .forName("freemarker.ext.jython.JythonWrapper");
641: setObjectWrapper((ObjectWrapper) clazz
642: .getField("INSTANCE").get(null));
643: } else {
644: throw invalidSettingValueException(key, value);
645: }
646:
647: } else {
648: setObjectWrapper((ObjectWrapper) ClassUtil.forName(
649: value).newInstance());
650: }
651: } else if (BOOLEAN_FORMAT_KEY.equals(key)) {
652: setBooleanFormat(value);
653: } else if (OUTPUT_ENCODING_KEY.equals(key)) {
654: setOutputEncoding(value);
655: } else if (URL_ESCAPING_CHARSET_KEY.equals(key)) {
656: setURLEscapingCharset(value);
657: } else if (STRICT_BEAN_MODELS.equals(key)) {
658: setStrictBeanModels(StringUtil.getYesNo(value));
659: } else {
660: throw unknownSettingException(key);
661: }
662: } catch (TemplateException e) {
663: throw e;
664: } catch (Exception e) {
665: throw new TemplateException("Failed to set setting " + key
666: + " to value " + value, e, getEnvironment());
667: }
668: }
669:
670: public void setStrictBeanModels(boolean strict) {
671: if (!(objectWrapper instanceof BeansWrapper)) {
672: throw new IllegalStateException("Not a beans wrapper");
673: }
674: ((BeansWrapper) objectWrapper).setStrict(strict);
675: }
676:
677: /**
678: * Returns the textual representation of a setting.
679: * @param key the setting key. Can be any of standard <tt>XXX_KEY</tt>
680: * constants, or a custom key.
681: *
682: * @deprecated This method was always defective, and certainly it always
683: * will be. Don't use it. (Simply, it's hardly possible in general to
684: * convert setting values to text in a way that ensures that
685: * {@link #setSetting(String, String)} will work with them correctly.)
686: */
687: public String getSetting(String key) {
688: return properties.getProperty(key);
689: }
690:
691: /**
692: * This meant to return the String-to-String <code>Map</code> of the
693: * settings. So it actually should return a <code>Properties</code> object,
694: * but it doesn't by mistake. The returned <code>Map</code> is read-only,
695: * but it will reflect the further configuration changes (aliasing effect).
696: *
697: * @deprecated This method was always defective, and certainly it always
698: * will be. Don't use it. (Simply, it's hardly possible in general to
699: * convert setting values to text in a way that ensures that
700: * {@link #setSettings(Properties)} will work with them correctly.)
701: */
702: public Map getSettings() {
703: return Collections.unmodifiableMap(properties);
704: }
705:
706: protected Environment getEnvironment() {
707: return this instanceof Environment ? (Environment) this
708: : Environment.getCurrentEnvironment();
709: }
710:
711: protected TemplateException unknownSettingException(String name) {
712: return new UnknownSettingException(name, getEnvironment());
713: }
714:
715: protected TemplateException invalidSettingValueException(
716: String name, String value) {
717: return new TemplateException("Invalid value for setting "
718: + name + ": " + value, getEnvironment());
719: }
720:
721: public class UnknownSettingException extends TemplateException {
722: private UnknownSettingException(String name, Environment env) {
723: super ("Unknown setting: " + name, env);
724: }
725: }
726:
727: /**
728: * Set the settings stored in a <code>Properties</code> object.
729: *
730: * @throws TemplateException if the <code>Properties</code> object contains
731: * invalid keys, or invalid setting values, or any other error occurs
732: * while changing the settings.
733: */
734: public void setSettings(Properties props) throws TemplateException {
735: Iterator it = props.keySet().iterator();
736: while (it.hasNext()) {
737: String key = (String) it.next();
738: setSetting(key, props.getProperty(key).trim());
739: }
740: }
741:
742: /**
743: * Reads a setting list (key and element pairs) from the input stream.
744: * The stream has to follow the usual <code>.properties</code> format.
745: *
746: * @throws TemplateException if the stream contains
747: * invalid keys, or invalid setting values, or any other error occurs
748: * while changing the settings.
749: * @throws IOException if an error occurred when reading from the input stream.
750: */
751: public void setSettings(InputStream propsIn)
752: throws TemplateException, IOException {
753: Properties p = new Properties();
754: p.load(propsIn);
755: setSettings(p);
756: }
757:
758: /**
759: * Internal entry point for setting unnamed custom attributes
760: */
761: void setCustomAttribute(Object key, Object value) {
762: synchronized (customAttributes) {
763: customAttributes.put(key, value);
764: }
765: }
766:
767: /**
768: * Internal entry point for getting unnamed custom attributes
769: */
770: Object getCustomAttribute(Object key, CustomAttribute attr) {
771: synchronized (customAttributes) {
772: Object o = customAttributes.get(key);
773: if (o == null && !customAttributes.containsKey(key)) {
774: o = attr.create();
775: customAttributes.put(key, o);
776: }
777: return o;
778: }
779: }
780:
781: /**
782: * Sets a named custom attribute for this configurable.
783: *
784: * @param name the name of the custom attribute
785: * @param value the value of the custom attribute. You can set the value to
786: * null, however note that there is a semantic difference between an
787: * attribute set to null and an attribute that is not present, see
788: * {@link #removeCustomAttribute(String)}.
789: */
790: public void setCustomAttribute(String name, Object value) {
791: synchronized (customAttributes) {
792: customAttributes.put(name, value);
793: }
794: }
795:
796: /**
797: * Returns an array with names of all custom attributes defined directly
798: * on this configurable. (That is, it doesn't contain the names of custom attributes
799: * defined indirectly on its parent configurables.) The returned array is never null,
800: * but can be zero-length.
801: * The order of elements in the returned array is not defined and can change
802: * between invocations.
803: */
804: public String[] getCustomAttributeNames() {
805: synchronized (customAttributes) {
806: Collection names = new LinkedList(customAttributes.keySet());
807: for (Iterator iter = names.iterator(); iter.hasNext();) {
808: if (!(iter.next() instanceof String)) {
809: iter.remove();
810: }
811: }
812: return (String[]) names.toArray(new String[names.size()]);
813: }
814: }
815:
816: /**
817: * Removes a named custom attribute for this configurable. Note that this
818: * is different than setting the custom attribute value to null. If you
819: * set the value to null, {@link #getCustomAttribute(String)} will return
820: * null, while if you remove the attribute, it will return the value of
821: * the attribute in the parent configurable (if there is a parent
822: * configurable, that is).
823: *
824: * @param name the name of the custom attribute
825: */
826: public void removeCustomAttribute(String name) {
827: synchronized (customAttributes) {
828: customAttributes.remove(name);
829: }
830: }
831:
832: /**
833: * Retrieves a named custom attribute for this configurable. If the
834: * attribute is not present in the configurable, and the configurable has
835: * a parent, then the parent is looked up as well.
836: *
837: * @param name the name of the custom attribute
838: *
839: * @return the value of the custom attribute. Note that if the custom attribute
840: * was created with <tt><#ftl attributes={...}></tt>, then this value is already
841: * unwrapped (i.e. it's a <code>String</code>, or a <code>List</code>, or a
842: * <code>Map</code>, ...etc., not a FreeMarker specific class).
843: */
844: public Object getCustomAttribute(String name) {
845: Object retval;
846: synchronized (customAttributes) {
847: retval = customAttributes.get(name);
848: if (retval == null && customAttributes.containsKey(name)) {
849: return null;
850: }
851: }
852: if (retval == null && parent != null) {
853: return parent.getCustomAttribute(name);
854: }
855: return retval;
856: }
857: }
|