Repository: PebbleTemplates/pebble (java)

Issue:
Provide support for Servlet 5.0
Pebble depends on the Servlet API 2.5 at the moment. Servlet 5.0, which will be part of the Jakarta EE 9 release, migrated the javax.* to the new jakarta.* package namespace. It would be great to see a Pebble version that supports this new package namespace.

Are there any plans for this at the moment?

Pull request description:
Added Servlet5Loader to support Servlet 5.0 (fixes #548)
This PR adds support for Servlet 5.0, which will be part of Jakarta EE 9 (the successor of Java EE), which will be released in about 2 weeks.

The PR contains the following changes.

  * Most of the existing code from `ServletLoader` has be moved to `AbstractServletLoader`.
  * A new loader named `Servlet5Loader` has been added. The code is essentially the same as in `ServletLoader`.

What do you think?



Patch:
diff --git a/docs/src/orchid/resources/wiki/guide/installation.md b/docs/src/orchid/resources/wiki/guide/installation.md
index cbc71198..05373cbf 100644
--- a/docs/src/orchid/resources/wiki/guide/installation.md
+++ b/docs/src/orchid/resources/wiki/guide/installation.md
@@ -65,6 +65,7 @@ Pebble ships with the following loader implementations:
 - `FileLoader`:  Finds templates using a filesystem path.
 - `ServletLoader`:  Uses a servlet context to find the template. This is the recommended loader for use within an
 application server but is not enabled by default.
+- `Servlet5Loader`:  Same as `ServletLoader`, but for Jakarta Servlet 5.0 or newer.
 - `StringLoader`: Considers the name of the template to be the contents of the template.
 - `DelegatingLoader`: Delegates responsibility to a collection of children loaders.
 
diff --git a/pebble/pom.xml b/pebble/pom.xml
index eb780dc7..fa1cf102 100644
--- a/pebble/pom.xml
+++ b/pebble/pom.xml
@@ -52,6 +52,12 @@
       <version>${servlet-api.version}</version>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <artifactId>jakarta.servlet-api</artifactId>
+      <groupId>jakarta.servlet</groupId>
+      <version>5.0.0</version>
+      <optional>true</optional>
+    </dependency>
 
     <!-- testing dependencies -->
     <dependency>
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/lexer/LexerImpl.java b/pebble/src/main/java/com/mitchellbosecke/pebble/lexer/LexerImpl.java
index f9f8671a..c18dcc0e 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/lexer/LexerImpl.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/lexer/LexerImpl.java
@@ -662,7 +662,7 @@ public final class LexerImpl implements Lexer {
        */
       char nextChar = operator.charAt(operator.length() - 1);
       if (Character.isLetter(nextChar) || Character.getType(nextChar) == Character.LETTER_NUMBER) {
-        regex.append("(?![a-zA-Z])");
+        regex.append("(?![a-zA-Z0-9_])");
       }
     }
 
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/loader/AbstractServletLoader.java b/pebble/src/main/java/com/mitchellbosecke/pebble/loader/AbstractServletLoader.java
new file mode 100644
index 00000000..56a9e516
--- /dev/null
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/loader/AbstractServletLoader.java
@@ -0,0 +1,128 @@
+package com.mitchellbosecke.pebble.loader;
+
+import com.mitchellbosecke.pebble.error.LoaderException;
+import com.mitchellbosecke.pebble.utils.PathUtils;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for loaders which user the servlet context to load templates.
+ *
+ * @author mbosecke
+ * @author chkal
+ */
+public abstract class AbstractServletLoader implements Loader<String> {
+
+  private static final Logger logger = LoggerFactory.getLogger(AbstractServletLoader.class);
+
+  private String prefix;
+
+  private String suffix;
+
+  private String charset = "UTF-8";
+
+  private char expectedSeparator = '/';
+
+  protected abstract InputStream getResourceAsStream(String location);
+
+  protected abstract URL getResource(String location) throws MalformedURLException;
+
+  @Override
+  public Reader getReader(String templateName) {
+
+    InputStreamReader isr = null;
+    Reader reader = null;
+
+    InputStream is = null;
+    String location = this.getLocation(templateName);
+
+    logger.debug("Looking for template in {}.", location);
+
+    is = getResourceAsStream(location);
+
+    if (is == null) {
+      throw new LoaderException(null, "Could not find template \"" + location + "\"");
+    }
+
+    try {
+      isr = new InputStreamReader(is, this.charset);
+      reader = new BufferedReader(isr);
+    } catch (UnsupportedEncodingException e) {
+    }
+
+    return reader;
+  }
+
+  private String getLocation(String templateName) {
+    // Add the prefix and make sure that it ends with a separator character
+    StringBuilder path = new StringBuilder(128);
+    if (this.getPrefix() != null) {
+
+      path.append(this.getPrefix());
+
+      // we do NOT use OS dependent separators here; getResourceAsStream
+      // explicitly requires forward slashes.
+      if (!this.getPrefix().endsWith(Character.toString(this.expectedSeparator))) {
+        path.append(this.expectedSeparator);
+      }
+    }
+    path.append(templateName);
+    if (this.getSuffix() != null) {
+      path.append(this.getSuffix());
+    }
+    return path.toString();
+  }
+
+  public String getSuffix() {
+    return this.suffix;
+  }
+
+  @Override
+  public void setSuffix(String suffix) {
+    this.suffix = suffix;
+  }
+
+  public String getPrefix() {
+    return this.prefix;
+  }
+
+  @Override
+  public void setPrefix(String prefix) {
+    this.prefix = prefix;
+  }
+
+  public String getCharset() {
+    return this.charset;
+  }
+
+  @Override
+  public void setCharset(String charset) {
+    this.charset = charset;
+  }
+
+  @Override
+  public String resolveRelativePath(String relativePath, String anchorPath) {
+    return PathUtils.resolveRelativePath(relativePath, anchorPath, this.expectedSeparator);
+  }
+
+  @Override
+  public String createCacheKey(String templateName) {
+    return templateName;
+  }
+
+  @Override
+  public boolean resourceExists(String templateName) {
+    try {
+      return getResource(this.getLocation(templateName)) != null;
+    } catch (MalformedURLException e) {
+      return false;
+    }
+  }
+}
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/loader/Servlet5Loader.java b/pebble/src/main/java/com/mitchellbosecke/pebble/loader/Servlet5Loader.java
new file mode 100644
index 00000000..3fef106a
--- /dev/null
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/loader/Servlet5Loader.java
@@ -0,0 +1,31 @@
+package com.mitchellbosecke.pebble.loader;
+
+import jakarta.servlet.ServletContext;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Loader that uses a servlet context to find templates. Requires Jakarta Servlet 5.0 or newer.
+ *
+ * @author mbosecke
+ * @author chkal
+ */
+public class Servlet5Loader extends AbstractServletLoader {
+
+  private final ServletContext context;
+
+  public Servlet5Loader(ServletContext context) {
+    this.context = context;
+  }
+
+  @Override
+  protected InputStream getResourceAsStream(String location) {
+    return context.getResourceAsStream(location);
+  }
+
+  @Override
+  protected URL getResource(String location) throws MalformedURLException {
+    return context.getResource(location);
+  }
+}
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/loader/ServletLoader.java b/pebble/src/main/java/com/mitchellbosecke/pebble/loader/ServletLoader.java
index e6268fde..3facc90f 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/loader/ServletLoader.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/loader/ServletLoader.java
@@ -1,36 +1,17 @@
 package com.mitchellbosecke.pebble.loader;
 
-import com.mitchellbosecke.pebble.error.LoaderException;
-import com.mitchellbosecke.pebble.utils.PathUtils;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
 import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
-
+import java.net.URL;
 import javax.servlet.ServletContext;
 
 /**
  * Loader that uses a servlet context to find templates.
  *
  * @author mbosecke
+ * @author chkal
  */
-public class ServletLoader implements Loader<String> {
-
-  private static final Logger logger = LoggerFactory.getLogger(ServletLoader.class);
-
-  private String prefix;
-
-  private String suffix;
-
-  private String charset = "UTF-8";
-
-  private char expectedSeparator = '/';
+public class ServletLoader extends AbstractServletLoader {
 
   private final ServletContext context;
 
@@ -39,94 +20,12 @@ public class ServletLoader implements Loader<String> {
   }
 
   @Override
-  public Reader getReader(String templateName) {
-
-    InputStreamReader isr = null;
-    Reader reader = null;
-
-    InputStream is = null;
-    String location = this.getLocation(templateName);
-
-    logger.debug("Looking for template in {}.", location);
-
-    is = this.context.getResourceAsStream(location);
-
-    if (is == null) {
-      throw new LoaderException(null, "Could not find template \"" + location + "\"");
-    }
-
-    try {
-      isr = new InputStreamReader(is, this.charset);
-      reader = new BufferedReader(isr);
-    } catch (UnsupportedEncodingException e) {
-    }
-
-    return reader;
-  }
-
-  private String getLocation(String templateName) {
-    // Add the prefix and make sure that it ends with a separator character
-    StringBuilder path = new StringBuilder(128);
-    if (this.getPrefix() != null) {
-
-      path.append(this.getPrefix());
-
-      // we do NOT use OS dependent separators here; getResourceAsStream
-      // explicitly requires forward slashes.
-      if (!this.getPrefix().endsWith(Character.toString(this.expectedSeparator))) {
-        path.append(this.expectedSeparator);
-      }
-    }
-    path.append(templateName);
-    if (this.getSuffix() != null) {
-      path.append(this.getSuffix());
-    }
-    return path.toString();
-  }
-
-  public String getSuffix() {
-    return this.suffix;
-  }
-
-  @Override
-  public void setSuffix(String suffix) {
-    this.suffix = suffix;
-  }
-
-  public String getPrefix() {
-    return this.prefix;
-  }
-
-  @Override
-  public void setPrefix(String prefix) {
-    this.prefix = prefix;
-  }
-
-  public String getCharset() {
-    return this.charset;
-  }
-
-  @Override
-  public void setCharset(String charset) {
-    this.charset = charset;
-  }
-
-  @Override
-  public String resolveRelativePath(String relativePath, String anchorPath) {
-    return PathUtils.resolveRelativePath(relativePath, anchorPath, this.expectedSeparator);
-  }
-
-  @Override
-  public String createCacheKey(String templateName) {
-    return templateName;
+  protected InputStream getResourceAsStream(String location) {
+    return context.getResourceAsStream(location);
   }
 
   @Override
-  public boolean resourceExists(String templateName) {
-    try {
-      return this.context.getResource(this.getLocation(templateName)) != null;
-    } catch (MalformedURLException e) {
-      return false;
-    }
+  protected URL getResource(String location) throws MalformedURLException {
+    return context.getResource(location);
   }
 }


Test patch:
diff --git a/pebble/src/test/java/com/mitchellbosecke/pebble/lexer/LexerImplTest.java b/pebble/src/test/java/com/mitchellbosecke/pebble/lexer/LexerImplTest.java
index decdd53e..001ba79f 100644
--- a/pebble/src/test/java/com/mitchellbosecke/pebble/lexer/LexerImplTest.java
+++ b/pebble/src/test/java/com/mitchellbosecke/pebble/lexer/LexerImplTest.java
@@ -82,6 +82,36 @@ class LexerImplTest {
     assertThat(tokenStream.peek(3).getValue()).isNull();
   }
 
+  /**
+   * Test tokenizing an expression, e.g. {{ expression }}
+   */
+  @Test
+  void testVariableNameStartingWithOperator() {
+    Loader<String> loader = new StringLoader();
+    Reader templateReader = loader.getReader("{{ is_active + contains0 }}");
+
+    TokenStream tokenStream = this.lexer.tokenize(templateReader, this.TEMPLATE_NAME);
+
+    int i = 0;
+    assertThat(tokenStream.peek(i).getType()).isEqualTo(Type.PRINT_START);
+    assertThat(tokenStream.peek(i++).getValue()).isNull();
+
+    assertThat(tokenStream.peek(i).getType()).isEqualTo(Type.NAME);
+    assertThat(tokenStream.peek(i++).getValue()).isEqualTo("is_active");
+
+    assertThat(tokenStream.peek(i).getType()).isEqualTo(Type.OPERATOR);
+    assertThat(tokenStream.peek(i++).getValue()).isEqualTo("+");
+
+    assertThat(tokenStream.peek(i).getType()).isEqualTo(Type.NAME);
+    assertThat(tokenStream.peek(i++).getValue()).isEqualTo("contains0");
+
+    assertThat(tokenStream.peek(i).getType()).isEqualTo(Type.PRINT_END);
+    assertThat(tokenStream.peek(i++).getValue()).isEqualTo("}}");
+
+    assertThat(tokenStream.peek(i).getType()).isEqualTo(Type.EOF);
+    assertThat(tokenStream.peek(i++).getValue()).isNull();
+  }
+
   /**
    * Test tokenizing Punctuation, such as the dot in item.itemType
    */