Merge branch '2.3-gae' into 2.3
diff --git a/osgi.bnd b/osgi.bnd
index f19506d..3853465 100644
--- a/osgi.bnd
+++ b/osgi.bnd
@@ -32,7 +32,10 @@
# This is needed for "a.class.from.another.Bundle"?new() to work.
DynamicImport-Package: *
-Bundle-RequiredExecutionEnvironment: J2SE-1.4, J2SE-1.5, JavaSE-1.6, JavaSE-1.7
+# The required minimum is 1.4, but we utilize 1.5 if available.
+# See also: http://wiki.eclipse.org/Execution_Environments, "Compiling
+# against more than is required"
+Bundle-RequiredExecutionEnvironment: J2SE-1.5, J2SE-1.4
# Non-OSGi meta:
Main-Class: freemarker.core.CommandLine
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index aa15521..81609df 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -24,7 +24,6 @@
import freemarker.core.BuiltInsForDates.iso_BI;
import freemarker.core.BuiltInsForDates.iso_utc_or_local_BI;
-import freemarker.core.BuiltInsForStringsMisc.evalBI;
import freemarker.core.BuiltInsForNodes.ancestorsBI;
import freemarker.core.BuiltInsForNodes.childrenBI;
import freemarker.core.BuiltInsForNodes.node_nameBI;
@@ -53,6 +52,7 @@
import freemarker.core.BuiltInsForSequences.seq_index_ofBI;
import freemarker.core.BuiltInsForSequences.sortBI;
import freemarker.core.BuiltInsForSequences.sort_byBI;
+import freemarker.core.BuiltInsForStringsMisc.evalBI;
import freemarker.template.Configuration;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateModel;
@@ -92,6 +92,8 @@
builtins.put("default", new ExistenceBuiltins.defaultBI());
builtins.put("double", new doubleBI());
builtins.put("ends_with", new BuiltInsForStringsBasic.ends_withBI());
+ builtins.put("ensure_ends_with", new BuiltInsForStringsBasic.ensure_ends_withBI());
+ builtins.put("ensure_starts_with", new BuiltInsForStringsBasic.ensure_starts_withBI());
builtins.put("eval", new evalBI());
builtins.put("exists", new ExistenceBuiltins.existsBI());
builtins.put("first", new firstBI());
@@ -194,6 +196,8 @@
builtins.put("join", new BuiltInsForSequences.joinBI());
builtins.put("js_string", new BuiltInsForStringsEncoding.js_stringBI());
builtins.put("json_string", new BuiltInsForStringsEncoding.json_stringBI());
+ builtins.put("keep_after", new BuiltInsForStringsBasic.keep_afterBI());
+ builtins.put("keep_before", new BuiltInsForStringsBasic.keep_beforeBI());
builtins.put("keys", new BuiltInsForHashes.keysBI());
builtins.put("last_index_of", new BuiltInsForStringsBasic.index_ofBI(true));
builtins.put("last", new lastBI());
@@ -215,6 +219,8 @@
builtins.put("right_pad", new BuiltInsForStringsBasic.padBI(false));
builtins.put("root", new rootBI());
builtins.put("round", new roundBI());
+ builtins.put("remove_ending", new BuiltInsForStringsBasic.remove_endingBI());
+ builtins.put("remove_beginning", new BuiltInsForStringsBasic.remove_beginningBI());
builtins.put("rtf", new BuiltInsForStringsEncoding.rtfBI());
builtins.put("seq_contains", new seq_containsBI());
builtins.put("seq_index_of", new seq_index_ofBI(1));
@@ -223,6 +229,7 @@
builtins.put("size", new BuiltInsForMultipleTypes.sizeBI());
builtins.put("sort_by", new sort_byBI());
builtins.put("sort", new sortBI());
+ builtins.put("split", new BuiltInsForStringsBasic.split_BI());
builtins.put("starts_with", new BuiltInsForStringsBasic.starts_withBI());
builtins.put("string", new BuiltInsForMultipleTypes.stringBI());
builtins.put("substring", new BuiltInsForStringsBasic.substringBI());
@@ -241,7 +248,6 @@
builtins.put("matches", new BuiltInsForStringsRegexp.matchesBI());
builtins.put("groups", new BuiltInsForStringsRegexp.groupsBI());
builtins.put("replace", new BuiltInsForStringsRegexp.replace_reBI());
- builtins.put("split", new BuiltInsForStringsRegexp.split_BI());
}
static BuiltIn newBuiltIn(int incompatibleImprovements, Expression target, String key) throws ParseException {
@@ -355,6 +361,14 @@
}
}
+ protected final TemplateModelException newMethodArgInvalidValueException(int argIdx, Object[] details) {
+ return MessageUtil.newMethodArgInvalidValueException("?" + key, argIdx, details);
+ }
+
+ protected final TemplateModelException newMethodArgsInvalidValueException(Object[] details) {
+ return MessageUtil.newMethodArgsInvalidValueException("?" + key, details);
+ }
+
protected final Expression deepCloneWithIdentifierReplaced_inner(
String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
try {
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
index c8323fd..bdc9f28 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
@@ -2,12 +2,16 @@
import java.util.List;
import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleNumber;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateException;
+import freemarker.template.TemplateMethodModel;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
@@ -87,6 +91,74 @@
}
}
+ static class ensure_ends_withBI extends BuiltInForString {
+
+ private class BIMethod implements TemplateMethodModelEx {
+ private String s;
+
+ private BIMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args, 1);
+ String suffix = getStringMethodArg(args, 0);
+ return new SimpleScalar(s.endsWith(suffix) ? s : s + suffix);
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+ return new BIMethod(s);
+ }
+ }
+
+ static class ensure_starts_withBI extends BuiltInForString {
+
+ private class BIMethod implements TemplateMethodModelEx {
+ private String s;
+
+ private BIMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args, 1, 3);
+
+ final String checkedPrefix = getStringMethodArg(args, 0);
+
+ final boolean startsWithPrefix;
+ final String addedPrefix;
+ if (args.size() > 1) {
+ addedPrefix = getStringMethodArg(args, 1);
+ long flags = args.size() > 2
+ ? RegexpHelper.parseFlagString(getStringMethodArg(args, 2))
+ : RegexpHelper.RE_FLAG_REGEXP;
+
+ if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+ RegexpHelper.checkNonRegexpFlags(key, flags, true);
+ if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+ startsWithPrefix = s.startsWith(checkedPrefix);
+ } else {
+ startsWithPrefix = s.toLowerCase().startsWith(checkedPrefix.toLowerCase());
+ }
+ } else {
+ Pattern pattern = RegexpHelper.getPattern(checkedPrefix, (int) flags);
+ final Matcher matcher = pattern.matcher(s);
+ startsWithPrefix = matcher.lookingAt();
+ }
+ } else {
+ startsWithPrefix = s.startsWith(checkedPrefix);
+ addedPrefix = checkedPrefix;
+ }
+ return new SimpleScalar(startsWithPrefix ? s : addedPrefix + s);
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+ return new BIMethod(s);
+ }
+ }
+
static class index_ofBI extends BuiltIn {
private class BIMethod implements TemplateMethodModelEx {
@@ -121,21 +193,106 @@
"For sequences/collections (lists and such) use \"?seq_index_of\" instead."));
}
}
+
+ static class keep_afterBI extends BuiltInForString {
+ class KeepAfterMethod implements TemplateMethodModelEx {
+ private String s;
+ KeepAfterMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ int argCnt = args.size();
+ checkMethodArgCount(argCnt, 1, 2);
+ String separatorString = getStringMethodArg(args, 0);
+ long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0;
+
+ int startIndex;
+ if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+ RegexpHelper.checkNonRegexpFlags(key, flags, true);
+ if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+ startIndex = s.indexOf(separatorString);
+ } else {
+ startIndex = s.toLowerCase().indexOf(separatorString.toLowerCase());
+ }
+ if (startIndex >= 0) {
+ startIndex += separatorString.length();
+ }
+ } else {
+ Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags);
+ final Matcher matcher = pattern.matcher(s);
+ if (matcher.find()) {
+ startIndex = matcher.end();
+ } else {
+ startIndex = -1;
+ }
+ }
+ return startIndex == -1 ? SimpleScalar.EMPTY_STRING : new SimpleScalar(s.substring(startIndex));
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+ return new KeepAfterMethod(s);
+ }
+
+ }
+
+ static class keep_beforeBI extends BuiltInForString {
+ class KeepUntilMethod implements TemplateMethodModelEx {
+ private String s;
+
+ KeepUntilMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ int argCnt = args.size();
+ checkMethodArgCount(argCnt, 1, 2);
+ String separatorString = getStringMethodArg(args, 0);
+ long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0;
+
+ int stopIndex;
+ if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+ RegexpHelper.checkNonRegexpFlags(key, flags, true);
+ if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) {
+ stopIndex = s.indexOf(separatorString);
+ } else {
+ stopIndex = s.toLowerCase().indexOf(separatorString.toLowerCase());
+ }
+ } else {
+ Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags);
+ final Matcher matcher = pattern.matcher(s);
+ if (matcher.find()) {
+ stopIndex = matcher.start();
+ } else {
+ stopIndex = -1;
+ }
+ }
+ return stopIndex == -1 ? new SimpleScalar(s) : new SimpleScalar(s.substring(0, stopIndex));
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+ return new KeepUntilMethod(s);
+ }
+
+ }
+
static class lengthBI extends BuiltInForString {
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
return new SimpleNumber(s.length());
}
- }
+ }
static class lower_caseBI extends BuiltInForString {
TemplateModel calculateResult(String s, Environment env)
{
return new SimpleScalar(s.toLowerCase(env.getLocale()));
}
- }
+ }
static class padBI extends BuiltInForString {
@@ -185,7 +342,81 @@
return new BIMethod(s);
}
}
+
+ static class remove_beginningBI extends BuiltInForString {
+
+ private class BIMethod implements TemplateMethodModelEx {
+ private String s;
+
+ private BIMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args, 1);
+ String prefix = getStringMethodArg(args, 0);
+ return new SimpleScalar(s.startsWith(prefix) ? s.substring(prefix.length()) : s);
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+ return new BIMethod(s);
+ }
+ }
+ static class remove_endingBI extends BuiltInForString {
+
+ private class BIMethod implements TemplateMethodModelEx {
+ private String s;
+
+ private BIMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args, 1);
+ String suffix = getStringMethodArg(args, 0);
+ return new SimpleScalar(s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s);
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+ return new BIMethod(s);
+ }
+ }
+
+ static class split_BI extends BuiltInForString {
+ class SplitMethod implements TemplateMethodModel {
+ private String s;
+
+ SplitMethod(String s) {
+ this.s = s;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ int argCnt = args.size();
+ checkMethodArgCount(argCnt, 1, 2);
+ String splitString = (String) args.get(0);
+ long flags = argCnt > 1 ? RegexpHelper.parseFlagString((String) args.get(1)) : 0;
+ String[] result = null;
+ if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
+ RegexpHelper.checkNonRegexpFlags("split", flags);
+ result = StringUtil.split(s, splitString,
+ (flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) != 0);
+ } else {
+ Pattern pattern = RegexpHelper.getPattern(splitString, (int) flags);
+ result = pattern.split(s);
+ }
+ return ObjectWrapper.DEFAULT_WRAPPER.wrap(result);
+ }
+ }
+
+ TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
+ return new SplitMethod(s);
+ }
+
+ }
+
static class starts_withBI extends BuiltInForString {
private class BIMethod implements TemplateMethodModelEx {
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java b/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
index 32dcca7..78adacb 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
@@ -21,7 +21,6 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateBooleanModel;
@@ -126,38 +125,6 @@
}
- static class split_BI extends BuiltInForString {
- class SplitMethod implements TemplateMethodModel {
- private String s;
-
- SplitMethod(String s) {
- this.s = s;
- }
-
- public Object exec(List args) throws TemplateModelException {
- int argCnt = args.size();
- checkMethodArgCount(argCnt, 1, 2);
- String splitString = (String) args.get(0);
- long flags = argCnt > 1 ? RegexpHelper.parseFlagString((String) args.get(1)) : 0;
- String[] result = null;
- if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) {
- RegexpHelper.checkNonRegexpFlags("split", flags);
- result = StringUtil.split(s, splitString,
- (flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) != 0);
- } else {
- Pattern pattern = RegexpHelper.getPattern(splitString, (int) flags);
- result = pattern.split(s);
- }
- return ObjectWrapper.DEFAULT_WRAPPER.wrap(result);
- }
- }
-
- TemplateModel calculateResult(String s, Environment env) throws TemplateModelException {
- return new SplitMethod(s);
- }
-
- }
-
// Represents the match
static class RegexMatchModel
diff --git a/src/main/java/freemarker/core/RegexpHelper.java b/src/main/java/freemarker/core/RegexpHelper.java
index 065ae98..9084942 100644
--- a/src/main/java/freemarker/core/RegexpHelper.java
+++ b/src/main/java/freemarker/core/RegexpHelper.java
@@ -146,27 +146,39 @@
return;
}
}
- message += " This will be an error in FreeMarker 2.4!";
+ message += " This will be an error in some later FreeMarker version!";
if (cnt + 1 == MAX_FLAG_WARNINGS_LOGGED) {
message += " [Will not log more regular expression flag problems until restart!]";
}
LOG.warn(message);
}
- static void checkNonRegexpFlags(String biName, long flags) {
- if (!flagWarningsEnabled) return;
+ static void checkNonRegexpFlags(String biName, long flags) throws _TemplateModelException {
+ checkNonRegexpFlags(biName, flags, false);
+ }
+
+ static void checkNonRegexpFlags(String biName, long flags, boolean strict)
+ throws _TemplateModelException {
+ if (!strict && !flagWarningsEnabled) return;
+ String flag;
if ((flags & RE_FLAG_MULTILINE) != 0) {
- logFlagWarning("?" + biName + " doesn't support the \"m\" flag "
- + "without the \"r\" flag.");
+ flag = "m";
+ } else if ((flags & RE_FLAG_DOTALL) != 0) {
+ flag = "s";
+ } else if ((flags & RE_FLAG_COMMENTS) != 0) {
+ flag = "c";
+ } else {
+ return;
}
- if ((flags & RE_FLAG_DOTALL) != 0) {
- logFlagWarning("?" + biName + " doesn't support the \"s\" flag "
- + "without the \"r\" flag.");
- }
- if ((flags & RE_FLAG_COMMENTS) != 0) {
- logFlagWarning("?" + biName + " doesn't support the \"c\" flag "
- + "without the \"r\" flag.");
+
+ final Object[] msg = new Object[] { "?", biName ," doesn't support the \"", flag, "\" flag "
+ + "without the \"r\" flag." };
+ if (strict) {
+ throw new _TemplateModelException(msg);
+ } else {
+ // Suppress error for backward compatibility
+ logFlagWarning(new _ErrorDescriptionBuilder(msg).toString());
}
}
diff --git a/src/manual/book.xml b/src/manual/book.xml
index db95744..3e9e79b 100644
--- a/src/manual/book.xml
+++ b/src/manual/book.xml
@@ -58,7 +58,7 @@
<para>FreeMarker is <link
xlink:href="http://www.fsf.org/philosophy/free-sw.html">Free</link>,
- released under a BSD-style license. It is <link
+ released under the Apache License, Version 2.0. It is <link
xlink:href="http://www.opensource.org/">OSI Certified Open Source
Software</link>. OSI Certified is a certification mark of the Open
Source Initiative.</para>
@@ -233,23 +233,24 @@
again for each visiting. This ensures that the displayed information
is always up-to-date.</para>
- <para>Now, you already may have noticed that the template contains no
- instructions regarding how to find out who the current visitor is, or
- how to query the database to find out what the latest product is. It
- seems it just already know these values. And indeed that's the case.
- An important idea behind FreeMarker (actually, behind Web MVC) is that
- presentation logic and "business logic" should be separated. In the
- template you only deal with presentation issues, that is, visual
- design issues, formatting issues. The data that will be displayed
- (such as the user name and so on) is prepared outside FreeMarker,
- usually by routines written in Java language or other general purpose
- language. So the template author doesn't have to know how these values
- are calculated. In fact, the way these values are calculated can be
- completely changed while the templates can remain the same, and also,
- the look of the page can be completely changed without touching
- anything but the template. This separation can be especially useful
- when the template authors (designers) and the programmers are
- different individuals.</para>
+ <para>As you see above, the template contains no instructions
+ regarding how to find out who the current visitor is, or how to query
+ the database to find out what the latest product is. It seems it just
+ already know these values. And indeed that's the case. An important
+ idea behind FreeMarker (actually, behind Web MVC) is that presentation
+ logic and "business logic" should be separated. In the template you
+ only deal with presentation issues, that is, visual design issues,
+ formatting issues. The data that will be displayed (such as the user
+ name and so on) is prepared outside FreeMarker, usually by routines
+ written in Java language or other general purpose language. So the
+ template author doesn't have to know how these values are calculated.
+ In fact, the way these values are calculated can be completely changed
+ while the templates can remain the same, and also, the look of the
+ page can be completely changed without touching anything but the
+ template. This separation can be especially useful when the template
+ authors (designers) and the programmers are different individuals, but
+ also helps managing application complexity even if the template author
+ and the programmer is the same person.</para>
<para><indexterm>
<primary>data-model</primary>
@@ -275,8 +276,9 @@
<para>(To prevent misunderstandings: The data-model is not a text
file, the above is just a visualization of a data-model for you. It's
- from Java objects, but let that be the problem of the Java
- programmers.)</para>
+ from Java objects, such as JavaBeans, <literal>Map</literal>-s,
+ <literal>List</literal>-s, etc., but let that be the problem of the
+ Java programmers.)</para>
<para>Compare this with what you saw in the template earlier:
<literal>${user}</literal> and
@@ -289,7 +291,7 @@
<literal>latestProduct</literal> directory. So
<literal>latestProduct.name</literal> is like saying
<literal>name</literal> in the <literal>latestProduct</literal>
- directory. But as I said, it was just a simile; there are no files or
+ directory. But as I said, it's just an analogy; there are no files or
directories here.</para>
<para>To recapitulate, a template and a data-model is needed for
@@ -2766,7 +2768,7 @@
instead of an error. (It should be an error as it's a
decreasing range.) Currently this bug is emulated for backward
compatibility, but you shouldn't utilize it, as in the future
- it will be certainly disabled.</para>
+ it will be certainly an error.</para>
</listitem>
</itemizedlist>
@@ -2786,6 +2788,18 @@
CDE
CDEF
CDEF</programlisting>
+
+ <note>
+ <para>Some of the typical use-cases of string slicing is covered
+ by convenient built-ins: <link
+ linkend="ref_builtin_remove_beginning"><literal>remove_beginning</literal></link>,
+ <link
+ linkend="ref_builtin_remove_ending"><literal>remove_ending</literal></link>,
+ <link
+ linkend="ref_builtin_keep_before"><literal>keep_before</literal></link>,
+ <link
+ linkend="ref_builtin_keep_after"><literal>keep_after</literal></link></para>
+ </note>
</section>
</section>
@@ -2932,6 +2946,13 @@
<para>Note above that slicing with length limited and right
unbounded ranges allow the starting index to be past the last item
<emphasis>by one</emphasis> (but no more).</para>
+
+ <note>
+ <para>To split a sequence to slices of a given size, you should
+ use the <link
+ linkend="ref_builtin_chunk"><literal>chunk</literal></link>
+ built-in.</para>
+ </note>
</section>
</section>
@@ -5444,8 +5465,8 @@
<chapter xml:id="pgui_quickstart">
<title>Getting Started</title>
- <para>Note that, if you are new to FreeMarker, you should read at least
- the <xref linkend="dgui_quickstart"/> before this chapter.</para>
+ <para>If you are new to FreeMarker, you should read at least the <xref
+ linkend="dgui_quickstart_basics"/> before this chapter.</para>
<section xml:id="pgui_quickstart_createconfiguration">
<title>Create a configuration instance</title>
@@ -5458,10 +5479,11 @@
<literal>freemarker.template.Configuration</literal> instance and
adjust its settings. A <literal>Configuration</literal> instance is a
central place to store the application level settings of FreeMarker.
- Also, it deals with the creation and caching of pre-parsed templates
- (i.e., <literal>Template</literal> objects).</para>
+ Also, it deals with the creation and <emphasis>caching</emphasis> of
+ pre-parsed templates (i.e., <literal>Template</literal>
+ objects).</para>
- <para>Probably you will <emphasis>do it only once</emphasis> at the
+ <para>Normally you will <emphasis>do this only once</emphasis> at the
beginning of the application (possibly servlet) life-cycle:</para>
<programlisting role="unspecified">// Create your Configuration instance, and specify if up to what FreeMarker
@@ -5485,9 +5507,14 @@
configuration instance (i.e., its a singleton). Note however that if a
system has multiple independent components that use FreeMarker, then
of course they will use their own private
- <literal>Configuration</literal> instances. Do not needlessly
- re-create configuration instances; it's expensive as you lose the
- caches.</para>
+ <literal>Configuration</literal> instances.</para>
+
+ <warning>
+ <para>Do not needlessly re-create <literal>Configuration</literal>
+ instances; it's expensive, among others because you lose the caches.
+ <literal>Configuration</literal> instances meant to
+ application-level singletons.</para>
+ </warning>
<para>When using in multi-threaded applications (like for Web sites),
the settings in the <literal>Configuration</literal> instance must not
@@ -5617,9 +5644,9 @@
<para><literal>Configuration</literal> caches
<literal>Template</literal> instances, so when you get
- <literal>test.ftl</literal> again, it probably will not create new
- <literal>Template</literal> instance (thus doesn't read and parse the
- file), just returns the same instance as for the first time.</para>
+ <literal>test.ftl</literal> again, it probably won't read and parse
+ the template file again, just returns the same
+ <literal>Template</literal> instance as for the first time.</para>
</section>
<section xml:id="pgui_quickstart_merge">
@@ -5651,10 +5678,20 @@
in the <link linkend="example.first">first example</link> of the
Template Author's Guide.</para>
- <para>Once you have obtained a <literal>Template</literal> instance,
- you can merge it with different data-models for unlimited times
- (<literal>Template</literal> instances are basically stateless). Also,
- the <literal>test.ftl</literal> file is accessed only while the
+ <para>Java I/O related notes: Depending on what <literal>out</literal>
+ is, you may need to ensure that <literal>out.close()</literal> is
+ called. This is typically needed when <literal>out</literal> writes
+ into a file that was opened to store the output of the template. In
+ other times, like in typical Web applications, you must
+ <emphasis>not</emphasis> close <literal>out</literal>. FreeMarker
+ calls <literal>out.flush()</literal> after a successful template
+ execution (can be disabled in <literal>Configuration</literal>), so
+ you don't need to worry about that.</para>
+
+ <para>Note that once you have obtained a <literal>Template</literal>
+ instance, you can merge it with different data-models for unlimited
+ times (<literal>Template</literal> instances are stateless). Also, the
+ <literal>test.ftl</literal> file is accessed only while the
<literal>Template</literal> instance is created, not when you call the
process method.</para>
</section>
@@ -5677,14 +5714,14 @@
/* ------------------------------------------------------------------------ */
/* You should do this ONLY ONCE in the whole application life-cycle: */
- /* Create and adjust the configuration */
+ /* Create and adjust the configuration singleton */
Configuration cfg = new Configuration(Configuration.VERSION_2_3_21);
cfg.setDirectoryForTemplateLoading(new File("<replaceable>/where/you/store/templates</replaceable>"));
cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
/* ------------------------------------------------------------------------ */
- /* You usually do these for many times in the application life-cycle: */
+ /* You usually do these for MULTIPLE TIMES in the application life-cycle: */
/* Create a data-model */
Map root = new HashMap();
@@ -5694,12 +5731,14 @@
latest.put("url", "products/greenmouse.html");
latest.put("name", "green mouse");
- /* Get the template */
+ /* Get the template (uses cache internally) */
Template temp = cfg.getTemplate("test.ftl");
/* Merge data-model with template */
Writer out = new OutputStreamWriter(System.out);
temp.process(root, out);
+ // Note: Depending on what `out` is, you may need to call `out.close()`.
+ // This is usually the case for file output, but not for servlet output.
}
}</programlisting>
@@ -11504,6 +11543,16 @@
</listitem>
<listitem>
+ <para><link
+ linkend="ref_builtin_ensure_ends_with">ensure_ends_with</link></para>
+ </listitem>
+
+ <listitem>
+ <para><link
+ linkend="ref_builtin_ensure_starts_with">ensure_starts_with</link></para>
+ </listitem>
+
+ <listitem>
<para><link linkend="ref_builtin_eval">eval</link></para>
</listitem>
@@ -11578,6 +11627,16 @@
</listitem>
<listitem>
+ <para><link
+ linkend="ref_builtin_keep_after">keep_after</link></para>
+ </listitem>
+
+ <listitem>
+ <para><link
+ linkend="ref_builtin_keep_before">keep_before</link></para>
+ </listitem>
+
+ <listitem>
<para><link linkend="ref_builtin_keys">keys</link></para>
</listitem>
@@ -11653,6 +11712,16 @@
</listitem>
<listitem>
+ <para><link
+ linkend="ref_builtin_remove_beginning">remove_beginning</link></para>
+ </listitem>
+
+ <listitem>
+ <para><link
+ linkend="ref_builtin_remove_ending">remove_ending</link></para>
+ </listitem>
+
+ <listitem>
<para><link linkend="ref_builtin_reverse">reverse</link></para>
</listitem>
@@ -12035,10 +12104,61 @@
<primary>ends_with built-in</primary>
</indexterm>
- <para>Returns if this string ends with the specified substring. For
- example <literal>"redhead"?ends_with("head")</literal> returns
- boolean true. Also, <literal>"head"?ends_with("head")</literal> will
- return true.</para>
+ <para>Returns whether this string ends with the substring specified
+ in the parameter. For example
+ <literal>"ahead"?ends_with("head")</literal> returns boolean
+ <literal>true</literal>. Also,
+ <literal>"head"?ends_with("head")</literal> will return
+ <literal>true</literal>.</para>
+ </section>
+
+ <section xml:id="ref_builtin_ensure_ends_with">
+ <title>ensure_ends_with</title>
+
+ <indexterm>
+ <primary>ensure_ends_with built-in</primary>
+ </indexterm>
+
+ <para>If the string doesn't end with the substring specified as the
+ 1st parameter, it adds it after the string, otherwise it returns the
+ original string. For example, both
+ <literal>"foo"?ensure_ends_with("/")</literal> and
+ <literal>"foo"?ensure_ends_with("/")</literal> returns
+ <literal>"foo/"</literal>.</para>
+ </section>
+
+ <section xml:id="ref_builtin_ensure_starts_with">
+ <title>ensure_starts_with</title>
+
+ <indexterm>
+ <primary>ensure_ends_with built-in</primary>
+ </indexterm>
+
+ <para>If the string doesn't start with the substring specified as
+ the 1st parameter, it adds it before the string, otherwise it
+ returns the original string. For example, both
+ <literal>"foo"?ensure_starts_with("/")</literal> and
+ <literal>"/foo"?ensure_starts_with("/")</literal> returns
+ <literal>"/foo"</literal>.</para>
+
+ <para>If you specify two parameters, then the 1st parameter is
+ interpreted as a Java regular expression, and if it doesn't match
+ the beginning of the string, then the string specified as the 2nd
+ parameter is added before the string. For example
+ <literal>someURL?ensure_starts_with("[a-zA-Z]+://",
+ "http://")</literal> will check if the string starts with something
+ that matches <literal>"[a-zA-Z]+://"</literal> (note that no
+ <literal>^</literal> is needed), and if it doesn't, it prepends
+ <literal>"http://"</literal>.</para>
+
+ <para>This method also accepts a 3rd <link
+ linkend="ref_builtin_string_flags">flags parameter</link>. As
+ calling with 2 parameters implies <literal>"r"</literal> there
+ (i.e., regular expression mode), you rarely need this. One notable
+ case is when you don't want the 1st parameter to be interpreted as a
+ regular expression, only as plain text, but you want the comparison
+ to be case-insensitive, in which case you would use
+ <literal>"i"</literal> as the 3rd parameter.</para>
</section>
<section xml:id="ref_builtin_groups">
@@ -12238,6 +12358,68 @@
(<literal>\u<replaceable>XXXX</replaceable></literal>).</para>
</section>
+ <section xml:id="ref_builtin_keep_after">
+ <title>keep_after</title>
+
+ <indexterm>
+ <primary>keep_after built-in</primary>
+ </indexterm>
+
+ <para>Removes the part of the string that is not after the first
+ occurrence of the given substring. For example:</para>
+
+ <programlisting role="template">${"abcdefgh"?keep_until("de")}</programlisting>
+
+ <para>will print</para>
+
+ <programlisting role="output">fgh</programlisting>
+
+ <para>If the parameter string is not found, it will return an empty
+ string. If the parameter string is a 0-length string, it will return
+ the original string unchanged.</para>
+
+ <para>This method accepts an optional <link
+ linkend="ref_builtin_string_flags">flags parameter</link>, as its
+ 2nd parameter:</para>
+
+ <programlisting role="template">${"foo : bar"?keep_until(r"\s*:\s*", "r")}</programlisting>
+
+ <para>will print</para>
+
+ <programlisting role="output">bar</programlisting>
+ </section>
+
+ <section xml:id="ref_builtin_keep_before">
+ <title>keep_before</title>
+
+ <indexterm>
+ <primary>keep_before built-in</primary>
+ </indexterm>
+
+ <para>Removes the part of the string that starts with the given
+ substring. For example:</para>
+
+ <programlisting role="template">${"abcdef"?keep_before("de")}</programlisting>
+
+ <para>will print</para>
+
+ <programlisting role="output">abc</programlisting>
+
+ <para>If the parameter string is not found, it will return the
+ original string unchanged. If the parameter string is a 0-length
+ string, it will return an empty string.</para>
+
+ <para>This method accepts an optional <link
+ linkend="ref_builtin_string_flags">flags parameter</link>, as its
+ 2nd parameter:</para>
+
+ <programlisting role="template">${"foo : bar"?keep_before(r"\s*:\s*", "r")}</programlisting>
+
+ <para>will print</para>
+
+ <programlisting role="output">foo</programlisting>
+ </section>
+
<section xml:id="ref_builtin_last_index_of">
<title>last_index_of</title>
@@ -12605,6 +12787,46 @@
[abcdoO.o]</programlisting>
</section>
+ <section xml:id="ref_builtin_remove_beginning">
+ <title>remove_beginning</title>
+
+ <indexterm>
+ <primary>remove_beginning built-in</primary>
+ </indexterm>
+
+ <para>Removes the parameter substring from the beginning of the
+ string, or returns the original string if it doesn't start with the
+ parameter substring. For example:</para>
+
+ <programlisting role="template">${"abcdef"?remove_beginning("abc")}
+${"foobar"?remove_beginning("abc")}</programlisting>
+
+ <para>will print:</para>
+
+ <programlisting role="output">def
+foobar</programlisting>
+ </section>
+
+ <section xml:id="ref_builtin_remove_ending">
+ <title>remove_ending</title>
+
+ <indexterm>
+ <primary>remove_ending built-in</primary>
+ </indexterm>
+
+ <para>Removes the parameter substring from the beginning of the
+ string, or returns the original string if it doesn't start with the
+ parameter substring. For example:</para>
+
+ <programlisting role="template">${"abcdef"?remove_ending("def")}
+${"foobar"?remove_ending("def")}</programlisting>
+
+ <para>will print:</para>
+
+ <programlisting role="output">abc
+foobar</programlisting>
+ </section>
+
<section xml:id="ref_builtin_rtf">
<title>rtf</title>
@@ -12682,6 +12904,12 @@
elements from the end of the resulting list, so with
<literal>?split(",", "r")</literal> in the last example the last
<literal>""</literal> would be missing from the output.</para>
+
+ <note>
+ <para>To check if a strings ends with something and append it
+ otherwise, use <link linkend="ref_builtin_ensure_ends_with">the
+ <literal>ensure_ends_with</literal> built-in</link>.</para>
+ </note>
</section>
<section xml:id="ref_builtin_starts_with">
@@ -12692,9 +12920,16 @@
</indexterm>
<para>Returns if this string starts with the specified substring.
- For example <literal>"redhead"?starts_with("red")</literal> returns
- boolean true. Also, <literal>"red"?starts_with("red")</literal> will
- return true.</para>
+ For example <literal>"redirect"?starts_with("red")</literal> returns
+ boolean <literal>true</literal>. Also,
+ <literal>"red"?starts_with("red")</literal> will return
+ <literal>true</literal>.</para>
+
+ <note>
+ <para>To check if a strings starts with something and prepend it
+ otherwise, use <link linkend="ref_builtin_ensure_starts_with">the
+ <literal>ensure_starts_with</literal> built-in</link>.</para>
+ </note>
</section>
<section xml:id="ref_builtin_string_for_string">
@@ -13153,10 +13388,9 @@
<link linkend="gloss.regularExpression">regular
expression</link>. FreeMarker uses the variation of regular
expressions described at <link
- xlink:href="http://java.sun.com/j2se/1.4.1/docs/api/java/util/regex/Pattern.html">http://java.sun.com/j2se/1.4.1/docs/api/java/util/regex/Pattern.html</link>.
- <emphasis>This flag will work only if you use Java2 platform 1.4
- or later. Otherwise it will cause template processing to stop
- with error.</emphasis></para>
+ xlink:href="http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html">http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html</link>
+ (note that the presence of some pattern features depends on the
+ Java version used).</para>
</listitem>
<listitem>
@@ -21238,15 +21472,19 @@
<title>Version history</title>
<section xml:id="versions_2_3_21_rc2">
- <title>2.3.21 RC2</title>
+ <title>2.3.21 RC2 (compared to RC1)</title>
- <para>Date of release for release: 2014-09-FIXME</para>
+ <para>Date of release for release: 2014-09-28</para>
<para>Date of release for the final release: Planned for
2014-10-12.</para>
- <para>See also <link linkend="versions_2_3_21_rc1">the RC1
- changes</link> below!</para>
+ <para>Also, check out <link linkend="versions_2_3_21_rc1">the RC1
+ changes here</link>!</para>
+
+ <para>Please give feedback if you find any problems, or you have any
+ comments on the new features! This is the last chance before things
+ get graved into stone, in October the 12th!</para>
<section>
<title>Changes on the FTL side</title>
@@ -21276,15 +21514,58 @@
</listitem>
<listitem>
+ <para>New built-ins for string manipulation:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para><literal><replaceable>someString</replaceable>?keep_before(<replaceable>substring</replaceable>[,
+ <replaceable>flags</replaceable>])</literal>: <link
+ linkend="ref_builtin_keep_before">More...</link></para>
+ </listitem>
+
+ <listitem>
+ <para><literal><replaceable>someString</replaceable>?keep_after(<replaceable>substring</replaceable>[,
+ <replaceable>flags</replaceable>])</literal>: <link
+ linkend="ref_builtin_keep_after">More...</link></para>
+ </listitem>
+
+ <listitem>
+ <para><literal><replaceable>someString</replaceable>?remove_beginning(<replaceable>substring</replaceable>)</literal>:
+ <link
+ linkend="ref_builtin_remove_beginning">More...</link></para>
+ </listitem>
+
+ <listitem>
+ <para><literal><replaceable>someString</replaceable>?remove_ending(<replaceable>substring</replaceable>)</literal>:
+ <link
+ linkend="ref_builtin_remove_ending">More...</link></para>
+ </listitem>
+
+ <listitem>
+ <para><literal><replaceable>someString</replaceable>?ensure_starts_with(<replaceable>substring</replaceable>[,
+ <replaceable>substitution</replaceable>[,
+ <replaceable>flags</replaceable>]])</literal>: <link
+ linkend="ref_builtin_ensure_starts_with">More...</link></para>
+ </listitem>
+
+ <listitem>
+ <para><literal><replaceable>someString</replaceable>?ensure_ends_with(<replaceable>substring</replaceable>)</literal>:
+ <link
+ linkend="ref_builtin_ensure_ends_with">More...</link></para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
+
+ <listitem>
<para>Bug fixed [<link
xlink:href="http://sourceforge.net/p/freemarker/bugs/257/">257</link>]:
- The result of <literal>?matches</literal> wasn't
+ The result value of <literal>?matches</literal> wasn't
<quote>reentrant</quote>. For example, you couldn't list the
matches inside another listing where you are also listing
- exactly the same result value, as they would consume from the
- same iterator. Most importantly, even accessing the
- <literal>?size</literal> of the same result value has terminated
- the outer listing of the same value.</para>
+ exactly the same result value (stored in a common variable), as
+ they would consume from the same iterator. Most importantly,
+ even accessing the <literal>?size</literal> of the same result
+ value has terminated the outer listing of the same value.</para>
</listitem>
<listitem>
@@ -21305,7 +21586,15 @@
<itemizedlist>
<listitem>
<para>The <literal>getResult()</literal> method of builder
- classes was renamed to <literal>build()</literal>.</para>
+ classes was renamed to <literal>build()</literal>, as that's a
+ more popular choice.</para>
+ </listitem>
+
+ <listitem>
+ <para>OSGi execution environment header was tweaked again, now
+ it's <literal>Bundle-RequiredExecutionEnvironment: J2SE-1.5,
+ J2SE-1.4</literal>. That is, we prefer (and compile against)
+ 1.5, but only require 1.4.</para>
</listitem>
<listitem>
@@ -21325,7 +21614,7 @@
</listitem>
<listitem>
- <para>Bugs found in R1 fixed:</para>
+ <para>Bugs introduced with RC1 fixed:</para>
<itemizedlist>
<listitem>
@@ -21338,6 +21627,13 @@
</listitem>
</itemizedlist>
</section>
+
+ <section>
+ <title>Reminder</title>
+
+ <para>Check out <link linkend="versions_2_3_21_rc1">the RC1
+ changes</link> too, as they aren't included in this section!</para>
+ </section>
</section>
<section xml:id="versions_2_3_21_rc1">
@@ -21384,6 +21680,8 @@
<para>Note that the minimum required Java version was increased from
1.2 to 1.4.</para>
+ <para>Please give feedback if you find any problems!</para>
+
<section>
<title>Changes on the FTL side</title>
@@ -30058,7 +30356,7 @@
familiar with them. But if you are interested in regular expressions,
you can find several Web pages and books about them. FreeMarker uses
the variation of regular expressions described at: <link
- xlink:href="http://java.sun.com/j2se/1.4.1/docs/api/java/util/regex/Pattern.html">http://java.sun.com/j2se/1.4.1/docs/api/java/util/regex/Pattern.html</link></para>
+ xlink:href="http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html">http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html</link></para>
</glossdef>
</glossentry>
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl b/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl
new file mode 100644
index 0000000..3770109
--- /dev/null
+++ b/src/test/resources/freemarker/test/templatesuite/templates/string-builtins3.ftl
@@ -0,0 +1,121 @@
+<@assertEquals expected='foo' actual='foo'?keep_before('x') />
+<@assertEquals expected='f' actual='foo'?keep_before('o') />
+<@assertEquals expected='' actual='foo'?keep_before('f') />
+<@assertEquals expected='fo' actual='foobar'?keep_before('ob') />
+<@assertEquals expected='foob' actual='foobar'?keep_before('ar') />
+<@assertEquals expected='' actual='foobar'?keep_before('foobar') />
+<@assertEquals expected='' actual='foobar'?keep_before('') />
+<@assertEquals expected='' actual='foobar'?keep_before('', 'r') />
+<@assertEquals expected='FOO' actual='FOO'?keep_before('o') />
+<@assertEquals expected='F' actual='FOO'?keep_before('o', 'i') />
+<@assertEquals expected='fo' actual='fo.o'?keep_before('.') />
+<@assertEquals expected='' actual='fo.o'?keep_before('.', 'r') />
+<@assertEquals expected='FOOb' actual='FOObaar'?keep_before(r'([a-z])\1', 'r') />
+<@assertEquals expected='F' actual='FOObaar'?keep_before(r'([a-z])\1', 'ri') />
+<@assertEquals expected='foo' actual="foo : bar"?keep_before(r"\s*:\s*", "r") />
+<@assertEquals expected='foo' actual="foo:bar"?keep_before(r"\s*:\s*", "r") />
+<@assertFails message='"m" flag'>
+ ${'x'?keep_before('x', 'm')}
+</@assertFails>
+<@assertFails message='3'>
+ ${'x'?keep_before('x', 'i', 'x')}
+</@assertFails>
+<@assertFails message='none'>
+ ${'x'?keep_before()}
+</@assertFails>
+
+<@assertEquals expected='' actual='foo'?keep_after('x') />
+<@assertEquals expected='o' actual='foo'?keep_after('o') />
+<@assertEquals expected='oo' actual='foo'?keep_after('f') />
+<@assertEquals expected='ar' actual='foobar'?keep_after('ob') />
+<@assertEquals expected='' actual='foobar'?keep_after('ar') />
+<@assertEquals expected='' actual='foobar'?keep_after('foobar') />
+<@assertEquals expected='foobar' actual='foobar'?keep_after('') />
+<@assertEquals expected='foobar' actual='foobar'?keep_after('', 'r') />
+<@assertEquals expected='' actual='FOO'?keep_after('o') />
+<@assertEquals expected='O' actual='FOO'?keep_after('o', 'i') />
+<@assertEquals expected='o' actual='fo.o'?keep_after('.') />
+<@assertEquals expected='o.o' actual='fo.o'?keep_after('.', 'r') />
+<@assertEquals expected='r' actual='FOObaar'?keep_after(r'([a-z])\1', 'r') />
+<@assertEquals expected='baar' actual='FOObaar'?keep_after(r'([a-z])\1', 'ri') />
+<@assertEquals expected='bar' actual="foo : bar"?keep_after(r"\s*:\s*", "r") />
+<@assertEquals expected='bar' actual="foo:bar"?keep_after(r"\s*:\s*", "r") />
+<@assertFails message='"m" flag'>
+ ${'x'?keep_after('x', 'm')}
+</@assertFails>
+<@assertFails message='3'>
+ ${'x'?keep_after('x', 'i', 'x')}
+</@assertFails>
+<@assertFails message='none'>
+ ${'x'?keep_after()}
+</@assertFails>
+
+<@assertEquals expected='foo' actual='foo'?remove_beginning('x') />
+<@assertEquals expected='foo' actual='foo'?remove_beginning('o') />
+<@assertEquals expected='foo' actual='foo'?remove_beginning('fooo') />
+<@assertEquals expected='oo' actual='foo'?remove_beginning('f') />
+<@assertEquals expected='o' actual='foo'?remove_beginning('fo') />
+<@assertEquals expected='' actual='foo'?remove_beginning('foo') />
+<@assertEquals expected='foo' actual='foo'?remove_beginning('') />
+<@assertFails message='2'>
+ ${'x'?remove_beginning('x', 'x')}
+</@assertFails>
+<@assertFails message='none'>
+ ${'x'?remove_beginning()}
+</@assertFails>
+
+<@assertEquals expected='bar' actual='bar'?remove_ending('x') />
+<@assertEquals expected='bar' actual='bar'?remove_ending('a') />
+<@assertEquals expected='bar' actual='bar'?remove_ending('barr') />
+<@assertEquals expected='ba' actual='bar'?remove_ending('r') />
+<@assertEquals expected='b' actual='bar'?remove_ending('ar') />
+<@assertEquals expected='' actual='bar'?remove_ending('bar') />
+<@assertEquals expected='bar' actual='bar'?remove_ending('') />
+<@assertFails message='2'>
+ ${'x'?remove_ending('x', 'x')}
+</@assertFails>
+<@assertFails message='none'>
+ ${'x'?remove_ending()}
+</@assertFails>
+
+<@assertEquals expected='xfoo' actual='foo'?ensure_starts_with('x') />
+<@assertEquals expected='foo' actual='foo'?ensure_starts_with('f') />
+<@assertEquals expected='foo' actual='foo'?ensure_starts_with('foo') />
+<@assertEquals expected='fooofoo' actual='foo'?ensure_starts_with('fooo') />
+<@assertEquals expected='foo' actual='foo'?ensure_starts_with('') />
+<@assertEquals expected='x' actual=''?ensure_starts_with('x') />
+<@assertEquals expected='' actual=''?ensure_starts_with('') />
+<@assertEquals expected='bacdef' actual="bacdef"?ensure_starts_with("[ab]{2}", "ab") />
+<@assertEquals expected='bacdef' actual="bacdef"?ensure_starts_with("^[ab]{2}", "ab") />
+<@assertEquals expected='abcacdef' actual="cacdef"?ensure_starts_with("[ab]{2}", "ab") />
+<@assertEquals expected='abcacdef' actual="cacdef"?ensure_starts_with("^[ab]{2}", "ab") />
+<@assertEquals expected='ab!cdef' actual="cdef"?ensure_starts_with("ab", "ab!") />
+<@assertEquals expected='ab!ABcdef' actual="ABcdef"?ensure_starts_with("ab", "ab!") />
+<@assertEquals expected='ABcdef' actual="ABcdef"?ensure_starts_with("ab", "ab!", 'i') />
+<@assertEquals expected='abABcdef' actual="ABcdef"?ensure_starts_with(".b", "ab", 'i') />
+<@assertEquals expected='ABcdef' actual="ABcdef"?ensure_starts_with(".b", "ab", 'ri') />
+<@assertEquals expected='http://example.com' actual="example.com"?ensure_starts_with("[a-z]+://", "http://") />
+<@assertEquals expected='http://example.com' actual="http://example.com"?ensure_starts_with("[a-z]+://", "http://") />
+<@assertEquals expected='https://example.com' actual="https://example.com"?ensure_starts_with("[a-z]+://", "http://") />
+<@assertEquals expected='http://HTTP://example.com' actual="HTTP://example.com"?ensure_starts_with("[a-z]+://", "http://") />
+<@assertEquals expected='HTTP://example.com' actual="HTTP://example.com"?ensure_starts_with("[a-z]+://", "http://", "ir") />
+<@assertFails message='4'>
+ ${'x'?ensure_starts_with('x', 'x', 'x', 'x')}
+</@assertFails>
+<@assertFails message='none'>
+ ${'x'?ensure_starts_with()}
+</@assertFails>
+
+<@assertEquals expected='foox' actual='foo'?ensure_ends_with('x') />
+<@assertEquals expected='foo' actual='foo'?ensure_ends_with('o') />
+<@assertEquals expected='foo' actual='foo'?ensure_ends_with('foo') />
+<@assertEquals expected='foofooo' actual='foo'?ensure_ends_with('fooo') />
+<@assertEquals expected='foo' actual='foo'?ensure_ends_with('') />
+<@assertEquals expected='x' actual=''?ensure_ends_with('x') />
+<@assertEquals expected='' actual=''?ensure_ends_with('') />
+<@assertFails message='2'>
+ ${'x'?ensure_ends_with('x', 'x')}
+</@assertFails>
+<@assertFails message='none'>
+ ${'x'?ensure_ends_with()}
+</@assertFails>
diff --git a/src/test/resources/freemarker/test/templatesuite/testcases.xml b/src/test/resources/freemarker/test/templatesuite/testcases.xml
index 6abba9b..b7782d4 100644
--- a/src/test/resources/freemarker/test/templatesuite/testcases.xml
+++ b/src/test/resources/freemarker/test/templatesuite/testcases.xml
@@ -134,6 +134,7 @@
<testcase name="existence-operators" nooutput="true" />
<testcase name="string-builtins1" />
<testcase name="string-builtins2" />
+ <testcase name="string-builtins3" nooutput="true" />
<testcase name="string-builtins-regexps" />
<testcase name="string-builtins-regexps-matches" />
<testcase name="stringbimethods" />