templateResolutionAttributes
+ ) {
+ return new ThymeleafClassLoaderTemplateResource(
+ template,
+ resourceName,
+ characterEncoding,
+ templateInputStreamLoaders
+ );
+ }
+}
diff --git a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResource.java b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResource.java
new file mode 100644
index 00000000..ecdfaef0
--- /dev/null
+++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResource.java
@@ -0,0 +1,159 @@
+/*
+ * =============================================================================
+ *
+ * Copyright (c) 2011-2025 Thymeleaf (http://www.thymeleaf.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * =============================================================================
+ */
+
+package com.tvd12.ezyhttp.server.thymeleaf;
+
+import com.tvd12.ezyhttp.server.core.view.ViewTemplateInputStreamLoader;
+import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
+import org.thymeleaf.templateresource.ITemplateResource;
+import org.thymeleaf.util.ClassLoaderUtils;
+import org.thymeleaf.util.StringUtils;
+import org.thymeleaf.util.Validate;
+
+import java.io.*;
+import java.util.List;
+
+import static com.tvd12.ezyhttp.server.thymeleaf.ThymeleafTemplateResourceUtils.*;
+
+/**
+ *
+ * Implementation of {@link ITemplateResource} representing a resource accessible
+ * by a {@link ClassLoader} (i.e. living at the class path).
+ *
+ *
+ * Objects of this class are usually created by
+ * {@link org.thymeleaf.templateresolver.ClassLoaderTemplateResolver}.
+ *
+ *
+ * @author Daniel Fernández
+ * @since 3.0.0
+ *
+ */
+public final class ThymeleafClassLoaderTemplateResource implements ITemplateResource {
+
+ private final String template;
+ private final String path;
+ private final String characterEncoding;
+ private final List templateInputStreamLoaders;
+
+ /**
+ *
+ * Create a ClassLoader-based template resource, specifying the specific class loader
+ * to be used for resolving the resource.
+ *
+ *
+ * @param template the name of template.
+ * @param path the path to the template resource.
+ * @param characterEncoding the character encoding to be used to read the resource.
+ * @param templateInputStreamLoaders the list of template input stream loaders
+ * if template not found in the classpath.
+ * @since 3.0.3
+ */
+ public ThymeleafClassLoaderTemplateResource(
+ final String template,
+ final String path,
+ final String characterEncoding,
+ final List templateInputStreamLoaders
+ ) {
+ super();
+
+ // Class Loader CAN be null (will apply the default sequence of class loaders
+ Validate.notEmpty(path, "Resource Path cannot be null or empty");
+
+ this.template = template;
+
+ // Character encoding CAN be null (system default will be used)
+ final String cleanPath = cleanPath(path);
+ this.path = cleanPath.charAt(0) == '/'
+ ? cleanPath.substring(1)
+ : cleanPath;
+ this.characterEncoding = characterEncoding;
+ this.templateInputStreamLoaders = templateInputStreamLoaders;
+ }
+
+ @Override
+ public String getDescription() {
+ return this.path;
+ }
+
+ @Override
+ public String getBaseName() {
+ return computeBaseName(path);
+ }
+
+ @Override
+ public Reader reader() throws IOException {
+ InputStream inputStream = ClassLoaderUtils
+ .findResourceAsStream(this.path);
+ if (inputStream == null) {
+ for (ViewTemplateInputStreamLoader loader : templateInputStreamLoaders) {
+ inputStream = loader.load(template, path);
+ if (inputStream != null) {
+ break;
+ }
+ }
+ }
+ if (inputStream == null) {
+ throw new FileNotFoundException(
+ String.format(
+ "ClassLoader resource \"%s\" could not be resolved",
+ this.path
+ )
+ );
+ }
+ if (!StringUtils.isEmptyOrWhitespace(this.characterEncoding)) {
+ return new BufferedReader(
+ new InputStreamReader(
+ new BufferedInputStream(inputStream),
+ this.characterEncoding
+ )
+ );
+ }
+ return new BufferedReader(
+ new InputStreamReader(
+ new BufferedInputStream(inputStream)
+ )
+ );
+ }
+
+ @Override
+ public ITemplateResource relative(
+ final String relativeLocation
+ ) {
+ Validate.notEmpty(
+ relativeLocation,
+ "Relative Path cannot be null or empty"
+ );
+ final String fullRelativeLocation = computeRelativeLocation(
+ this.path,
+ relativeLocation
+ );
+ return new ClassLoaderTemplateResource(
+ null,
+ fullRelativeLocation,
+ this.characterEncoding
+ );
+ }
+
+ @Override
+ public boolean exists() {
+ return ClassLoaderUtils.isResourcePresent(this.path);
+ }
+}
diff --git a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtils.java b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtils.java
new file mode 100644
index 00000000..f1deaa6a
--- /dev/null
+++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtils.java
@@ -0,0 +1,140 @@
+/*
+ * =============================================================================
+ *
+ * Copyright (c) 2011-2025 Thymeleaf (http://www.thymeleaf.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * =============================================================================
+ */
+
+package com.tvd12.ezyhttp.server.thymeleaf;
+
+import org.thymeleaf.templateresource.ITemplateResource;
+import org.thymeleaf.util.StringUtils;
+
+/**
+ *
+ * Utility methods used by several implementations of {@link ITemplateResource}.
+ *
+ *
+ * @author Daniel Fernández
+ * @since 3.0.0
+ *
+ */
+final class ThymeleafTemplateResourceUtils {
+
+ private ThymeleafTemplateResourceUtils() {}
+
+ static String cleanPath(final String path) {
+ if (path == null) {
+ return null;
+ }
+
+ // First replace Windows folder separators with UNIX's
+ String unixPath = StringUtils.replace(path, "\\", "/");
+ // Some shortcuts, just in case this is empty or simply has no '.' or '..'
+ // (and no double-/ we should simplify)
+ if (unixPath.isEmpty()
+ || (!unixPath.contains("/.") && !unixPath.contains("//"))
+ ) {
+ return unixPath;
+ }
+ // We make sure path starts with '/' in order to simplify the algorithm
+ boolean rootBased = (unixPath.charAt(0) == '/');
+ unixPath = (rootBased ? unixPath : ('/' + unixPath));
+ // We will traverse path in reverse order, looking for '.' and '..' tokens
+ // and processing them
+ final StringBuilder strBuilder = new StringBuilder(unixPath.length());
+ int index = unixPath.lastIndexOf('/');
+ int pos = unixPath.length() - 1;
+ int topCount = 0;
+ // Will always be 0 for the last iteration, as we prefixed the path with '/'
+ while (index >= 0) {
+ final int tokenLen = pos - index;
+ if (tokenLen > 0) {
+ if (tokenLen == 1 && unixPath.charAt(index + 1) == '.') {
+ // Token is '.' -> just ignore it
+ } else if (tokenLen == 2
+ && unixPath.charAt(index + 1) == '.'
+ && unixPath.charAt(index + 2) == '.'
+ ) {
+ // Token is '..' -> count as a 'top' operation
+ topCount++;
+ } else if (topCount > 0) {
+ // Whatever comes here has been removed by a 'top' operation, so ignore
+ topCount--;
+ } else {
+ // Token is OK, just add (with its corresponding '/')
+ strBuilder.insert(0, unixPath, index, (index + tokenLen + 1));
+ }
+ }
+ pos = index - 1;
+ index = (pos >= 0 ? unixPath.lastIndexOf('/', pos) : -1);
+ }
+ // Add all 'top' tokens appeared at the very beginning of the path
+ for (int i = 0; i < topCount; i++) {
+ strBuilder.insert(0, "/..");
+ }
+ // Perform last cleanup
+ if (!rootBased) {
+ strBuilder.deleteCharAt(0);
+ }
+ return strBuilder.toString();
+ }
+
+ static String computeRelativeLocation(
+ final String location,
+ final String relativeLocation
+ ) {
+ final int separatorPos = location.lastIndexOf('/');
+ if (separatorPos != -1) {
+ final StringBuilder relativeBuilder = new StringBuilder(
+ location.length() + relativeLocation.length()
+ );
+ relativeBuilder.append(location, 0, separatorPos);
+ if (relativeLocation.charAt(0) != '/') {
+ relativeBuilder.append('/');
+ }
+ relativeBuilder.append(relativeLocation);
+ return relativeBuilder.toString();
+ }
+ return relativeLocation;
+ }
+
+ static String computeBaseName(final String path) {
+ if (path == null || path.isEmpty()) {
+ return null;
+ }
+
+ // First remove a trailing '/' if it exists
+ final String basePath = path.charAt(path.length() - 1) == '/'
+ ? path.substring(0, path.length() - 1)
+ : path;
+
+ final int slashPos = basePath.lastIndexOf('/');
+ if (slashPos != -1) {
+ final int dotPos = basePath.lastIndexOf('.');
+ if (dotPos != -1 && dotPos > slashPos + 1) {
+ return basePath.substring(slashPos + 1, dotPos);
+ }
+ return basePath.substring(slashPos + 1);
+ } else {
+ final int dotPos = basePath.lastIndexOf('.');
+ if (dotPos != -1) {
+ return basePath.substring(0, dotPos);
+ }
+ }
+ return (basePath.isEmpty() ? null : basePath);
+ }
+}
diff --git a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContext.java b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContext.java
index 222a998e..44563b89 100644
--- a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContext.java
+++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContext.java
@@ -7,7 +7,6 @@
import org.thymeleaf.context.IContext;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.templatemode.TemplateMode;
-import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.StringTemplateResolver;
import javax.servlet.ServletContext;
@@ -27,19 +26,22 @@ public class ThymeleafViewContext implements ViewContext {
private final List messageProviders;
private final ThymeleafMessageResolver messageResolver;
private final AbsentMessageResolver absentMessageResolver;
+ private final List templateInputStreamLoaders;
public ThymeleafViewContext(
TemplateResolver metadata,
List viewDialects,
List viewDecorators,
List messageProviders,
- AbsentMessageResolver absentMessageResolver
+ AbsentMessageResolver absentMessageResolver,
+ List templateInputStreamLoaders
) {
this.metadata = metadata;
this.viewDialects = viewDialects;
this.viewDecorators = viewDecorators;
this.messageProviders = messageProviders;
this.absentMessageResolver = absentMessageResolver;
+ this.templateInputStreamLoaders = templateInputStreamLoaders;
this.messageResolver = createMessageResolver();
this.templateEngine = createTemplateEngine();
this.contentTemplateEngine = createContentTemplateEngine();
@@ -109,8 +111,8 @@ private ThymeleafMessageResolver createMessageResolver() {
}
private TemplateEngine createTemplateEngine() {
- ClassLoaderTemplateResolver templateResolver =
- new ClassLoaderTemplateResolver();
+ ThymeleafClassLoaderTemplateResolver templateResolver =
+ new ThymeleafClassLoaderTemplateResolver(templateInputStreamLoaders);
TemplateMode templateMode = TemplateMode.valueOf(metadata.getTemplateMode());
templateResolver.setTemplateMode(templateMode);
templateResolver.setPrefix(metadata.getPrefix());
diff --git a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContextBuilder.java b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContextBuilder.java
index 12fe47d5..fd7f3d1a 100644
--- a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContextBuilder.java
+++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafViewContextBuilder.java
@@ -14,7 +14,8 @@ public ViewContext build() {
viewDialects,
viewDecorators,
messageProviders,
- absentMessageResolver
+ absentMessageResolver,
+ templateInputStreamLoaders
);
}
}
diff --git a/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtilsTest.java b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtilsTest.java
new file mode 100644
index 00000000..dbdb8838
--- /dev/null
+++ b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtilsTest.java
@@ -0,0 +1,216 @@
+package com.tvd12.ezyhttp.server.thymeleaf;
+
+import com.tvd12.test.assertion.Asserts;
+import org.testng.annotations.Test;
+
+public class ThymeleafTemplateResourceUtilsTest {
+
+ @Test
+ public void cleanPathNullEmptyAndSimple() {
+ // given
+ String nullPath = null;
+ String emptyPath = "";
+ String simplePath = "templates\\home.html";
+ String plainPath = "a/b/c";
+
+ // when
+ String nullResult = ThymeleafTemplateResourceUtils.cleanPath(nullPath);
+ String emptyResult = ThymeleafTemplateResourceUtils.cleanPath(emptyPath);
+ String simpleResult = ThymeleafTemplateResourceUtils.cleanPath(simplePath);
+ String plainResult = ThymeleafTemplateResourceUtils.cleanPath(plainPath);
+
+ // then
+ Asserts.assertNull(nullResult);
+ Asserts.assertEquals("", emptyResult);
+ Asserts.assertEquals("templates/home.html", simpleResult);
+ Asserts.assertEquals("a/b/c", plainResult);
+ }
+
+ @Test
+ public void cleanPathRelativeWithDotsAndDoubleSlash() {
+ // given
+ String path = "a/./b/../c//d/..";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+
+ // then
+ Asserts.assertEquals("a/c", result);
+ }
+
+ @Test
+ public void cleanPathSimpleWithoutDotOrDoubleSlash() {
+ // given
+ String path = "foo/bar";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+
+ // then
+ Asserts.assertEquals("foo/bar", result);
+ }
+
+ @Test
+ public void cleanPathRootBasedWithParentSegments() {
+ // given
+ String path = "/a/b/../c";
+ String pathWithLeadingParents = "/../a";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+ String resultWithParents = ThymeleafTemplateResourceUtils.cleanPath(pathWithLeadingParents);
+
+ // then
+ Asserts.assertEquals("/a/c", result);
+ Asserts.assertEquals("/../a", resultWithParents);
+ }
+
+ @Test
+ public void cleanPathWithUnixPathContainsDoubleSplash() {
+ // given
+ String path = "foo//bar";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+
+ // then
+ Asserts.assertEquals("foo/bar", result);
+ }
+
+ @Test
+ public void cleanPathContainsNoBackText() {
+ // given
+ String path = "foo//bar/.";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+
+ // then
+ Asserts.assertEquals("foo/bar", result);
+ }
+
+ @Test
+ public void cleanPathContainsNoBack1Text() {
+ // given
+ String path = "foo//bar/aa";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+
+ // then
+ Asserts.assertEquals("foo/bar/aa", result);
+ }
+
+ @Test
+ public void cleanPathContainsNoBack2Text() {
+ // given
+ String path = "foo//bar/.a";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.cleanPath(path);
+
+ // then
+ Asserts.assertEquals("foo/bar/.a", result);
+ }
+
+ @Test
+ public void computeRelativeLocationWithSeparator() {
+ // given
+ String location = "templates/index.html";
+ String relative = "fragments/header.html";
+ String absoluteRelative = "/fragments/footer.html";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.computeRelativeLocation(location, relative);
+ String absoluteResult = ThymeleafTemplateResourceUtils.computeRelativeLocation(location, absoluteRelative);
+
+ // then
+ Asserts.assertEquals("templates/fragments/header.html", result);
+ Asserts.assertEquals("templates/fragments/footer.html", absoluteResult);
+ }
+
+ @Test
+ public void computeRelativeLocationWithoutSeparator() {
+ // given
+ String location = "index.html";
+ String relative = "fragments/header.html";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.computeRelativeLocation(location, relative);
+
+ // then
+ Asserts.assertEquals("fragments/header.html", result);
+ }
+
+ @Test
+ public void computeBaseNameNullEmptyAndTrailingSlash() {
+ // given
+ String nullPath = null;
+ String emptyPath = "";
+ String trailingSlash = "folder/";
+ String rootPath = "/";
+
+ // when
+ String nullResult = ThymeleafTemplateResourceUtils.computeBaseName(nullPath);
+ String emptyResult = ThymeleafTemplateResourceUtils.computeBaseName(emptyPath);
+ String trailingResult = ThymeleafTemplateResourceUtils.computeBaseName(trailingSlash);
+ String rootResult = ThymeleafTemplateResourceUtils.computeBaseName(rootPath);
+
+ // then
+ Asserts.assertNull(nullResult);
+ Asserts.assertNull(emptyResult);
+ Asserts.assertEquals("folder", trailingResult);
+ Asserts.assertNull(rootResult);
+ }
+
+ @Test
+ public void computeBaseNameWithSlashAndDot() {
+ // given
+ String path = "templates/index.html";
+ String hiddenPath = "templates/.hidden";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.computeBaseName(path);
+ String hiddenResult = ThymeleafTemplateResourceUtils.computeBaseName(hiddenPath);
+
+ // then
+ Asserts.assertEquals("index", result);
+ Asserts.assertEquals(".hidden", hiddenResult);
+ }
+
+ @Test
+ public void computeBaseNameWithSlashAndDotAfterName() {
+ // given
+ String path = "templates/home.page.html";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.computeBaseName(path);
+
+ // then
+ Asserts.assertEquals("home.page", result);
+ }
+
+ @Test
+ public void computeBaseNameWithoutSlash() {
+ // given
+ String path = "archive.tar.gz";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.computeBaseName(path);
+
+ // then
+ Asserts.assertEquals("archive.tar", result);
+ }
+
+ @Test
+ public void computeBaseNamDotPost1Test() {
+ // given
+ String path = "a/b";
+
+ // when
+ String result = ThymeleafTemplateResourceUtils.computeBaseName(path);
+
+ // then
+ Asserts.assertEquals("b", result);
+ }
+}
diff --git a/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafClassLoaderTemplateResourceTest.java b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafClassLoaderTemplateResourceTest.java
new file mode 100644
index 00000000..ce3c4238
--- /dev/null
+++ b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafClassLoaderTemplateResourceTest.java
@@ -0,0 +1,199 @@
+package com.tvd12.ezyhttp.server.thymeleaf.test;
+
+import com.tvd12.ezyhttp.server.core.view.ViewTemplateInputStreamLoader;
+import com.tvd12.ezyhttp.server.thymeleaf.ThymeleafClassLoaderTemplateResource;
+import com.tvd12.test.assertion.Asserts;
+import org.testng.annotations.Test;
+import org.thymeleaf.templateresource.ITemplateResource;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ThymeleafClassLoaderTemplateResourceTest {
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void constructorWithEmptyPath() {
+ // given
+ String template = "index";
+ String path = "";
+
+ // when
+ new ThymeleafClassLoaderTemplateResource(
+ template,
+ path,
+ null,
+ Collections.emptyList()
+ );
+
+ // then
+ // expect exception
+ }
+
+ @Test
+ public void getDescriptionAndBaseName() {
+ // given
+ ThymeleafClassLoaderTemplateResource sut =
+ new ThymeleafClassLoaderTemplateResource(
+ "index",
+ "/templates/./index.html",
+ null,
+ Collections.emptyList()
+ );
+
+ // when
+ String description = sut.getDescription();
+ String baseName = sut.getBaseName();
+
+ // then
+ Asserts.assertEquals("templates/index.html", description);
+ Asserts.assertEquals("index", baseName);
+ }
+
+ @Test
+ public void readerWithClasspathResourceAndEncoding() throws Exception {
+ // given
+ ThymeleafClassLoaderTemplateResource sut =
+ new ThymeleafClassLoaderTemplateResource(
+ "index",
+ "templates/index.html",
+ StandardCharsets.UTF_8.name(),
+ Collections.emptyList()
+ );
+
+ // when
+ Reader reader = sut.reader();
+ String content = readAll(reader);
+
+ // then
+ Asserts.assertNotNull(content);
+ Asserts.assertTrue(content.contains("EzyHTTP"));
+ }
+
+ @Test
+ public void readerWithInputStreamLoader() throws Exception {
+ // given
+ ViewTemplateInputStreamLoader loader = mock(ViewTemplateInputStreamLoader.class);
+ when(
+ loader.load(
+ "custom",
+ "templates/custom.html"
+ )
+ ).thenReturn(
+ new ByteArrayInputStream(
+ "Hello Loader".getBytes(StandardCharsets.UTF_8)
+ )
+ );
+
+ ThymeleafClassLoaderTemplateResource sut =
+ new ThymeleafClassLoaderTemplateResource(
+ "custom",
+ "templates/custom.html",
+ null,
+ Collections.singletonList(loader)
+ );
+
+ // when
+ Reader reader = sut.reader();
+ String content = readAll(reader);
+
+ // then
+ Asserts.assertEquals("Hello Loader", content);
+ }
+
+ @Test
+ public void readerWith2InputStreamLoader() throws Exception {
+ // given
+ ViewTemplateInputStreamLoader loader1 = mock(ViewTemplateInputStreamLoader.class);
+ ViewTemplateInputStreamLoader loader2 = mock(ViewTemplateInputStreamLoader.class);
+ when(
+ loader2.load(
+ "custom",
+ "templates/custom.html"
+ )
+ ).thenReturn(
+ new ByteArrayInputStream(
+ "Hello Loader".getBytes(StandardCharsets.UTF_8)
+ )
+ );
+
+ ThymeleafClassLoaderTemplateResource sut =
+ new ThymeleafClassLoaderTemplateResource(
+ "custom",
+ "templates/custom.html",
+ null,
+ Arrays.asList(loader1, loader2)
+ );
+
+ // when
+ Reader reader = sut.reader();
+ String content = readAll(reader);
+
+ // then
+ Asserts.assertEquals("Hello Loader", content);
+ }
+
+ @Test(expectedExceptions = FileNotFoundException.class)
+ public void readerWithMissingResource() throws Exception {
+ // given
+ ThymeleafClassLoaderTemplateResource sut =
+ new ThymeleafClassLoaderTemplateResource(
+ "missing",
+ "templates/missing.html",
+ null,
+ Collections.emptyList()
+ );
+
+ // when
+ sut.reader();
+
+ // then
+ // expect exception
+ }
+
+ @Test
+ public void relativeAndExists() {
+ // given
+ ThymeleafClassLoaderTemplateResource sut =
+ new ThymeleafClassLoaderTemplateResource(
+ "index",
+ "templates/index.html",
+ null,
+ Collections.emptyList()
+ );
+ ThymeleafClassLoaderTemplateResource missing =
+ new ThymeleafClassLoaderTemplateResource(
+ "missing",
+ "templates/missing.html",
+ null,
+ Collections.emptyList()
+ );
+
+ // when
+ ITemplateResource relative = sut.relative("partials/header.html");
+ boolean exists = sut.exists();
+ boolean missingExists = missing.exists();
+
+ // then
+ Asserts.assertTrue(relative != null);
+ Asserts.assertTrue(exists);
+ Asserts.assertTrue(!missingExists);
+ }
+
+ private String readAll(Reader reader) throws Exception {
+ StringBuilder builder = new StringBuilder();
+ char[] buffer = new char[256];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ builder.append(buffer, 0, read);
+ }
+ reader.close();
+ return builder.toString();
+ }
+}
diff --git a/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafViewContextTest.java b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafViewContextTest.java
index aa0ccd49..f69e7b93 100644
--- a/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafViewContextTest.java
+++ b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafViewContextTest.java
@@ -17,7 +17,9 @@
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
import java.util.*;
import static org.mockito.Mockito.*;
@@ -145,6 +147,53 @@ public void renderWithDialectButNotViewDialect() throws Exception {
Asserts.assertNotNull(viewContext);
}
+
+ @Test
+ public void renderWithoutTemplateTest() throws Exception {
+ // given
+ TemplateResolver resolver = TemplateResolver.builder()
+ .build();
+ ViewTemplateInputStreamLoader loader = mock(ViewTemplateInputStreamLoader.class);
+ when(
+ loader.load(
+ "posts/details",
+ "templates/posts/details.html"
+ )
+ ).thenReturn(
+ new ByteArrayInputStream("Hello World".getBytes(StandardCharsets.UTF_8))
+ );
+ ViewContext viewContext = new ThymeleafViewContextBuilder()
+ .templateResolver(resolver)
+ .templateInputStreamLoaders(
+ Collections.singletonList(loader)
+ )
+ .build();
+
+ ServletContext servletContext = mock(ServletContext.class);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ PrintWriter writer = mock(PrintWriter.class);
+ when(response.getWriter()).thenReturn(writer);
+
+ View view = View.builder()
+ .template("posts/details")
+ .build();
+
+ // when
+ viewContext.render(servletContext, request, response, view);
+
+ // then
+ Asserts.assertNotNull(viewContext);
+ Asserts.assertNotNull(viewContext.getMessageResolver());
+ Asserts.assertNotNull(viewContext.getTemplateEngine());
+ Asserts.assertNotNull(viewContext.getContentTemplateEngine());
+ verify(loader, times(1)).load(
+ "posts/details",
+ "templates/posts/details.html"
+ );
+ }
+
@Test
public void resolveMessageTest() {
// given
@@ -271,9 +320,12 @@ public SayToAttributeTagProcessor(final String dialectPrefix) {
protected void doProcess(
- final ITemplateContext context, final IProcessableElementTag tag,
- final AttributeName attributeName, final String attributeValue,
- final IElementTagStructureHandler structureHandler) {
+ final ITemplateContext context,
+ final IProcessableElementTag tag,
+ final AttributeName attributeName,
+ final String attributeValue,
+ final IElementTagStructureHandler structureHandler
+ ) {
structureHandler.setBody(
"Hello, " + HtmlEscape.escapeHtml5(attributeValue) + "!", false);
diff --git a/ezyhttp-server-thymeleaf/src/test/resources/AllTests.tng.xml b/ezyhttp-server-thymeleaf/src/test/resources/AllTests.tng.xml
index 714499fb..3aa23e77 100644
--- a/ezyhttp-server-thymeleaf/src/test/resources/AllTests.tng.xml
+++ b/ezyhttp-server-thymeleaf/src/test/resources/AllTests.tng.xml
@@ -3,7 +3,8 @@
-
+
+
diff --git a/ezyhttp-server-tomcat/pom.xml b/ezyhttp-server-tomcat/pom.xml
index f37f7aa7..e9b006a6 100644
--- a/ezyhttp-server-tomcat/pom.xml
+++ b/ezyhttp-server-tomcat/pom.xml
@@ -5,7 +5,7 @@
com.tvd12
ezyhttp
- 1.4.6
+ 1.4.7
ezyhttp-server-tomcat
diff --git a/ezyhttp-server-tomcat/src/main/java/com/tvd12/ezyhttp/server/tomcat/TomcatApplicationBootstrap.java b/ezyhttp-server-tomcat/src/main/java/com/tvd12/ezyhttp/server/tomcat/TomcatApplicationBootstrap.java
index 833cd709..296d0fbe 100644
--- a/ezyhttp-server-tomcat/src/main/java/com/tvd12/ezyhttp/server/tomcat/TomcatApplicationBootstrap.java
+++ b/ezyhttp-server-tomcat/src/main/java/com/tvd12/ezyhttp/server/tomcat/TomcatApplicationBootstrap.java
@@ -43,10 +43,10 @@ public class TomcatApplicationBootstrap
protected String host = "0.0.0.0";
@EzyProperty("server.max_threads")
- protected int maxThreads = 256;
+ protected int maxThreads = 16;
@EzyProperty("server.min_threads")
- protected int minThreads = 16;
+ protected int minThreads = 4;
@EzyProperty("server.idle_timeout")
protected int idleTimeout = 150 * 1000;
diff --git a/pom.xml b/pom.xml
index e61a4caa..eec80bff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
1.0.7
ezyhttp
- 1.4.6
+ 1.4.7
pom
ezyhttp