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" />