From a0f6ce79046db31219dec2068688ea3ee5ec312d Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sun, 18 Jan 2026 16:13:43 +0700 Subject: [PATCH 01/10] reduce default thread pool size, add getRequestValueAnyway --- ezyhttp-client/pom.xml | 2 +- ezyhttp-core/pom.xml | 2 +- .../resources/ResourceDownloadManager.java | 2 +- .../core/resources/ResourceUploadManager.java | 6 +- .../io/BytesRangeFileInputStreamTest.java | 27 ++- ezyhttp-server-boot/pom.xml | 2 +- ezyhttp-server-core/pom.xml | 2 +- .../server/core/request/RequestArguments.java | 30 ++- .../core/request/SimpleRequestArguments.java | 46 +++- .../server/core/util/HttpServletRequests.java | 74 +++++- .../request/SimpleRequestArgumentsTest.java | 212 ++++++++++++++++-- .../test/util/HttpServletRequestsTest.java | 57 ++++- ezyhttp-server-graphql/pom.xml | 2 +- ezyhttp-server-jetty/pom.xml | 2 +- .../jetty/JettyApplicationBootstrap.java | 4 +- ezyhttp-server-management/pom.xml | 2 +- ezyhttp-server-thymeleaf/pom.xml | 2 +- ezyhttp-server-tomcat/pom.xml | 2 +- .../tomcat/TomcatApplicationBootstrap.java | 4 +- pom.xml | 2 +- 20 files changed, 425 insertions(+), 57 deletions(-) diff --git a/ezyhttp-client/pom.xml b/ezyhttp-client/pom.xml index 30d17f1a..8b369df8 100644 --- a/ezyhttp-client/pom.xml +++ b/ezyhttp-client/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-client diff --git a/ezyhttp-core/pom.xml b/ezyhttp-core/pom.xml index 7b9c3575..8dadc95c 100644 --- a/ezyhttp-core/pom.xml +++ b/ezyhttp-core/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-core diff --git a/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceDownloadManager.java b/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceDownloadManager.java index b2925061..5b696daa 100644 --- a/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceDownloadManager.java +++ b/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceDownloadManager.java @@ -39,7 +39,7 @@ public class ResourceDownloadManager public static final int DEFAULT_BUFFER_SIZE = 1024; public static final int DEFAULT_TIMEOUT = 15 * 60 * 1000; public static final int DEFAULT_THREAD_POOL_SIZE = - Runtime.getRuntime().availableProcessors() * 2; + Runtime.getRuntime().availableProcessors(); public ResourceDownloadManager() { this( diff --git a/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceUploadManager.java b/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceUploadManager.java index c6166007..214b41a8 100644 --- a/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceUploadManager.java +++ b/ezyhttp-core/src/main/java/com/tvd12/ezyhttp/core/resources/ResourceUploadManager.java @@ -33,7 +33,7 @@ public class ResourceUploadManager public static final int DEFAULT_TIMEOUT = 15 * 60 * 1000; public static final long UNLIMITED_UPLOAD_SIZE = -1; public static final int DEFAULT_THREAD_POOL_SIZE = - Runtime.getRuntime().availableProcessors() * 2; + Runtime.getRuntime().availableProcessors(); public ResourceUploadManager() { this( @@ -45,7 +45,9 @@ public ResourceUploadManager() { public ResourceUploadManager( int capacity, - int threadPoolSize, int bufferSize) { + int threadPoolSize, + int bufferSize + ) { this.capacity = capacity; this.threadPoolSize = threadPoolSize; this.bufferSize = bufferSize; diff --git a/ezyhttp-core/src/test/java/com/tvd12/ezyhttp/core/test/io/BytesRangeFileInputStreamTest.java b/ezyhttp-core/src/test/java/com/tvd12/ezyhttp/core/test/io/BytesRangeFileInputStreamTest.java index 798c9d37..648aee5a 100644 --- a/ezyhttp-core/src/test/java/com/tvd12/ezyhttp/core/test/io/BytesRangeFileInputStreamTest.java +++ b/ezyhttp-core/src/test/java/com/tvd12/ezyhttp/core/test/io/BytesRangeFileInputStreamTest.java @@ -9,7 +9,6 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.file.Files; public class BytesRangeFileInputStreamTest { @@ -323,4 +322,30 @@ public void seekIoErrorTest() { // then Asserts.assertEqualsType(e, IOException.class); } + + @Test + public void readTest() throws Exception { + // given + final String pomFilePath = "pom.xml"; + final File pomFile = new File(pomFilePath); + final long fileLength = pomFile.length(); + final String range = "bytes=0-1"; + + final BytesRangeFileInputStream sut = new BytesRangeFileInputStream( + pomFilePath, + range + ); + FieldUtil.setFieldValue( + sut, + "from", + fileLength + ); + + // when + Throwable e = Asserts.assertThrows(() -> sut.read()); + + // then + sut.close(); + Asserts.assertEqualsType(e, UnsupportedOperationException.class); + } } diff --git a/ezyhttp-server-boot/pom.xml b/ezyhttp-server-boot/pom.xml index bb2daacf..f480f965 100644 --- a/ezyhttp-server-boot/pom.xml +++ b/ezyhttp-server-boot/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-server-boot diff --git a/ezyhttp-server-core/pom.xml b/ezyhttp-server-core/pom.xml index 7c8d521a..9e967d8a 100644 --- a/ezyhttp-server-core/pom.xml +++ b/ezyhttp-server-core/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-server-core diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java index ed7faf92..1f33573a 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java @@ -1,15 +1,17 @@ package com.tvd12.ezyhttp.server.core.request; -import java.util.Map; +import com.tvd12.ezyfox.util.EzyReleasable; +import com.tvd12.ezyhttp.core.constant.HttpMethod; +import com.tvd12.ezyhttp.core.data.BodyData; import javax.servlet.AsyncContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.Map; -import com.tvd12.ezyfox.util.EzyReleasable; -import com.tvd12.ezyhttp.core.constant.HttpMethod; -import com.tvd12.ezyhttp.core.data.BodyData; +import static com.tvd12.ezyfox.io.EzyStrings.EMPTY_STRING; +import static com.tvd12.ezyfox.io.EzyStrings.isBlank; public interface RequestArguments extends BodyData, EzyReleasable { @@ -77,6 +79,26 @@ default String getCookieValue(String name, String defaultValue) { return answer != null ? answer : defaultValue; } + String getRequestValue(String name); + + default String getRequestValueAnyway( + String argumentName + ) { + String value = getRequestValue(argumentName); + String argumentNameLowerCase = EMPTY_STRING; + if (isBlank(value)) { + argumentNameLowerCase = argumentName.toLowerCase(); + value = getRequestValue(argumentNameLowerCase); + } + if (isBlank(value)) { + String argumentNameFirstUpperCase = + argumentNameLowerCase.substring(0, 1).toUpperCase() + + argumentNameLowerCase.substring(1); + value = getRequestValue(argumentNameFirstUpperCase); + } + return value; + } + Map getRedirectionAttributes(); T getRedirectionAttribute(String name); diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java index 19c79640..f1e64ae2 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java @@ -24,6 +24,9 @@ import java.util.Map; import java.util.Map.Entry; +import static com.tvd12.ezyfox.io.EzyStrings.EMPTY_STRING; +import static com.tvd12.ezyfox.io.EzyStrings.isBlank; + @SuppressWarnings("unchecked") public class SimpleRequestArguments implements RequestArguments { @@ -242,18 +245,46 @@ public Cookie getCookie(String name) { return cookieMap != null ? cookieMap.get(name) : null; } + @Override + public String getRequestValue(String name) { + String value = (String) request.getAttribute(name); + if (isBlank(value)) { + value = getPathVariable(name); + } + if (isBlank(value)) { + value = getHeader(name); + } + if (isBlank(value)) { + value = getParameter(name); + } + if (isBlank(value)) { + value = getCookieValue(name); + } + if (isBlank(value)) { + value = getArgument(name); + } + return value; + } + public void setRedirectionAttributesFromCookie() { - Cookie cookie = getCookie(CoreConstants.COOKIE_REDIRECT_ATTRIBUTES_NAME); + Cookie cookie = getCookie(CoreConstants + .COOKIE_REDIRECT_ATTRIBUTES_NAME); if (cookie == null) { return; } try { String value = EzyBase64.decodeUtf(cookie.getValue()); - this.redirectionAttributes = objectMapper.readValue(value, Map.class); + this.redirectionAttributes = objectMapper.readValue( + value, + Map.class + ); } catch (Exception e) { // do nothing } - Cookie newCookie = new Cookie(CoreConstants.COOKIE_REDIRECT_ATTRIBUTES_NAME, ""); + Cookie newCookie = new Cookie( + CoreConstants.COOKIE_REDIRECT_ATTRIBUTES_NAME, + EMPTY_STRING + ); newCookie.setMaxAge(0); response.addCookie(newCookie); } @@ -267,9 +298,14 @@ public T getRedirectionAttribute(String name) { } @Override - public T getRedirectionAttribute(String name, Class outType) { + public T getRedirectionAttribute( + String name, + Class outType + ) { Object value = getRedirectionAttribute(name); - return value != null ? objectMapper.convertValue(value, outType) : null; + return value != null + ? objectMapper.convertValue(value, outType) + : null; } @Override diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java index dd4957ab..45fc4a68 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java @@ -3,8 +3,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import static com.tvd12.ezyfox.io.EzyStrings.isBlank; -import static com.tvd12.ezyfox.io.EzyStrings.isNotBlank; +import static com.tvd12.ezyfox.io.EzyStrings.*; public final class HttpServletRequests { @@ -20,6 +19,22 @@ private HttpServletRequests() {} public static String getRequestValue( HttpServletRequest request, String name + ) { + return getRequestValue(request, name, Boolean.TRUE); + } + + /** + * Get request value from attribute or header or parameter or cookie. + * + * @param request the http request. + * @param name the name of value. + * @param checkCookie check request value in cookie or not + * @return the request value. + */ + public static String getRequestValue( + HttpServletRequest request, + String name, + boolean checkCookie ) { String value = (String) request.getAttribute(name); if (isBlank(value)) { @@ -28,12 +43,15 @@ public static String getRequestValue( if (isBlank(value)) { value = request.getParameter(name); } - if (isBlank(value) && request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (cookie.getName().equals(name)) { - value = cookie.getValue(); - if (isNotBlank(value)) { - break; + if (isBlank(value) && checkCookie) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + value = cookie.getValue(); + if (isNotBlank(value)) { + break; + } } } } @@ -44,6 +62,8 @@ public static String getRequestValue( /** * Get request value from attribute or header or parameter or cookie. * If the value is blank, try to get by lowercase of name. + * If the value is still blank, try retrieving it using + * the name with the first letter capitalized. * * @param request the http request. * @param name the name of value. @@ -53,9 +73,43 @@ public static String getRequestValueAnyway( HttpServletRequest request, String name ) { - String value = getRequestValue(request, name); + return getRequestValueAnyway(request, name, Boolean.TRUE); + } + + /** + * Get request value from attribute or header or parameter or cookie. + * If the value is blank, try to get by lowercase of name. + * If the value is still blank, try retrieving it using + * the name with the first letter capitalized. + * + * @param request the http request. + * @param name the name of value. + * @return the request value. + */ + public static String getRequestValueAnyway( + HttpServletRequest request, + String name, + boolean checkCookie + ) { + String value = getRequestValue(request, name, checkCookie); + String argumentNameLowerCase = EMPTY_STRING; + if (isBlank(value)) { + argumentNameLowerCase = name.toLowerCase(); + value = getRequestValue( + request, + argumentNameLowerCase, + checkCookie + ); + } if (isBlank(value)) { - value = getRequestValue(request, name.toLowerCase()); + String argumentNameFirstUpperCase = + argumentNameLowerCase.substring(0, 1).toUpperCase() + + argumentNameLowerCase.substring(1); + value = getRequestValue( + request, + argumentNameFirstUpperCase, + checkCookie + ); } return value; } diff --git a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java index 58ea347d..15868808 100644 --- a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java +++ b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java @@ -1,23 +1,5 @@ package com.tvd12.ezyhttp.server.core.test.request; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.AsyncContext; -import javax.servlet.ServletInputStream; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.testng.annotations.Test; - import com.fasterxml.jackson.databind.ObjectMapper; import com.tvd12.ezyfox.security.EzyBase64; import com.tvd12.ezyfox.util.EzyMapBuilder; @@ -26,6 +8,19 @@ import com.tvd12.ezyhttp.server.core.constant.CoreConstants; import com.tvd12.ezyhttp.server.core.request.SimpleRequestArguments; import com.tvd12.test.assertion.Asserts; +import org.testng.annotations.Test; + +import javax.servlet.AsyncContext; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.*; public class SimpleRequestArgumentsTest { @@ -378,4 +373,185 @@ public void setRedirectionAttributesFromCookieExceptionTest() { Asserts.assertNull(sut.getRedirectionAttribute("hello")); Asserts.assertNull(sut.getRedirectionAttribute("hello", Map.class)); } + + @Test + public void getRequestValueAnywayCaseInAttributeTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + when(request.getAttribute("hello")).thenReturn("world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("worlD"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInPathVariableTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/{hello}"); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("worlD"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInHeaderTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + sut.setHeader("hello", "world"); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("worlD"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInParameterTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + sut.setParameter("hello", new String[] {"world"}); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("worlD"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInCookieTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + sut.setCookies(new Cookie[] {new Cookie("hello", "world")}); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("worlD"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInArgumentTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + sut.setArgument("hello", "world"); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("worlD"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInArgument2Test() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + sut.setArgument("hello", "world"); + + // when + String actual1 = sut.getRequestValueAnyway("hello"); + String actual2 = sut.getRequestValueAnyway("w"); + + // then + Asserts.assertEquals(actual1, "world"); + Asserts.assertEquals(actual2, null); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(2)).getAttribute("w"); + verify(request, times(1)).getAttribute("W"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } } diff --git a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/util/HttpServletRequestsTest.java b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/util/HttpServletRequestsTest.java index c3d79759..314278fd 100644 --- a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/util/HttpServletRequestsTest.java +++ b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/util/HttpServletRequestsTest.java @@ -62,7 +62,29 @@ public void getRequestValueTestWithCookie() { verify(request, times(1)).getAttribute("1"); verify(request, times(1)).getHeader("2"); verify(request, times(1)).getParameter("3"); - verify(request, times(8)).getCookies(); + verify(request, times(4)).getCookies(); + } + + @Test + public void getRequestValueTestWithoutCookie() { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + + // when + when(request.getCookies()).thenReturn(new Cookie[]{ + new Cookie("1", ""), new Cookie("2", "b"), new Cookie("3", "c") + }); + + // then + Asserts.assertNull(HttpServletRequests.getRequestValue(request, "1", false)); + Asserts.assertNull(HttpServletRequests.getRequestValue(request, "2", false)); + Asserts.assertNull(HttpServletRequests.getRequestValue(request, "3", false)); + Asserts.assertNull(HttpServletRequests.getRequestValue(request, "unknown", false)); + + verify(request, times(1)).getAttribute("1"); + verify(request, times(1)).getHeader("2"); + verify(request, times(1)).getParameter("3"); + verify(request, times(0)).getCookies(); } @Test @@ -88,6 +110,37 @@ public void getRequestValueAnywayTest() { verify(request, times(1)).getParameter("3"); verify(request, times(1)).getParameter("A"); verify(request, times(1)).getParameter("a"); - verify(request, times(3)).getCookies(); + verify(request, times(4)).getCookies(); + } + + @Test + public void getRequestValueAnywayNoCheckCookieTest() { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + + // when + when(request.getAttribute("1")).thenReturn("a"); + when(request.getHeader("2")).thenReturn("b"); + when(request.getParameter("3")).thenReturn("c"); + when(request.getParameter("a")).thenReturn("d"); + when(request.getParameter("a")).thenReturn("d"); + when(request.getCookies()).thenReturn(new Cookie[]{ + new Cookie("e", "e1"), new Cookie("f", "f1"), new Cookie("g", "g1") + }); + + // then + Asserts.assertEquals(HttpServletRequests.getRequestValueAnyway(request, "1", false), "a"); + Asserts.assertEquals(HttpServletRequests.getRequestValueAnyway(request, "2", false), "b"); + Asserts.assertEquals(HttpServletRequests.getRequestValueAnyway(request, "3", false), "c"); + Asserts.assertEquals(HttpServletRequests.getRequestValueAnyway(request, "A", false), "d"); + Asserts.assertNull(HttpServletRequests.getRequestValueAnyway(request, "unknown", false)); + Asserts.assertNull(HttpServletRequests.getRequestValueAnyway(request, "e", false)); + + verify(request, times(1)).getAttribute("1"); + verify(request, times(1)).getHeader("2"); + verify(request, times(1)).getParameter("3"); + verify(request, times(1)).getParameter("A"); + verify(request, times(1)).getParameter("a"); + verify(request, times(0)).getCookies(); } } diff --git a/ezyhttp-server-graphql/pom.xml b/ezyhttp-server-graphql/pom.xml index b3fd66a5..08e6f7d0 100644 --- a/ezyhttp-server-graphql/pom.xml +++ b/ezyhttp-server-graphql/pom.xml @@ -6,7 +6,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-server-graphql ezyhttp-server-graphql diff --git a/ezyhttp-server-jetty/pom.xml b/ezyhttp-server-jetty/pom.xml index b80bf5da..ca0365f0 100644 --- a/ezyhttp-server-jetty/pom.xml +++ b/ezyhttp-server-jetty/pom.xml @@ -5,7 +5,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-server-jetty diff --git a/ezyhttp-server-jetty/src/main/java/com/tvd12/ezyhttp/server/jetty/JettyApplicationBootstrap.java b/ezyhttp-server-jetty/src/main/java/com/tvd12/ezyhttp/server/jetty/JettyApplicationBootstrap.java index 50b01e8a..06fc3c82 100644 --- a/ezyhttp-server-jetty/src/main/java/com/tvd12/ezyhttp/server/jetty/JettyApplicationBootstrap.java +++ b/ezyhttp-server-jetty/src/main/java/com/tvd12/ezyhttp/server/jetty/JettyApplicationBootstrap.java @@ -37,10 +37,10 @@ public class JettyApplicationBootstrap 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/ezyhttp-server-management/pom.xml b/ezyhttp-server-management/pom.xml index 488eb55d..e6f95f12 100644 --- a/ezyhttp-server-management/pom.xml +++ b/ezyhttp-server-management/pom.xml @@ -6,7 +6,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-server-management ezyhttp-server-management diff --git a/ezyhttp-server-thymeleaf/pom.xml b/ezyhttp-server-thymeleaf/pom.xml index 032acea7..5fb8dc56 100644 --- a/ezyhttp-server-thymeleaf/pom.xml +++ b/ezyhttp-server-thymeleaf/pom.xml @@ -6,7 +6,7 @@ com.tvd12 ezyhttp - 1.4.6 + 1.4.7 ezyhttp-server-thymeleaf ezyhttp-server-thymeleaf 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 From 78d3292791c751ef6fe76c61932927455de91677 Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sun, 18 Jan 2026 20:19:01 +0700 Subject: [PATCH 02/10] update --- .../core/ApplicationContextBuilder.java | 11 +- .../server/core/view/ViewContextBuilder.java | 13 +- .../view/ViewTemplateInputStreamLoader.java | 27 +++ .../test/ApplicationContextBuilderTest.java | 12 ++ .../ThymeleafClassLoaderTemplateResolver.java | 78 +++++++++ .../ThymeleafClassLoaderTemplateResource.java | 154 ++++++++++++++++++ .../ThymeleafTemplateResourceUtils.java | 144 ++++++++++++++++ .../thymeleaf/ThymeleafViewContext.java | 10 +- .../ThymeleafViewContextBuilder.java | 3 +- .../test/ThymeleafViewContextTest.java | 58 ++++++- 10 files changed, 493 insertions(+), 17 deletions(-) create mode 100644 ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewTemplateInputStreamLoader.java create mode 100644 ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java create mode 100644 ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResource.java create mode 100644 ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtils.java diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/ApplicationContextBuilder.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/ApplicationContextBuilder.java index b02a645d..7b1a5a30 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/ApplicationContextBuilder.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/ApplicationContextBuilder.java @@ -54,13 +54,7 @@ import com.tvd12.ezyhttp.server.core.resources.ResourceResolvers; import com.tvd12.ezyhttp.server.core.util.InterceptorAnnotations; import com.tvd12.ezyhttp.server.core.util.ServiceAnnotations; -import com.tvd12.ezyhttp.server.core.view.AbsentMessageResolver; -import com.tvd12.ezyhttp.server.core.view.MessageProvider; -import com.tvd12.ezyhttp.server.core.view.TemplateResolver; -import com.tvd12.ezyhttp.server.core.view.ViewContext; -import com.tvd12.ezyhttp.server.core.view.ViewContextBuilder; -import com.tvd12.ezyhttp.server.core.view.ViewDecorator; -import com.tvd12.ezyhttp.server.core.view.ViewDialect; +import com.tvd12.ezyhttp.server.core.view.*; @SuppressWarnings({"rawtypes", "unchecked", "MethodCount"}) public class ApplicationContextBuilder implements EzyBuilder { @@ -408,6 +402,9 @@ protected ViewContext buildViewContext(EzyBeanContext beanContext) { .viewDecorators(beanContext.getSingletonsOf(ViewDecorator.class)) .messageProviders(beanContext.getSingletonsOf(MessageProvider.class)) .absentMessageResolver(beanContext.getSingleton(AbsentMessageResolver.class)) + .templateInputStreamLoaders( + beanContext.getSingletonsOf(ViewTemplateInputStreamLoader.class) + ) .build(); } } diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewContextBuilder.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewContextBuilder.java index 9b54b2a3..6b8487df 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewContextBuilder.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewContextBuilder.java @@ -1,11 +1,11 @@ package com.tvd12.ezyhttp.server.core.view; +import com.tvd12.ezyfox.builder.EzyBuilder; + import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import com.tvd12.ezyfox.builder.EzyBuilder; - public abstract class ViewContextBuilder implements EzyBuilder { protected TemplateResolver templateResolver; @@ -13,11 +13,13 @@ public abstract class ViewContextBuilder implements EzyBuilder { protected final List viewDialects; protected final List viewDecorators; protected final List messageProviders; + protected final List templateInputStreamLoaders; public ViewContextBuilder() { this.viewDialects = new ArrayList<>(); this.viewDecorators = new ArrayList<>(); this.messageProviders = new ArrayList<>(); + this.templateInputStreamLoaders = new ArrayList<>(); } public ViewContextBuilder templateResolver(TemplateResolver templateResolver) { @@ -47,4 +49,11 @@ public ViewContextBuilder absentMessageResolver(AbsentMessageResolver absentMess } return this; } + + public ViewContextBuilder templateInputStreamLoaders( + List templateInputStreamLoaders + ) { + this.templateInputStreamLoaders.addAll(templateInputStreamLoaders); + return this; + } } diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewTemplateInputStreamLoader.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewTemplateInputStreamLoader.java new file mode 100644 index 00000000..c1502b3e --- /dev/null +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/view/ViewTemplateInputStreamLoader.java @@ -0,0 +1,27 @@ +package com.tvd12.ezyhttp.server.core.view; + +import java.io.IOException; +import java.io.InputStream; + +public interface ViewTemplateInputStreamLoader { + + /** + * Load view template input stream. + * + * @param template the name of template. + * @param templatePath the path of template. + * @return the input stream of template. + * @throws IOException when exception occurs. + */ + InputStream load( + String template, + String templatePath + ) throws IOException; + + /** + * Get priority of the loader, smaller is run first. + * + * @return the priority. + */ + int priority(); +} diff --git a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/ApplicationContextBuilderTest.java b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/ApplicationContextBuilderTest.java index bc61a4eb..d6f96eb1 100644 --- a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/ApplicationContextBuilderTest.java +++ b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/ApplicationContextBuilderTest.java @@ -292,6 +292,15 @@ public void buildViewContextTemplateResolverNotNullIsNull() { when(beanContext.getSingleton(AbsentMessageResolver.class)).thenReturn(absentMessageResolver); when(viewContextBuilder.absentMessageResolver(absentMessageResolver)).thenReturn(viewContextBuilder); + ViewTemplateInputStreamLoader viewTemplateInputStreamLoader = + mock(ViewTemplateInputStreamLoader.class); + List viewTemplateInputStreamLoaders = + Collections.singletonList(viewTemplateInputStreamLoader); + when(beanContext.getSingletonsOf(ViewTemplateInputStreamLoader.class)) + .thenReturn(viewTemplateInputStreamLoaders); + when(viewContextBuilder.templateInputStreamLoaders(viewTemplateInputStreamLoaders)) + .thenReturn(viewContextBuilder); + ApplicationContextBuilder sut = new ApplicationContextBuilder(); // when @@ -311,11 +320,14 @@ public void buildViewContextTemplateResolverNotNullIsNull() { verify(beanContext, times(1)).getSingletonsOf(ViewDecorator.class); verify(beanContext, times(1)).getSingleton(AbsentMessageResolver.class); verify(beanContext, times(1)).getSingletonsOf(MessageProvider.class); + verify(beanContext, times(1)).getSingletonsOf(ViewTemplateInputStreamLoader.class); verify(viewContextBuilder, times(1)).templateResolver(templateResolver); verify(viewContextBuilder, times(1)).viewDialects(viewDialects); verify(viewContextBuilder, times(1)).viewDecorators(viewDecorators); verify(viewContextBuilder, times(1)).messageProviders(messageProviders); verify(viewContextBuilder, times(1)).absentMessageResolver(absentMessageResolver); + verify(viewContextBuilder, times(1)) + .templateInputStreamLoaders(viewTemplateInputStreamLoaders); verify(viewContextBuilder, times(1)).build(); verify(singletonFactory, times(1)).addSingleton(viewContext); } diff --git a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java new file mode 100644 index 00000000..8874f01d --- /dev/null +++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java @@ -0,0 +1,78 @@ +/* + * ============================================================================= + * + * 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.IEngineConfiguration; +import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver; +import org.thymeleaf.templateresolver.ITemplateResolver; +import org.thymeleaf.templateresource.ClassLoaderTemplateResource; +import org.thymeleaf.templateresource.ITemplateResource; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + *

+ * Implementation of {@link ITemplateResolver} that extends {@link AbstractConfigurableTemplateResolver} + * and creates {@link ClassLoaderTemplateResource} instances for template resources. + *

+ *

+ * Note a class with this name existed since 1.0, but it was completely rewritten in Thymeleaf 3.0. + *

+ * + * @author Daniel Fernández + * + * @since 3.0.0 + * + */ +public class ThymeleafClassLoaderTemplateResolver + extends AbstractConfigurableTemplateResolver { + + private final List templateInputStreamLoaders; + + public ThymeleafClassLoaderTemplateResolver( + List templateInputStreamLoaders + ) { + this.templateInputStreamLoaders = templateInputStreamLoaders + .stream() + .sorted(Comparator.comparingInt(ViewTemplateInputStreamLoader::priority)) + .collect(Collectors.toList()); + } + + @Override + protected ITemplateResource computeTemplateResource( + final IEngineConfiguration configuration, + final String ownerTemplate, + final String template, + final String resourceName, + final String characterEncoding, + final Map 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..c1308cc8 --- /dev/null +++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResource.java @@ -0,0 +1,154 @@ +/* + * ============================================================================= + * + * 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) { + 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..a21afc8d --- /dev/null +++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtils.java @@ -0,0 +1,144 @@ +/* + * ============================================================================= + * + * 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; + while (index >= 0) { // will always be 0 for the last iteration, as we prefixed the path with '/' + 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/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); From def47435f5e78749fd51245c381851ec3368cf1c Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sun, 18 Jan 2026 20:22:40 +0700 Subject: [PATCH 03/10] update --- .../ThymeleafClassLoaderTemplateResolver.java | 13 ++++--- .../ThymeleafClassLoaderTemplateResource.java | 10 +++--- .../ThymeleafTemplateResourceUtils.java | 35 ++++++++----------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java index 8874f01d..84d81acc 100644 --- a/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java +++ b/ezyhttp-server-thymeleaf/src/main/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafClassLoaderTemplateResolver.java @@ -17,6 +17,7 @@ * * ============================================================================= */ + package com.tvd12.ezyhttp.server.thymeleaf; import com.tvd12.ezyhttp.server.core.view.ViewTemplateInputStreamLoader; @@ -33,15 +34,17 @@ /** *

- * Implementation of {@link ITemplateResolver} that extends {@link AbstractConfigurableTemplateResolver} - * and creates {@link ClassLoaderTemplateResource} instances for template resources. + * Implementation of {@link ITemplateResolver} that extends + * {@link AbstractConfigurableTemplateResolver} and creates + * {@link ClassLoaderTemplateResource} instances for template resources. *

*

- * Note a class with this name existed since 1.0, but it was completely rewritten in Thymeleaf 3.0. + * Note a class with this name existed since 1.0, but it was completely + * rewritten in Thymeleaf 3.0. *

- * + * * @author Daniel Fernández - * + * * @since 3.0.0 * */ 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 index c1308cc8..e648e9e7 100644 --- 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 @@ -17,6 +17,7 @@ * * ============================================================================= */ + package com.tvd12.ezyhttp.server.thymeleaf; import com.tvd12.ezyhttp.server.core.view.ViewTemplateInputStreamLoader; @@ -33,16 +34,17 @@ /** *

- * Implementation of {@link ITemplateResource} representing a resource accessible by a {@link ClassLoader} - * (i.e. living at the class path). + * 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}. + * 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 { 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 index a21afc8d..22cf4b07 100644 --- 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 @@ -17,6 +17,7 @@ * * ============================================================================= */ + package com.tvd12.ezyhttp.server.thymeleaf; import org.thymeleaf.templateresource.ITemplateResource; @@ -24,12 +25,12 @@ /** *

- * Utility methods used by several implementations of {@link ITemplateResource} + * Utility methods used by several implementations of {@link ITemplateResource}. *

* * @author Daniel Fernández * @since 3.0.0 - * + * */ final class ThymeleafTemplateResourceUtils { @@ -42,23 +43,23 @@ static String cleanPath(final String path) { // 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("//"))) { + // 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 + 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; - while (index >= 0) { // will always be 0 for the last iteration, as we prefixed the path with '/' + // 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) == '.') { @@ -69,31 +70,25 @@ static String cleanPath(final String path) { ) { // Token is '..' -> count as a 'top' operation topCount++; - } else if (topCount > 0){ + } 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); - + 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(); } @@ -123,7 +118,7 @@ static String computeBaseName(final String path) { // First remove a trailing '/' if it exists final String basePath = path.charAt(path.length() - 1) == '/' - ? path.substring(0,path.length() - 1) + ? path.substring(0, path.length() - 1) : path; final int slashPos = basePath.lastIndexOf('/'); From 0f0abd8a5ff382c0147f0cb14cb812a636173468 Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sun, 18 Jan 2026 20:31:15 +0700 Subject: [PATCH 04/10] update --- .../ThymeleafTemplateResourceUtilsTest.java | 131 ++++++++++++++ ...meleafClassLoaderTemplateResourceTest.java | 166 ++++++++++++++++++ .../src/test/resources/AllTests.tng.xml | 3 +- 3 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtilsTest.java create mode 100644 ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafClassLoaderTemplateResourceTest.java 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..e9c83c3a --- /dev/null +++ b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/ThymeleafTemplateResourceUtilsTest.java @@ -0,0 +1,131 @@ +package com.tvd12.ezyhttp.server.thymeleaf; + +import com.tvd12.test.assertion.Asserts; +import org.testng.annotations.Test; + +import com.tvd12.ezyhttp.server.thymeleaf.ThymeleafTemplateResourceUtils; + +public class ThymeleafTemplateResourceUtilsTest { + + @Test + public void cleanPathNullEmptyAndSimple() { + // given + String nullPath = null; + String emptyPath = ""; + String simplePath = "templates\\home.html"; + + // when + String nullResult = ThymeleafTemplateResourceUtils.cleanPath(nullPath); + String emptyResult = ThymeleafTemplateResourceUtils.cleanPath(emptyPath); + String simpleResult = ThymeleafTemplateResourceUtils.cleanPath(simplePath); + + // then + Asserts.assertNull(nullResult); + Asserts.assertEquals("", emptyResult); + Asserts.assertEquals("templates/home.html", simpleResult); + } + + @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 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 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 computeBaseNameWithoutSlash() { + // given + String path = "archive.tar.gz"; + + // when + String result = ThymeleafTemplateResourceUtils.computeBaseName(path); + + // then + Asserts.assertEquals("archive.tar", 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..c8447e46 --- /dev/null +++ b/ezyhttp-server-thymeleaf/src/test/java/com/tvd12/ezyhttp/server/thymeleaf/test/ThymeleafClassLoaderTemplateResourceTest.java @@ -0,0 +1,166 @@ +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.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(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/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 @@ - + + From 723aa1a83e55391d0b747b81dbb43b8310ac30e7 Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sun, 18 Jan 2026 20:41:38 +0700 Subject: [PATCH 05/10] update --- .../ThymeleafTemplateResourceUtils.java | 3 +- .../ThymeleafTemplateResourceUtilsTest.java | 41 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) 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 index 22cf4b07..f1deaa6a 100644 --- 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 @@ -46,7 +46,8 @@ static String cleanPath(final String 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("//"))) { + || (!unixPath.contains("/.") && !unixPath.contains("//")) + ) { return unixPath; } // We make sure path starts with '/' in order to simplify the algorithm 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 index e9c83c3a..12ac2dbc 100644 --- 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 @@ -3,8 +3,6 @@ import com.tvd12.test.assertion.Asserts; import org.testng.annotations.Test; -import com.tvd12.ezyhttp.server.thymeleaf.ThymeleafTemplateResourceUtils; - public class ThymeleafTemplateResourceUtilsTest { @Test @@ -13,16 +11,19 @@ public void cleanPathNullEmptyAndSimple() { 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 @@ -37,6 +38,18 @@ public void cleanPathRelativeWithDotsAndDoubleSlash() { 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 @@ -52,6 +65,30 @@ public void cleanPathRootBasedWithParentSegments() { 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 computeRelativeLocationWithSeparator() { // given From 1fc6cb140c2a918f39a07db4d977ae2119000abb Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sun, 18 Jan 2026 20:46:53 +0700 Subject: [PATCH 06/10] update --- .../ThymeleafTemplateResourceUtilsTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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 index 12ac2dbc..dbdb8838 100644 --- 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 @@ -89,6 +89,30 @@ public void cleanPathContainsNoBackText() { 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 @@ -154,6 +178,18 @@ public void computeBaseNameWithSlashAndDot() { 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 @@ -165,4 +201,16 @@ public void computeBaseNameWithoutSlash() { // 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); + } } From 5a525ddd5b45b7bcc2362765276b38bf75068bf1 Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Mon, 19 Jan 2026 08:45:58 +0700 Subject: [PATCH 07/10] add callback to ResourceRequestHandler --- .../core/handler/ResourceRequestHandler.java | 26 ++++ .../handler/ResourceRequestHandlerTest.java | 123 ++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/handler/ResourceRequestHandler.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/handler/ResourceRequestHandler.java index 9727ecbf..fba1f2ca 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/handler/ResourceRequestHandler.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/handler/ResourceRequestHandler.java @@ -31,9 +31,29 @@ public class ResourceRequestHandler implements RequestHandler { private final EzyInputStreamLoader inputStreamLoader; private final ResourceDownloadManager downloadManager; private final int defaultTimeout; + private final EzyResultCallback callback; private final ActualContentTypeDetector actualContentTypeDetector = ActualContentTypeDetector.getInstance(); + public ResourceRequestHandler( + String resourcePath, + String resourceURI, + String resourceExtension, + EzyInputStreamLoader inputStreamLoader, + ResourceDownloadManager downloadManager, + int defaultTimeout + ) { + this( + resourcePath, + resourceURI, + resourceExtension, + inputStreamLoader, + downloadManager, + defaultTimeout, + null + ); + } + public ResourceRequestHandler( String resourcePath, String resourceURI, @@ -170,6 +190,9 @@ public void onResponse(Boolean response) { processWithLogException(inputStream::close); servletResponse.setStatus(statusCodeFinal); asyncContext.complete(); + if (callback != null) { + callback.onResponse(response); + } } @Override @@ -177,6 +200,9 @@ public void onException(Exception e) { processWithLogException(inputStream::close); servletResponse.setStatus(StatusCodes.INTERNAL_SERVER_ERROR); asyncContext.complete(); + if (callback != null) { + callback.onException(e); + } } } ); diff --git a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/handler/ResourceRequestHandlerTest.java b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/handler/ResourceRequestHandlerTest.java index e84371de..283b2a67 100644 --- a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/handler/ResourceRequestHandlerTest.java +++ b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/handler/ResourceRequestHandlerTest.java @@ -69,6 +69,61 @@ public void handleAsyncTest() throws Exception { verify(asyncContext, times(1)).complete(); } + @SuppressWarnings("unchecked") + @Test + public void handleAsyncWitCallbackTest() throws Exception { + // given + String resourcePath = "static/index.html"; + String resourceURI = "/index.html"; + String resourceExtension = "html"; + ResourceDownloadManager downloadManager = new ResourceDownloadManager(); + EzyInputStreamLoader inputStreamLoader = mock(EzyInputStreamLoader.class); + InputStream inputStream = mock(InputStream.class); + when(inputStreamLoader.load(resourcePath)).thenReturn(inputStream); + EzyResultCallback callback = mock(EzyResultCallback.class); + ResourceRequestHandler sut = new ResourceRequestHandler( + resourcePath, + resourceURI, + resourceExtension, + inputStreamLoader, + downloadManager, + 0, + callback + ); + + RequestArguments arguments = mock(RequestArguments.class); + + AsyncContext asyncContext = mock(AsyncContext.class); + when(arguments.getAsyncContext()).thenReturn(asyncContext); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(asyncContext.getResponse()).thenReturn(response); + + ServletOutputStream outputStream = mock(ServletOutputStream.class); + when(response.getOutputStream()).thenReturn(outputStream); + + when(asyncContext.getResponse()).thenReturn(response); + + // when + sut.handle(arguments); + + // then + Asserts.assertTrue(sut.isAsync()); + Asserts.assertEquals(HttpMethod.GET, sut.getMethod()); + Asserts.assertEquals("/index.html", sut.getRequestURI()); + Asserts.assertEquals(ContentTypes.TEXT_HTML_UTF8, sut.getResponseContentType()); + Thread.sleep(300); + downloadManager.stop(); + verify(arguments, times(1)).getAsyncContext(); + verify(response, times(1)).getOutputStream(); + verify(response, times(1)).setStatus(StatusCodes.OK); + verify(asyncContext, times(1)).getResponse(); + verify(asyncContext, times(1)).complete(); + verify(inputStreamLoader, times(1)).load(resourcePath); + verify(inputStream, times(1)).close(); + verify(callback, times(1)).onResponse(true); + } + @Test public void handleAsyncWithRangeTest() throws Exception { // given @@ -299,6 +354,74 @@ public void handleWithDrainExceptionTest() throws Exception { verify(asyncContext, times(1)).complete(); } + @SuppressWarnings("unchecked") + @Test + public void handleWithDrainExceptionWitCallbackTest() throws Exception { + // given + String resourcePath = "static/index.html"; + String resourceURI = "/index.html"; + String resourceExtension = "html"; + ResourceDownloadManager downloadManager = new ResourceDownloadManager(); + EzyInputStreamLoader inputStreamLoader = mock(EzyInputStreamLoader.class); + InputStream inputStream = mock(InputStream.class); + when(inputStream.read(any())).thenReturn(1); + when(inputStreamLoader.load(resourcePath)).thenReturn(inputStream); + EzyResultCallback callback = mock(EzyResultCallback.class); + ResourceRequestHandler sut = new ResourceRequestHandler( + resourcePath, + resourceURI, + resourceExtension, + inputStreamLoader, + downloadManager, + 0, + callback + ); + + RequestArguments arguments = mock(RequestArguments.class); + + AsyncContext asyncContext = mock(AsyncContext.class); + when(arguments.getAsyncContext()).thenReturn(asyncContext); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(asyncContext.getResponse()).thenReturn(response); + + ServletOutputStream outputStream = mock(ServletOutputStream.class); + when(response.getOutputStream()).thenReturn(outputStream); + doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt()); + + when(asyncContext.getResponse()).thenReturn(response); + + // when + sut.handle(arguments); + + // then + Asserts.assertEquals(HttpMethod.GET, sut.getMethod()); + Asserts.assertEquals("/index.html", sut.getRequestURI()); + Asserts.assertEquals(ContentTypes.TEXT_HTML_UTF8, sut.getResponseContentType()); + Thread.sleep(300); + downloadManager.stop(); + verify(arguments, times(1)).getAsyncContext(); + verify(response, times(1)).getOutputStream(); + verify(arguments, times(1)).getHeader("Range"); + verify(response, times(1)).setStatus(StatusCodes.INTERNAL_SERVER_ERROR); + verify(response, times(1)).setContentType("text/html; charset=utf-8"); + verify(asyncContext, times(1)).getResponse(); + verify(asyncContext, times(1)).complete(); + verify(inputStreamLoader, times(1)).load(resourcePath); + verify(inputStream, times(1)).read(any()); + verify(inputStream, times(1)).close(); + verify(callback, times(1)).onException(any(IOException.class)); + + verifyNoMoreInteractions( + arguments, + response, + asyncContext, + inputStreamLoader, + inputStream, + callback + ); + } + @SuppressWarnings("unchecked") @Test public void handleWithDrainExceptionWhenCallTest() throws Exception { From 3e4e7671ecb80be8a63c101716ba68ff0a79d89f Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Mon, 19 Jan 2026 09:50:09 +0700 Subject: [PATCH 08/10] update --- .../server/core/request/RequestArguments.java | 19 ++++++-- .../core/request/SimpleRequestArguments.java | 14 +++--- .../request/SimpleRequestArgumentsTest.java | 43 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java index 1f33573a..2b570d0b 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/RequestArguments.java @@ -10,9 +10,9 @@ import javax.servlet.http.HttpServletResponse; import java.util.Map; -import static com.tvd12.ezyfox.io.EzyStrings.EMPTY_STRING; -import static com.tvd12.ezyfox.io.EzyStrings.isBlank; +import static com.tvd12.ezyfox.io.EzyStrings.*; +@SuppressWarnings("MethodCount") public interface RequestArguments extends BodyData, EzyReleasable { HttpMethod getMethod(); @@ -79,7 +79,20 @@ default String getCookieValue(String name, String defaultValue) { return answer != null ? answer : defaultValue; } - String getRequestValue(String name); + String getRequestValue(String argumentName); + + default String getRequestValueAnyway( + String... argumentNames + ) { + String value = null; + for (String argumentName : argumentNames) { + value = getRequestValueAnyway(argumentName); + if (isNotBlank(value)) { + break; + } + } + return value; + } default String getRequestValueAnyway( String argumentName diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java index f1e64ae2..28da1fa9 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java @@ -246,22 +246,22 @@ public Cookie getCookie(String name) { } @Override - public String getRequestValue(String name) { - String value = (String) request.getAttribute(name); + public String getRequestValue(String argumentName) { + String value = (String) request.getAttribute(argumentName); if (isBlank(value)) { - value = getPathVariable(name); + value = getPathVariable(argumentName); } if (isBlank(value)) { - value = getHeader(name); + value = getHeader(argumentName); } if (isBlank(value)) { - value = getParameter(name); + value = getParameter(argumentName); } if (isBlank(value)) { - value = getCookieValue(name); + value = getCookieValue(argumentName); } if (isBlank(value)) { - value = getArgument(name); + value = getArgument(argumentName); } return value; } diff --git a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java index 15868808..6970456c 100644 --- a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java +++ b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java @@ -401,6 +401,49 @@ public void getRequestValueAnywayCaseInAttributeTest() { verifyNoMoreInteractions(request); } + @Test + public void getRequestValueAnywayCaseInAttribute2Test() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + when(request.getAttribute("hello")).thenReturn("world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + + // when + String actual = sut.getRequestValueAnyway("worlD", "hello"); + + // then + Asserts.assertEquals(actual, "world"); + + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getAttribute("worlD"); + verify(request, times(1)).getAttribute("world"); + verify(request, times(1)).getAttribute("World"); + verify(request, times(1)).getAttribute("hello"); + verify(request, times(1)).getRequestURI(); + verifyNoMoreInteractions(request); + } + + @Test + public void getRequestValueAnywayCaseInAttributeEmtpyTest() { + // given + SimpleRequestArguments sut = new SimpleRequestArguments(); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/hello/world"); + when(request.getAttribute("hello")).thenReturn("world"); + sut.setRequest(request); + sut.setUriTemplate("/hello/hello"); + + // when + String actual = sut.getRequestValueAnyway(); + + // then + Asserts.assertNull(actual); + verifyNoMoreInteractions(request); + } + @Test public void getRequestValueAnywayCaseInPathVariableTest() { // given From 779833703fc1f2d8f105fa4d083dabfc9df8e7f8 Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Mon, 19 Jan 2026 22:30:14 +0700 Subject: [PATCH 09/10] update --- .../core/request/SimpleRequestArguments.java | 18 +++++++++--------- .../server/core/util/HttpServletRequests.java | 7 ++++++- .../request/SimpleRequestArgumentsTest.java | 8 ++++---- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java index 28da1fa9..cad9cc97 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/request/SimpleRequestArguments.java @@ -18,10 +18,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import static com.tvd12.ezyfox.io.EzyStrings.EMPTY_STRING; @@ -247,10 +244,7 @@ public Cookie getCookie(String name) { @Override public String getRequestValue(String argumentName) { - String value = (String) request.getAttribute(argumentName); - if (isBlank(value)) { - value = getPathVariable(argumentName); - } + String value = getCookieValue(argumentName); if (isBlank(value)) { value = getHeader(argumentName); } @@ -258,7 +252,13 @@ public String getRequestValue(String argumentName) { value = getParameter(argumentName); } if (isBlank(value)) { - value = getCookieValue(argumentName); + value = getPathVariable(argumentName); + } + if (isBlank(value)) { + value = Objects.toString( + request.getAttribute(argumentName), + null + ); } if (isBlank(value)) { value = getArgument(argumentName); diff --git a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java index 45fc4a68..5fec8f6b 100644 --- a/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java +++ b/ezyhttp-server-core/src/main/java/com/tvd12/ezyhttp/server/core/util/HttpServletRequests.java @@ -3,6 +3,8 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import java.util.Objects; + import static com.tvd12.ezyfox.io.EzyStrings.*; public final class HttpServletRequests { @@ -36,7 +38,10 @@ public static String getRequestValue( String name, boolean checkCookie ) { - String value = (String) request.getAttribute(name); + String value = Objects.toString( + request.getAttribute(name), + null + ); if (isBlank(value)) { value = request.getHeader(name); } diff --git a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java index 6970456c..80d67b21 100644 --- a/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java +++ b/ezyhttp-server-core/src/test/java/com/tvd12/ezyhttp/server/core/test/request/SimpleRequestArgumentsTest.java @@ -461,7 +461,7 @@ public void getRequestValueAnywayCaseInPathVariableTest() { Asserts.assertEquals(actual1, "world"); Asserts.assertEquals(actual2, null); - verify(request, times(1)).getAttribute("hello"); + verify(request, times(0)).getAttribute("hello"); verify(request, times(1)).getAttribute("worlD"); verify(request, times(1)).getAttribute("world"); verify(request, times(1)).getAttribute("World"); @@ -487,7 +487,7 @@ public void getRequestValueAnywayCaseInHeaderTest() { Asserts.assertEquals(actual1, "world"); Asserts.assertEquals(actual2, null); - verify(request, times(1)).getAttribute("hello"); + verify(request, times(0)).getAttribute("hello"); verify(request, times(1)).getAttribute("worlD"); verify(request, times(1)).getAttribute("world"); verify(request, times(1)).getAttribute("World"); @@ -513,7 +513,7 @@ public void getRequestValueAnywayCaseInParameterTest() { Asserts.assertEquals(actual1, "world"); Asserts.assertEquals(actual2, null); - verify(request, times(1)).getAttribute("hello"); + verify(request, times(0)).getAttribute("hello"); verify(request, times(1)).getAttribute("worlD"); verify(request, times(1)).getAttribute("world"); verify(request, times(1)).getAttribute("World"); @@ -539,7 +539,7 @@ public void getRequestValueAnywayCaseInCookieTest() { Asserts.assertEquals(actual1, "world"); Asserts.assertEquals(actual2, null); - verify(request, times(1)).getAttribute("hello"); + verify(request, times(0)).getAttribute("hello"); verify(request, times(1)).getAttribute("worlD"); verify(request, times(1)).getAttribute("world"); verify(request, times(1)).getAttribute("World"); From aa26447c3b35cc798cd27e98b6654c27ff68c3f6 Mon Sep 17 00:00:00 2001 From: Dung Ta Van Date: Sat, 24 Jan 2026 16:52:29 +0700 Subject: [PATCH 10/10] update ThymeleafClassLoaderTemplateResource --- .../ThymeleafClassLoaderTemplateResource.java | 3 ++ ...meleafClassLoaderTemplateResourceTest.java | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) 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 index e648e9e7..ecdfaef0 100644 --- 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 @@ -105,6 +105,9 @@ public Reader reader() throws IOException { if (inputStream == null) { for (ViewTemplateInputStreamLoader loader : templateInputStreamLoaders) { inputStream = loader.load(template, path); + if (inputStream != null) { + break; + } } } if (inputStream == null) { 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 index c8447e46..ce3c4238 100644 --- 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 @@ -10,6 +10,7 @@ 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; @@ -106,6 +107,38 @@ public void readerWithInputStreamLoader() throws Exception { 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