From 063b48723b136e2ee7bba22a450a31f5d928824f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 6 May 2025 09:06:15 +0200 Subject: [PATCH 01/12] LPD-52709 Remove LPD-11848 feature flag and disable LanguageFilter for internal ESM files We need to use language labels modules for caching to be feasible so we will split .js modules in two parts: code itself and labels in a different file, thus we don't need the LanguageFilter to kick in any more. This will make the use of ETags for .js files not necessary any more. Note however that legacy /combo URLs are still routed through the filter. In any case this should only be used for YUI code, not for ESM. --- .../liferay/portal/language/LanguageImpl.java | 19 -------- .../filters/language/LanguageFilter.java | 15 ++++++ portal-impl/src/portal.properties | 5 -- .../portal/language/LanguageImplTest.java | 46 ------------------- .../kernel/frontend/esm/FrontendESMUtil.java | 10 ++++ 5 files changed, 25 insertions(+), 70 deletions(-) delete mode 100644 portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java diff --git a/portal-impl/src/com/liferay/portal/language/LanguageImpl.java b/portal-impl/src/com/liferay/portal/language/LanguageImpl.java index 4dfbd0c4e3ba10..acf45454dd490c 100644 --- a/portal-impl/src/com/liferay/portal/language/LanguageImpl.java +++ b/portal-impl/src/com/liferay/portal/language/LanguageImpl.java @@ -17,7 +17,6 @@ import com.liferay.portal.kernel.cookies.constants.CookiesConstants; import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.exception.SystemException; -import com.liferay.portal.kernel.feature.flag.FeatureFlagManagerUtil; import com.liferay.portal.kernel.language.Language; import com.liferay.portal.kernel.language.LanguageWrapper; import com.liferay.portal.kernel.log.Log; @@ -1646,19 +1645,6 @@ public String process( Supplier resourceBundleSupplier, Locale locale, String content) { - if (FeatureFlagManagerUtil.isEnabled("LPD-11848")) { - Matcher matcher = _liferayLanguageImportPattern.matcher(content); - - if (matcher.find()) { - return content; - } - } - else { - content = content.replaceAll( - _LIFERAY_LANGUAGE_IMPORT_REGEXP, - "{/*removed: await import('@liferay/language...')*/}"); - } - StringBundler sb = null; ResourceBundle resourceBundle = null; @@ -2029,9 +2015,6 @@ private void _updateLastModified() { private static final String _GROUP_LOCALES_PORTAL_CACHE_NAME = LanguageImpl.class.getName() + "._groupLocalesPortalCache"; - private static final String _LIFERAY_LANGUAGE_IMPORT_REGEXP = - "await import\\(.@liferay/language/.+?/all\\.js.\\)"; - private static final double _STORAGE_SIZE_DENOMINATOR = 1024.0; private static final Log _log = LogFactoryUtil.getLog(LanguageImpl.class); @@ -2041,8 +2024,6 @@ private void _updateLastModified() { private static PortalCache _companyLocalesPortalCache; private static PortalCache _groupLocalesPortalCache; private static volatile long _lastModified = System.currentTimeMillis(); - private static final Pattern _liferayLanguageImportPattern = - Pattern.compile(_LIFERAY_LANGUAGE_IMPORT_REGEXP, Pattern.MULTILINE); private static final Pattern _pattern = Pattern.compile( "Liferay\\s*\\.\\s*Language\\s*\\.\\s*get\\s*" + "\\(\\s*[\"']([^)]+)[\"']\\s*\\)", diff --git a/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java b/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java index bc498ed47913d0..00501a3f8e8380 100644 --- a/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java +++ b/portal-impl/src/com/liferay/portal/servlet/filters/language/LanguageFilter.java @@ -7,6 +7,7 @@ import com.liferay.petra.string.CharPool; import com.liferay.petra.string.StringPool; +import com.liferay.portal.kernel.frontend.esm.FrontendESMUtil; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; @@ -48,6 +49,20 @@ public void init(FilterConfig filterConfig) { super.init(filterConfig); } + @Override + public boolean isFilterEnabled( + HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse) { + + if (FrontendESMUtil.isInternalESMRequest( + httpServletRequest.getRequestURI())) { + + return false; + } + + return super.isFilterEnabled(httpServletRequest, httpServletResponse); + } + @Override protected void processFilter( HttpServletRequest httpServletRequest, diff --git a/portal-impl/src/portal.properties b/portal-impl/src/portal.properties index e239f503bc27a4..c1448fffd08847 100644 --- a/portal-impl/src/portal.properties +++ b/portal-impl/src/portal.properties @@ -6521,11 +6521,6 @@ # feature.flag.LPD-11342=false - # - # Env: LIFERAY_FEATURE_PERIOD_FLAG_PERIOD__UPPERCASEL__UPPERCASEP__UPPERCASED__MINUS__NUMBER1__NUMBER1__NUMBER8__NUMBER4__NUMBER8_ - # - feature.flag.LPD-11848=false - # # Env: LIFERAY_FEATURE_PERIOD_FLAG_PERIOD__UPPERCASEL__UPPERCASEP__UPPERCASED__MINUS__NUMBER1__NUMBER3__NUMBER3__NUMBER1__NUMBER1_ # diff --git a/portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java b/portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java deleted file mode 100644 index c1bec1cb7ff704..00000000000000 --- a/portal-impl/test/unit/com/liferay/portal/language/LanguageImplTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * SPDX-FileCopyrightText: (c) 2024 Liferay, Inc. https://liferay.com - * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 - */ - -package com.liferay.portal.language; - -import com.liferay.portal.kernel.test.rule.AggregateTestRule; -import com.liferay.portal.test.rule.LiferayUnitTestRule; - -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; - -/** - * @author Bryce Osterhaus - */ -public class LanguageImplTest { - - @ClassRule - @Rule - public static final AggregateTestRule aggregateTestRule = - LiferayUnitTestRule.INSTANCE; - - @Test - public void testProcess() { - LanguageImpl languageImpl = new LanguageImpl(); - - Assert.assertEquals( - "foo;bar;{/*removed: await import('@liferay/language...')*/};baz;", - languageImpl.process( - null, null, - "foo;bar;await import('@liferay/language/foo/all.js');baz;" - ).toString()); - Assert.assertEquals( - "foo;{/*removed: await import('@liferay/language...')*/};bar;" + - "{/*removed: await import('@liferay/language...')*/};baz;", - languageImpl.process( - null, null, - "foo;await import('@liferay/language/foo/all.js');bar;" + - "await import('@liferay/language/foo/all.js');baz;" - ).toString()); - } - -} \ No newline at end of file diff --git a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java index 788e0cef0a1bdd..8948ac91b87cce 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java +++ b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/FrontendESMUtil.java @@ -37,4 +37,14 @@ public static String buildURL( submodule, ".js"); } + public static boolean isInternalESMRequest(String requestURI) { + if (requestURI.contains("/__liferay__/") || + requestURI.startsWith("/o/js/language/")) { + + return true; + } + + return false; + } + } \ No newline at end of file From ab6effbfb87e0a3ca96cfa274f5e8af3929281fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 6 May 2025 09:54:00 +0200 Subject: [PATCH 02/12] LPD-52709 Remove language servlet We will integrate all resources managed by frontend-js-web in a single filter that will delegate to request helpers in the next commit. This is to be able to centralize all frontend resources management (CSS, JS, source maps, etc) in one place and reuse strategies for serving them. --- .../servlet/FrontendJsWebLanguageServlet.java | 254 ------------------ .../internal/servlet/dependencies/all.js.tpl | 11 - 2 files changed, 265 deletions(-) delete mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java delete mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/servlet/dependencies/all.js.tpl diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java deleted file mode 100644 index 5fd1d002e4e5ab..00000000000000 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/FrontendJsWebLanguageServlet.java +++ /dev/null @@ -1,254 +0,0 @@ -/** - * SPDX-FileCopyrightText: (c) 2024 Liferay, Inc. https://liferay.com - * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 - */ - -package com.liferay.frontend.js.web.internal.servlet; - -import com.liferay.osgi.service.tracker.collections.map.ServiceTrackerMap; -import com.liferay.osgi.service.tracker.collections.map.ServiceTrackerMapFactory; -import com.liferay.petra.string.StringPool; -import com.liferay.portal.kernel.json.JSONArray; -import com.liferay.portal.kernel.json.JSONException; -import com.liferay.portal.kernel.json.JSONFactory; -import com.liferay.portal.kernel.json.JSONObject; -import com.liferay.portal.kernel.language.Language; -import com.liferay.portal.kernel.log.Log; -import com.liferay.portal.kernel.log.LogFactoryUtil; -import com.liferay.portal.kernel.servlet.HttpHeaders; -import com.liferay.portal.kernel.util.ContentTypes; -import com.liferay.portal.kernel.util.DigesterUtil; -import com.liferay.portal.kernel.util.LocaleUtil; -import com.liferay.portal.kernel.util.Portal; -import com.liferay.portal.kernel.util.StringUtil; -import com.liferay.portal.kernel.util.URLUtil; - -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; - -import java.net.URL; - -import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; - -import org.osgi.framework.BundleContext; -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; - -/** - * @author Iván Zaera Avellón - */ -@Component( - property = { - "osgi.http.whiteboard.servlet.name=Language Resources Servlet", - "osgi.http.whiteboard.servlet.pattern=/js/language/*", - "service.ranking:Integer=" + (Integer.MAX_VALUE - 1000) - }, - service = Servlet.class -) -public class FrontendJsWebLanguageServlet extends HttpServlet { - - @Activate - protected void activate(BundleContext bundleContext) { - _eTags.clear(); - - _serviceTrackerMap = ServiceTrackerMapFactory.openSingleValueMap( - bundleContext, ServletContext.class, null, - (serviceReference, emitter) -> { - ServletContext servletContext = bundleContext.getService( - serviceReference); - - try { - emitter.emit(servletContext.getContextPath()); - } - finally { - bundleContext.ungetService(serviceReference); - } - }); - } - - @Deactivate - protected void deactivate() { - _eTags.clear(); - - _serviceTrackerMap.close(); - - _serviceTrackerMap = null; - } - - @Override - protected void doGet( - HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse) - throws IOException, ServletException { - - String pathInfo = httpServletRequest.getPathInfo(); - - // Check if path is valid - - String[] parts = pathInfo.split(StringPool.SLASH); - - if ((parts.length != 4) || !parts[3].equals("all.js")) { - httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); - - return; - } - - // Check if browser cache can be used - - String ifNoneMatch = httpServletRequest.getHeader( - HttpHeaders.IF_NONE_MATCH); - - if (ifNoneMatch != null) { - String eTag = _eTags.get(pathInfo); - - if ((eTag != null) && eTag.equals(ifNoneMatch)) { - httpServletResponse.setStatus( - HttpServletResponse.SC_NOT_MODIFIED); - httpServletResponse.setContentLength(0); - - return; - } - } - - // Check if servlet context exists - - String modulePath = _portal.getPathModule(); - String proxyPath = _portal.getPathProxy(); - String webContextPath = parts[2]; - - ServletContext servletContext = _serviceTrackerMap.getService( - modulePath.substring(proxyPath.length()) + StringPool.SLASH + - webContextPath); - - if (servletContext == null) { - httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); - - return; - } - - // Send response - - Locale locale = LocaleUtil.fromLanguageId(parts[1]); - - String content = _getContent(locale, servletContext); - - if (content == null) { - httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND); - - return; - } - - String etag = - StringPool.QUOTE + DigesterUtil.digestBase64("SHA-1", content) + - StringPool.QUOTE; - - _eTags.put(pathInfo, etag); - - httpServletResponse.setCharacterEncoding(StringPool.UTF8); - httpServletResponse.setContentType(ContentTypes.TEXT_JAVASCRIPT_UTF8); - httpServletResponse.setHeader(HttpHeaders.ETAG, etag); - - PrintWriter printWriter = httpServletResponse.getWriter(); - - printWriter.write(content); - } - - private static String _loadTemplate(String name) { - try (InputStream inputStream = - FrontendJsWebLanguageServlet.class.getResourceAsStream( - "dependencies/" + name)) { - - return StringUtil.read(inputStream); - } - catch (Exception exception) { - _log.error("Unable to read template " + name, exception); - } - - return StringPool.BLANK; - } - - private String _getContent(Locale locale, ServletContext servletContext) - throws IOException { - - JSONArray languageKeysJSONArray = _getLanguageKeysJSONArray( - servletContext); - - if (languageKeysJSONArray == null) { - return null; - } - - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < languageKeysJSONArray.length(); i++) { - String key = languageKeysJSONArray.getString(i); - - String label = _language.get(locale, key); - - sb.append(StringPool.APOSTROPHE); - sb.append(key.replaceAll("'", "\\\\'")); - sb.append("':'"); - sb.append(label.replaceAll("'", "\\\\'")); - sb.append("',\n"); - } - - return StringUtil.replace( - _TPL_JAVA_SCRIPT, new String[] {"[$LABELS$]"}, - new String[] {sb.toString()}); - } - - private JSONArray _getLanguageKeysJSONArray(ServletContext servletContext) - throws IOException { - - URL url = servletContext.getResource("/language.json"); - - if (url == null) { - return null; - } - - try { - JSONObject jsonObject = _jsonFactory.createJSONObject( - URLUtil.toString(url)); - - return jsonObject.getJSONArray("keys"); - } - catch (JSONException jsonException) { - throw new IOException( - "Invalid language JSON file " + url, jsonException); - } - } - - private static final String _TPL_JAVA_SCRIPT; - - private static final Log _log = LogFactoryUtil.getLog( - FrontendJsWebLanguageServlet.class); - - static { - _TPL_JAVA_SCRIPT = _loadTemplate("all.js.tpl"); - } - - private final ConcurrentHashMap _eTags = - new ConcurrentHashMap<>(); - - @Reference - private JSONFactory _jsonFactory; - - @Reference - private Language _language; - - @Reference - private Portal _portal; - - private ServiceTrackerMap _serviceTrackerMap; - -} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/servlet/dependencies/all.js.tpl b/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/servlet/dependencies/all.js.tpl deleted file mode 100644 index da968c126f8a80..00000000000000 --- a/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/servlet/dependencies/all.js.tpl +++ /dev/null @@ -1,11 +0,0 @@ -const labels = { -[$LABELS$]}; - -if (Liferay && Liferay.Language && Liferay.Language._cache) { - Liferay.Language._cache = { - ...Liferay.Language._cache, - ...labels - }; -} - -export default labels; \ No newline at end of file From 1c6d11667d5e487534a614d1bcf0cbb30b50f778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Wed, 11 Jun 2025 15:10:42 +0200 Subject: [PATCH 03/12] LPD-52709 Add configuration option for I18N labels modules --- .../configuration/FrontendCachingConfiguration.java | 12 ++++++++++++ .../src/main/resources/content/Language.properties | 4 ++++ ...ient-extension-type-instance-settings.schema.json | 12 ++++++++++++ portal-impl/src/portal-osgi-configuration.properties | 8 ++++++++ 4 files changed, 36 insertions(+) diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java index 78973d971b490e..ec73ae8fbe5195 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/configuration/FrontendCachingConfiguration.java @@ -35,6 +35,12 @@ public interface FrontendCachingConfiguration { ) public long esModulesMaxAge(); + @Meta.AD( + deflt = "3600", description = "labels-modules-max-age-help", + name = "labels-modules-max-age", required = false + ) + public long labelsModulesMaxAge(); + @Meta.AD( deflt = "false", description = "send-no-cache-for-css-style-sheets-help", @@ -48,4 +54,10 @@ public interface FrontendCachingConfiguration { ) public boolean sendNoCacheForESModules(); + @Meta.AD( + deflt = "false", description = "send-no-cache-for-labels-modules-help", + name = "send-no-cache-for-labels-modules", required = false + ) + public boolean sendNoCacheForLabelsModules(); + } \ No newline at end of file diff --git a/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties b/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties index 6a938b8763ae07..e4e90929342c69 100644 --- a/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties +++ b/modules/apps/portal-language/portal-language-lang/src/main/resources/content/Language.properties @@ -10454,6 +10454,8 @@ label-text=Label Text label-x-was-added-to-the-list=Label {0} was added to the list. label-x-was-removed-from-the-list=Label {0} was removed from the list. labels=Labels +labels-modules-max-age=Labels Modules Max Age Directive Value +labels-modules-max-age-help=Set the value of the "max-age" directive in the "Cache-Control" header for labels modules. landscape-phone=Landscape Phone lang.dir=ltr lang.line.begin=left @@ -17189,6 +17191,8 @@ send-no-cache-for-css-style-sheets=Send No Cache Directive For CSS Style Sheets send-no-cache-for-css-style-sheets-help=If checked, a "no-cache" directive (instead of "must-revalidate") will be sent in the "Cache-Control" header for CSS style sheets. send-no-cache-for-es-modules=Send No Cache Directive For ES Modules send-no-cache-for-es-modules-help=If checked, a "no-cache" directive (instead of "must-revalidate") will be sent in the "Cache-Control" header for ES modules. +send-no-cache-for-labels-modules=Send No Cache Directive For Labels Modules +send-no-cache-for-labels-modules-help=If checked, a "no-cache" directive (instead of "must-revalidate") will be sent in the "Cache-Control" header for labels modules. send-notifications-to-blogs-entry-creator=Send Notifications to Blogs Entry Creator send-order-email=Send Order Email send-order-email-to-users=Send order email to users? diff --git a/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json b/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json index a4f16c6fab2add..8af18851965280 100644 --- a/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json +++ b/modules/sdk/gradle-plugins-workspace/src/main/resources/schemas/client-extension-type-instance-settings.schema.json @@ -2008,6 +2008,12 @@ "title": "ES Modules Max Age Directive Value", "type": "number" }, + "labelsModulesMaxAge": { + "default": 3600, + "description": "Sets the value that will be sent to the browser as max-age directive in Cache-Control header for modules that contain translation labels.", + "title": "Labels Modules max-age Directive Value", + "type": "number" + }, "pid": { "const": "com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration", "title": "Frontend Caching" @@ -2023,6 +2029,12 @@ "description": "If checked, a \"no-cache\" directive (instead of \"must-revalidate\") will be sent in the \"Cache-Control\" header for ES modules.", "title": "Send No Cache Directive For ES Modules", "type": "boolean" + }, + "sendNoCacheForLabelsModules": { + "default": false, + "description": "Send no-cache (instead of must-revalidate) directive in Cache-Control header for modules that contain translation labels.", + "title": "Send no-cache Directive For Labels Modules", + "type": "boolean" } }, "required": [ diff --git a/portal-impl/src/portal-osgi-configuration.properties b/portal-impl/src/portal-osgi-configuration.properties index a4f34807b88af3..40a6b9489345fe 100644 --- a/portal-impl/src/portal-osgi-configuration.properties +++ b/portal-impl/src/portal-osgi-configuration.properties @@ -2727,6 +2727,10 @@ # #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_esModulesMaxAge= # + # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_CONFIGURATION_PERIOD__UPPERCASEF_RONTEND_UPPERCASEC_ACHING_UPPERCASEC_ONFIGURATION_UNDERLINE_LABELS_UPPERCASEM_ODULES_UPPERCASEM_AX_UPPERCASEA_GE + # + #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_labelsModulesMaxAge= + # # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_CONFIGURATION_PERIOD__UPPERCASEF_RONTEND_UPPERCASEC_ACHING_UPPERCASEC_ONFIGURATION_UNDERLINE_SEND_UPPERCASEN_O_UPPERCASEC_ACHE_UPPERCASEF_OR_UPPERCASEC__UPPERCASES__UPPERCASES__UPPERCASES_TYLE_UPPERCASES_HEETS # #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_sendNoCacheForCSSStyleSheets= @@ -2735,6 +2739,10 @@ # #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_sendNoCacheForESModules= # + # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_CONFIGURATION_PERIOD__UPPERCASEF_RONTEND_UPPERCASEC_ACHING_UPPERCASEC_ONFIGURATION_UNDERLINE_SEND_UPPERCASEN_O_UPPERCASEC_ACHE_UPPERCASEF_OR_UPPERCASEL_ABELS_UPPERCASEM_ODULES + # + #configuration.override.com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration_sendNoCacheForLabelsModules= + # # Env: LIFERAY_CONFIGURATION_PERIOD_OVERRIDE_PERIOD_COM_PERIOD_LIFERAY_PERIOD_FRONTEND_PERIOD_JS_PERIOD_WEB_PERIOD_INTERNAL_PERIOD_SESSION_PERIOD_TIMEOUT_PERIOD_CONFIGURATION_PERIOD__UPPERCASES_ESSION_UPPERCASET_IMEOUT_UPPERCASEC_ONFIGURATION_UNDERLINE_AUTO_UPPERCASEE_XTEND # #configuration.override.com.liferay.frontend.js.web.internal.session.timeout.configuration.SessionTimeoutConfiguration_autoExtend= From b73c33b6dbade492437eebdcfcf0a40ab515660b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Wed, 11 Jun 2025 15:11:50 +0200 Subject: [PATCH 04/12] LPD-52709 Add a frontend resource request handler for I18N labels modules --- ...ndJsWebDynamicJSImportMapsContributor.java | 10 +- .../resource/LanguageFrontendResource.java | 144 ++++++++++++++++++ ...anguageFrontendResourceRequestHandler.java | 114 ++++++++++++++ .../filter/FrontendResourceFilter.java | 18 +++ .../internal/resource/dependencies/all.js.tpl | 11 ++ 5 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java create mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java create mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/resource/dependencies/all.js.tpl diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java index 856ba654bbcbf9..e810e28554ecb3 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/js/importmaps/extender/FrontendJsWebDynamicJSImportMapsContributor.java @@ -6,6 +6,7 @@ package com.liferay.frontend.js.web.internal.js.importmaps.extender; import com.liferay.frontend.js.importmaps.extender.DynamicJSImportMapsContributor; +import com.liferay.frontend.js.web.internal.resource.handler.LanguageFrontendResourceRequestHandler; import com.liferay.petra.string.StringPool; import com.liferay.portal.kernel.exception.PortalException; import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; @@ -31,14 +32,19 @@ public void writeGlobalImports( HttpServletRequest httpServletRequest, Writer writer) throws IOException { - writer.write("\"@liferay/language/\": \""); + writer.write(StringPool.QUOTE); + writer.write( + LanguageFrontendResourceRequestHandler.LANGUAGE_MODULE_PREFIX); + writer.write("\": \""); String cdnHost = _getCDNHost(httpServletRequest); writer.write(cdnHost); writer.write(_portal.getPathContext(httpServletRequest)); - writer.write("/o/js/language/\""); + writer.write( + LanguageFrontendResourceRequestHandler.LANGUAGE_URI_PREFIX); + writer.write(StringPool.QUOTE); _hashedFilesRegistry.forEach( (unhashedFileURI, hashedFileURI) -> { diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java new file mode 100644 index 00000000000000..1c8ee3593ffdaf --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/LanguageFrontendResource.java @@ -0,0 +1,144 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource; + +import com.liferay.petra.string.StringPool; +import com.liferay.portal.kernel.json.JSONArray; +import com.liferay.portal.kernel.json.JSONException; +import com.liferay.portal.kernel.json.JSONFactory; +import com.liferay.portal.kernel.json.JSONObject; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.log.Log; +import com.liferay.portal.kernel.log.LogFactoryUtil; +import com.liferay.portal.kernel.util.ContentTypes; +import com.liferay.portal.kernel.util.LocaleUtil; +import com.liferay.portal.kernel.util.StringUtil; +import com.liferay.portal.kernel.util.URLUtil; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.Locale; + +/** + * @author Iván Zaera Avellón + */ +public class LanguageFrontendResource implements FrontendResource { + + public LanguageFrontendResource( + JSONFactory jsonFactory, Language language, String languageId, + long maxAge, boolean sendNoCache, URL url) { + + _jsonFactory = jsonFactory; + _language = language; + _languageId = languageId; + _maxAge = maxAge; + _sendNoCache = sendNoCache; + _url = url; + } + + @Override + public String getContentType() { + return ContentTypes.TEXT_JAVASCRIPT; + } + + @Override + public String getETag() { + return null; + } + + @Override + public InputStream getInputStream() throws IOException { + JSONArray languageKeysJSONArray = _getLanguageKeysJSONArray(); + + StringBuilder sb = new StringBuilder(); + + Locale locale = LocaleUtil.fromLanguageId(_languageId); + + for (int i = 0; i < languageKeysJSONArray.length(); i++) { + String key = languageKeysJSONArray.getString(i); + + String label = _language.get(locale, key); + + sb.append(StringPool.APOSTROPHE); + sb.append(key.replaceAll("'", "\\\\'")); + sb.append("':'"); + sb.append(label.replaceAll("'", "\\\\'")); + sb.append("',\n"); + } + + String content = StringUtil.replace( + _TPL_JAVA_SCRIPT, new String[] {"[$LABELS$]"}, + new String[] {sb.toString()}); + + return new ByteArrayInputStream( + content.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public long getMaxAge() { + return _maxAge; + } + + @Override + public boolean isImmutable() { + return false; + } + + @Override + public boolean isSendNoCache() { + return _sendNoCache; + } + + private static String _loadTemplate(String name) { + try (InputStream inputStream = + LanguageFrontendResource.class.getResourceAsStream( + "dependencies/" + name)) { + + return StringUtil.read(inputStream); + } + catch (Exception exception) { + _log.error("Unable to read template " + name, exception); + } + + return StringPool.BLANK; + } + + private JSONArray _getLanguageKeysJSONArray() throws IOException { + try { + JSONObject jsonObject = _jsonFactory.createJSONObject( + URLUtil.toString(_url)); + + return jsonObject.getJSONArray("keys"); + } + catch (JSONException jsonException) { + throw new IOException( + "Invalid language JSON file " + _url, jsonException); + } + } + + private static final String _TPL_JAVA_SCRIPT; + + private static final Log _log = LogFactoryUtil.getLog( + LanguageFrontendResource.class); + + static { + _TPL_JAVA_SCRIPT = _loadTemplate("all.js.tpl"); + } + + private final JSONFactory _jsonFactory; + private final Language _language; + private final String _languageId; + private final long _maxAge; + private final boolean _sendNoCache; + private final URL _url; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java new file mode 100644 index 00000000000000..1f0caefca4d9c2 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandler.java @@ -0,0 +1,114 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.frontend.js.web.internal.resource.LanguageFrontendResource; +import com.liferay.petra.string.StringBundler; +import com.liferay.petra.string.StringPool; +import com.liferay.portal.configuration.module.configuration.ConfigurationProvider; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.json.JSONFactory; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.log.Log; +import com.liferay.portal.kernel.log.LogFactoryUtil; +import com.liferay.portal.kernel.module.configuration.ConfigurationException; +import com.liferay.portal.kernel.util.Portal; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; + +import java.net.URL; + +/** + * @author Iván Zaera Avellón + */ +public class LanguageFrontendResourceRequestHandler + implements FrontendResourceRequestHandler { + + public static final String LANGUAGE_MODULE_PREFIX = "@liferay/language/"; + + public static final String LANGUAGE_URI_PREFIX = "/o/js/language/"; + + public LanguageFrontendResourceRequestHandler( + ConfigurationProvider configurationProvider, + HashedFilesRegistry hashedFilesRegistry, JSONFactory jsonFactory, + Language language, Portal portal) { + + _configurationProvider = configurationProvider; + _hashedFilesRegistry = hashedFilesRegistry; + _jsonFactory = jsonFactory; + _language = language; + _portal = portal; + } + + @Override + public boolean canHandleRequest(HttpServletRequest httpServletRequest) { + String requestURI = httpServletRequest.getRequestURI(); + + return requestURI.startsWith(LANGUAGE_URI_PREFIX); + } + + @Override + public FrontendResource handleRequest(HttpServletRequest httpServletRequest) + throws IOException, ServletException { + + String requestURI = httpServletRequest.getRequestURI(); + + requestURI = requestURI.substring(LANGUAGE_URI_PREFIX.length()); + + String[] requestURIParts = requestURI.split(StringPool.SLASH); + + if ((requestURIParts.length != 3) || + !requestURIParts[2].equals("all.js")) { + + return null; + } + + long maxAge = 3600; + boolean sendNoCache = false; + + long companyId = _portal.getCompanyId(httpServletRequest); + + try { + FrontendCachingConfiguration frontendCachingConfiguration = + _configurationProvider.getCompanyConfiguration( + FrontendCachingConfiguration.class, companyId); + + maxAge = frontendCachingConfiguration.labelsModulesMaxAge(); + sendNoCache = + frontendCachingConfiguration.sendNoCacheForLabelsModules(); + } + catch (ConfigurationException configurationException) { + _log.error( + "Unable to get frontend caching configuration: will use " + + "reasonable defaults instead", + configurationException); + } + + URL resourceURL = _hashedFilesRegistry.getResource( + StringBundler.concat( + Portal.PATH_MODULE, StringPool.SLASH, requestURIParts[1], + "/language.json")); + + return new LanguageFrontendResource( + _jsonFactory, _language, requestURIParts[0], maxAge, sendNoCache, + resourceURL); + } + + private static final Log _log = LogFactoryUtil.getLog( + LanguageFrontendResourceRequestHandler.class); + + private final ConfigurationProvider _configurationProvider; + private final HashedFilesRegistry _hashedFilesRegistry; + private final JSONFactory _jsonFactory; + private final Language _language; + private final Portal _portal; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java index 3dde2d34afb7e2..ba02ff8dc7f844 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java @@ -9,11 +9,15 @@ import com.liferay.frontend.js.web.internal.resource.FrontendResource; import com.liferay.frontend.js.web.internal.resource.handler.FrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.HashedFileFrontendResourceRequestHandler; +import com.liferay.frontend.js.web.internal.resource.handler.LanguageFrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.StyleSheetFrontendResourceRequestHandler; import com.liferay.petra.io.StreamUtil; import com.liferay.petra.string.StringPool; import com.liferay.portal.configuration.metatype.bnd.util.ConfigurableUtil; +import com.liferay.portal.configuration.module.configuration.ConfigurationProvider; import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.json.JSONFactory; +import com.liferay.portal.kernel.language.Language; import com.liferay.portal.kernel.service.ThemeLocalService; import com.liferay.portal.kernel.servlet.HttpHeaders; import com.liferay.portal.kernel.util.ContentTypes; @@ -101,6 +105,11 @@ protected void activate(Map properties) { frontendCachingConfiguration, _hashedFilesRegistry, _portal, _themeLocalService)); + frontendResourceRequestHandlers.add( + new LanguageFrontendResourceRequestHandler( + _configurationProvider, _hashedFilesRegistry, _jsonFactory, + _language, _portal)); + _frontendResourceRequestHandlers.set(frontendResourceRequestHandlers); } @@ -199,6 +208,9 @@ protected void send( } } + @Reference + private ConfigurationProvider _configurationProvider; + private final AtomicReference> _frontendResourceRequestHandlers = new AtomicReference<>( Collections.emptyList()); @@ -206,6 +218,12 @@ protected void send( @Reference private HashedFilesRegistry _hashedFilesRegistry; + @Reference + private JSONFactory _jsonFactory; + + @Reference + private Language _language; + @Reference private Portal _portal; diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/resource/dependencies/all.js.tpl b/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/resource/dependencies/all.js.tpl new file mode 100644 index 00000000000000..da968c126f8a80 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/resources/com/liferay/frontend/js/web/internal/resource/dependencies/all.js.tpl @@ -0,0 +1,11 @@ +const labels = { +[$LABELS$]}; + +if (Liferay && Liferay.Language && Liferay.Language._cache) { + Liferay.Language._cache = { + ...Liferay.Language._cache, + ...labels + }; +} + +export default labels; \ No newline at end of file From b22221711599888aaf4307f993e9c13623e61d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Wed, 11 Jun 2025 15:12:02 +0200 Subject: [PATCH 05/12] LPD-52709 Add tests --- ...ageFrontendResourceRequestHandlerTest.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java diff --git a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java new file mode 100644 index 00000000000000..f91f6653940caf --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/LanguageFrontendResourceRequestHandlerTest.java @@ -0,0 +1,220 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.petra.io.StreamUtil; +import com.liferay.portal.configuration.module.configuration.ConfigurationProvider; +import com.liferay.portal.json.JSONFactoryImpl; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.language.LanguageUtil; +import com.liferay.portal.kernel.util.ContentTypes; +import com.liferay.portal.kernel.util.LocaleUtil; +import com.liferay.portal.kernel.util.Portal; +import com.liferay.portal.test.rule.LiferayUnitTestRule; + +import jakarta.servlet.http.HttpServletRequest; + +import java.io.ByteArrayInputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.Locale; + +import org.junit.Assert; +import org.junit.Test; + +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Iván Zaera Avellón + */ +public class LanguageFrontendResourceRequestHandlerTest { + + public static final LiferayUnitTestRule liferayUnitTestRule = + LiferayUnitTestRule.INSTANCE; + + @Test + public void testCanHandleRequest() throws Exception { + LanguageFrontendResourceRequestHandler + languageFrontendResourceRequestHandler = + new LanguageFrontendResourceRequestHandler( + _mockConfigurationProvider(1234L, false), + _mockHashedFilesRegistry(), new JSONFactoryImpl(), + _mockLanguage(), _mockPortal()); + + Assert.assertTrue( + languageFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest( + "/o/js/language/en_US/frontend-js-web/all.js"))); + + Assert.assertFalse( + languageFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest("/nonsense/request/index.js"))); + } + + @Test + public void testConfiguration() throws Exception { + LanguageFrontendResourceRequestHandler + languageFrontendResourceRequestHandler = + new LanguageFrontendResourceRequestHandler( + _mockConfigurationProvider(4321L, true), + _mockHashedFilesRegistry(), new JSONFactoryImpl(), + _mockLanguage(), _mockPortal()); + + FrontendResource frontendResource = + languageFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/js/language/en_US/frontend-js-web/all.js")); + + Assert.assertNotNull(frontendResource); + + Assert.assertEquals(4321L, frontendResource.getMaxAge()); + + Assert.assertTrue(frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequest() throws Exception { + LanguageFrontendResourceRequestHandler + languageFrontendResourceRequestHandler = + new LanguageFrontendResourceRequestHandler( + _mockConfigurationProvider(1234L, false), + _mockHashedFilesRegistry(), new JSONFactoryImpl(), + _mockLanguage(), _mockPortal()); + + FrontendResource frontendResource = + languageFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/js/language/en_US/frontend-js-web/all.js")); + + Assert.assertNotNull(frontendResource); + + Assert.assertEquals( + ContentTypes.TEXT_JAVASCRIPT, frontendResource.getContentType()); + + Assert.assertNull(frontendResource.getETag()); + + Assert.assertEquals(1234L, frontendResource.getMaxAge()); + + Assert.assertFalse(frontendResource.isImmutable()); + + Assert.assertFalse(frontendResource.isSendNoCache()); + + String content = StreamUtil.toString(frontendResource.getInputStream()); + + Assert.assertTrue(content.contains("'a-key':'a-key',")); + } + + private ConfigurationProvider _mockConfigurationProvider( + long maxAge, boolean sendNoCache) + throws Exception { + + ConfigurationProvider configurationProvider = Mockito.mock( + ConfigurationProvider.class); + + FrontendCachingConfiguration frontendCachingConfiguration = + Mockito.mock(FrontendCachingConfiguration.class); + + Mockito.when( + frontendCachingConfiguration.labelsModulesMaxAge() + ).thenReturn( + maxAge + ); + + Mockito.when( + frontendCachingConfiguration.sendNoCacheForLabelsModules() + ).thenReturn( + sendNoCache + ); + + Mockito.when( + configurationProvider.getCompanyConfiguration( + FrontendCachingConfiguration.class, _COMPANY_ID) + ).thenReturn( + frontendCachingConfiguration + ); + + return configurationProvider; + } + + private HashedFilesRegistry _mockHashedFilesRegistry() throws Exception { + HashedFilesRegistry hashedFilesRegistry = Mockito.mock( + HashedFilesRegistry.class); + + URL url = Mockito.mock(URL.class); + + Mockito.when( + url.openStream() + ).thenReturn( + new ByteArrayInputStream( + "{keys:['a-key']}".getBytes(StandardCharsets.UTF_8)) + ); + + Mockito.when( + hashedFilesRegistry.getResource(Mockito.anyString()) + ).thenReturn( + url + ); + + return hashedFilesRegistry; + } + + private HttpServletRequest _mockHttpServletRequest(String requestURI) { + MockHttpServletRequest mockHttpServletRequest = + new MockHttpServletRequest(); + + mockHttpServletRequest.setRequestURI(requestURI); + + return mockHttpServletRequest; + } + + private Language _mockLanguage() { + Language language = Mockito.mock(Language.class); + + Mockito.when( + language.isAvailableLocale(LocaleUtil.ENGLISH) + ).thenReturn( + true + ); + + Mockito.when( + language.get(Mockito.any(Locale.class), Mockito.anyString()) + ).thenAnswer( + (Answer)invocationOnMock -> invocationOnMock.getArgument( + 1, String.class) + ); + + new LanguageUtil( + ).setLanguage( + language + ); + + return language; + } + + private Portal _mockPortal() { + Portal portal = Mockito.mock(Portal.class); + + Mockito.when( + portal.getCompanyId(Mockito.any(HttpServletRequest.class)) + ).thenReturn( + _COMPANY_ID + ); + + return portal; + } + + private static final long _COMPANY_ID = 1L; + +} \ No newline at end of file From a982d76473c4cb769eb52fc511def09d0348fdd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Thu, 9 Oct 2025 10:07:19 +0200 Subject: [PATCH 06/12] LPD-52709 Hashify loader.js so that it can be infinitely cached --- .../build.gradle | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle b/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle index 22881a3df2dc10..7120d0c25a4f9b 100644 --- a/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle +++ b/modules/apps/frontend-js/frontend-js-loader-modules-extender/build.gradle @@ -1,4 +1,6 @@ -import com.liferay.gradle.util.copy.StripPathSegmentsAction +import com.liferay.gradle.util.hash.HashUtil + +import org.gradle.api.file.RelativePath task buildLiferayAMDLoader(type: Copy) @@ -6,7 +8,6 @@ File jsDestinationDir = file("tmp/META-INF/resources") buildLiferayAMDLoader { dependsOn npmInstall - eachFile new StripPathSegmentsAction(4) from npmInstall.nodeModulesDir include "@liferay/amd-loader/build/loader/loader.js.map" @@ -14,6 +15,32 @@ buildLiferayAMDLoader { includeEmptyDirs = false into jsDestinationDir + + eachFile { + details -> + + def sourcePath = details.sourcePath + + if (sourcePath.endsWith(".map")) { + sourcePath = sourcePath[0..-5] + } + + def md5 = HashUtil.md5( + new File("${npmInstall.nodeModulesDir}/${sourcePath}")) + + def hash = md5.asCompactString()[0..11] + + def segments = details.relativePath.segments + + def fileName = segments[-1] + + def i = fileName.indexOf(".") + + segments[-1] = "${fileName[0..i]}(${hash})${fileName[i..-1]}" + + details.relativePath = new RelativePath( + true, segments[4..-1].toArray(new String[]{})) + } } classes { From 64e503d2d2b04c7938417e5e8ca561fec91a41f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 9 Dec 2025 12:03:51 +0100 Subject: [PATCH 07/12] LPD-52709 Implement translation of portlet JS files to avoid breaking changes Since portlet JS files are not intended to be ESM format and outside of that context we cannot use the new translation architecture based on all.js files, I'm putting back again portlet JS files translation. It essentially works the same as tokenized CSS files. Note that translated JS files will not be infinitely cacheable but time-based, for obvious reasons. --- .../resource/JavaScriptFrontendResource.java | 99 ++++++++ ...aScriptFrontendResourceRequestHandler.java | 218 ++++++++++++++++++ .../filter/FrontendResourceFilter.java | 11 +- .../portlet/render/PortletRenderUtil.java | 82 ++++++- 4 files changed, 396 insertions(+), 14 deletions(-) create mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java create mode 100644 modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java new file mode 100644 index 00000000000000..4646a02142d086 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/JavaScriptFrontendResource.java @@ -0,0 +1,99 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource; + +import com.liferay.petra.io.StreamUtil; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.util.ContentTypes; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.ResourceBundle; + +/** + * @author Iván Zaera Avellón + */ +public class JavaScriptFrontendResource implements FrontendResource { + + public JavaScriptFrontendResource( + String eTag, boolean immutable, Language language, long maxAge, + ResourceBundle resourceBundle, boolean sendNoCache, URL url) { + + if (resourceBundle != null) { + if (eTag != null) { + throw new IllegalArgumentException( + "Translated resources cannot have an eTag"); + } + + if (immutable) { + throw new IllegalArgumentException( + "Translated resources cannot be immutable"); + } + } + + _eTag = eTag; + _immutable = immutable; + _language = language; + _maxAge = maxAge; + _resourceBundle = resourceBundle; + _sendNoCache = sendNoCache; + _url = url; + } + + @Override + public String getContentType() { + return ContentTypes.APPLICATION_JAVASCRIPT; + } + + @Override + public String getETag() { + return _eTag; + } + + @Override + public InputStream getInputStream() throws IOException { + if (_resourceBundle == null) { + return _url.openStream(); + } + + String content = _language.process( + () -> _resourceBundle, _resourceBundle.getLocale(), + StreamUtil.toString(_url.openStream())); + + return new ByteArrayInputStream( + content.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public long getMaxAge() { + return _maxAge; + } + + @Override + public boolean isImmutable() { + return _immutable; + } + + @Override + public boolean isSendNoCache() { + return _sendNoCache; + } + + private final String _eTag; + private final boolean _immutable; + private final Language _language; + private final long _maxAge; + private final ResourceBundle _resourceBundle; + private final boolean _sendNoCache; + private final URL _url; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java new file mode 100644 index 00000000000000..c079627b1ff30c --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandler.java @@ -0,0 +1,218 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.frontend.js.web.internal.resource.JavaScriptFrontendResource; +import com.liferay.petra.string.CharPool; +import com.liferay.petra.string.StringBundler; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesUtil; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.log.Log; +import com.liferay.portal.kernel.log.LogFactoryUtil; +import com.liferay.portal.kernel.portlet.PortletConfigFactory; +import com.liferay.portal.kernel.util.AggregateResourceBundle; +import com.liferay.portal.kernel.util.LocaleUtil; +import com.liferay.portal.language.LanguageResources; + +import jakarta.portlet.PortletConfig; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; + +import java.net.URL; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * @author Iván Zaera Avellón + */ +public class JavaScriptFrontendResourceRequestHandler + implements FrontendResourceRequestHandler { + + public JavaScriptFrontendResourceRequestHandler( + FrontendCachingConfiguration frontendCachingConfiguration, + HashedFilesRegistry hashedFilesRegistry, Language language, + PortletConfigFactory portletConfigFactory) { + + _frontendCachingConfiguration = frontendCachingConfiguration; + _hashedFilesRegistry = hashedFilesRegistry; + _language = language; + _portletConfigFactory = portletConfigFactory; + } + + @Override + public boolean canHandleRequest(HttpServletRequest httpServletRequest) { + String requestURI = httpServletRequest.getRequestURI(); + + if (!requestURI.endsWith(".js")) { + return false; + } + + if (HashedFilesUtil.containsHash(requestURI)) { + return true; + } + + String hashedFileURI = _hashedFilesRegistry.getHashedFileURI( + requestURI); + + if (hashedFileURI != null) { + return true; + } + + URL resourceURL = _hashedFilesRegistry.getResource(requestURI); + + if (resourceURL != null) { + return true; + } + + return false; + } + + @Override + public FrontendResource handleRequest(HttpServletRequest httpServletRequest) + throws IOException, ServletException { + + String requestURI = httpServletRequest.getRequestURI(); + + String requestHash = HashedFilesUtil.getHash(requestURI); + + ResourceBundle resourceBundle = _getResourceBundle(httpServletRequest); + + if (requestHash != null) { + if (_log.isDebugEnabled()) { + _log.debug("Handling request " + requestURI); + } + + if (resourceBundle == null) { + return _createFrontendResource( + requestHash, true, null, requestURI); + } + + return _createFrontendResource( + null, false, resourceBundle, requestURI); + } + + String hashedFileURI = _hashedFilesRegistry.getHashedFileURI( + requestURI); + + if (hashedFileURI == null) { + if (_log.isDebugEnabled()) { + _log.debug("Handling request " + requestURI); + } + + return _createFrontendResource( + null, false, resourceBundle, requestURI); + } + + if (_log.isDebugEnabled()) { + _log.debug( + StringBundler.concat( + "Handling request ", requestURI, " with static file ", + hashedFileURI)); + } + + return _createFrontendResource( + (resourceBundle == null) ? HashedFilesUtil.getHash(hashedFileURI) : + null, + false, resourceBundle, hashedFileURI); + } + + private FrontendResource _createFrontendResource( + String eTag, boolean immutable, ResourceBundle resourceBundle, + String resourceURI) { + + long maxAge = 31536000; + boolean sendNoCache = false; + + if (!immutable) { + maxAge = _frontendCachingConfiguration.esModulesMaxAge(); + sendNoCache = + _frontendCachingConfiguration.sendNoCacheForESModules(); + } + + URL resourceURL = _hashedFilesRegistry.getResource(resourceURI); + + if (resourceURL == null) { + return null; + } + + return new JavaScriptFrontendResource( + eTag, immutable, _language, maxAge, resourceBundle, sendNoCache, + resourceURL); + } + + private ResourceBundle _getResourceBundle( + HttpServletRequest httpServletRequest) { + + if (Boolean.valueOf(httpServletRequest.getParameter("translate"))) { + Locale locale = LocaleUtil.fromLanguageId( + _language.getLanguageId(httpServletRequest)); + + ResourceBundle resourceBundle = LanguageResources.getResourceBundle( + locale); + + PortletConfig portletConfig = null; + + Enumeration enumeration = + httpServletRequest.getParameterNames(); + + while (enumeration.hasMoreElements()) { + String parameterName = enumeration.nextElement(); + + int index = parameterName.indexOf(CharPool.COLON); + + if (index > 0) { + portletConfig = _portletConfigFactory.get( + parameterName.substring(0, index)); + } + } + + if (portletConfig != null) { + resourceBundle = new AggregateResourceBundle( + portletConfig.getResourceBundle(locale), resourceBundle); + } + + final ResourceBundle finalResourceBundle = resourceBundle; + + return new ResourceBundle() { + + @Override + public Enumeration getKeys() { + return finalResourceBundle.getKeys(); + } + + @Override + public Locale getLocale() { + return locale; + } + + @Override + protected Object handleGetObject(String s) { + return finalResourceBundle.getObject(s); + } + + }; + } + + return null; + } + + private static final Log _log = LogFactoryUtil.getLog( + JavaScriptFrontendResourceRequestHandler.class); + + private final FrontendCachingConfiguration _frontendCachingConfiguration; + private final HashedFilesRegistry _hashedFilesRegistry; + private final Language _language; + private final PortletConfigFactory _portletConfigFactory; + +} \ No newline at end of file diff --git a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java index ba02ff8dc7f844..6808c878700e5d 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java +++ b/modules/apps/frontend-js/frontend-js-web/src/main/java/com/liferay/frontend/js/web/internal/servlet/filter/FrontendResourceFilter.java @@ -9,6 +9,7 @@ import com.liferay.frontend.js.web.internal.resource.FrontendResource; import com.liferay.frontend.js.web.internal.resource.handler.FrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.HashedFileFrontendResourceRequestHandler; +import com.liferay.frontend.js.web.internal.resource.handler.JavaScriptFrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.LanguageFrontendResourceRequestHandler; import com.liferay.frontend.js.web.internal.resource.handler.StyleSheetFrontendResourceRequestHandler; import com.liferay.petra.io.StreamUtil; @@ -18,6 +19,7 @@ import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; import com.liferay.portal.kernel.json.JSONFactory; import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.portlet.PortletConfigFactoryUtil; import com.liferay.portal.kernel.service.ThemeLocalService; import com.liferay.portal.kernel.servlet.HttpHeaders; import com.liferay.portal.kernel.util.ContentTypes; @@ -90,16 +92,15 @@ protected void activate(Map properties) { ContentTypes.APPLICATION_JSON, ".map", _hashedFilesRegistry, 86400, "esModulesMaxAge", _portal, false, "sendNoCacheForESModules")); - frontendResourceRequestHandlers.add( - new HashedFileFrontendResourceRequestHandler( - ContentTypes.TEXT_JAVASCRIPT, ".js", _hashedFilesRegistry, - 86400, "esModulesMaxAge", _portal, false, - "sendNoCacheForESModules")); FrontendCachingConfiguration frontendCachingConfiguration = ConfigurableUtil.createConfigurable( FrontendCachingConfiguration.class, properties); + frontendResourceRequestHandlers.add( + new JavaScriptFrontendResourceRequestHandler( + frontendCachingConfiguration, _hashedFilesRegistry, _language, + PortletConfigFactoryUtil.getPortletConfigFactory())); frontendResourceRequestHandlers.add( new StyleSheetFrontendResourceRequestHandler( frontendCachingConfiguration, _hashedFilesRegistry, _portal, diff --git a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java index 0c63a04c8e3b92..8c82791d99107e 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java +++ b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java @@ -268,6 +268,7 @@ private static String _getStaticCSSResourceURL( } } + if (_isTokenized(unhashedFileURI)) { url = HttpComponentsUtil.addParameter( url, "themeId", themeDisplay.getThemeId()); @@ -277,6 +278,45 @@ private static String _getStaticCSSResourceURL( return url; } + private static String _getStaticJSResourceURL( + String originalURL, ThemeDisplay themeDisplay) { + + String queryString = HttpComponentsUtil.getQueryString(originalURL); + + if (!Validator.isBlank(queryString)) { + originalURL = originalURL.substring( + 0, originalURL.length() - queryString.length() - 1); + } + + String url; + + String proxyPath = PortalUtil.getPathProxy(); + + String unhashedFileURI = originalURL.substring(proxyPath.length()); + + String hashedFileURI = HashedFilesRegistryUtil.getHashedFileURI( + unhashedFileURI); + + if (hashedFileURI == null) { + url = originalURL; + } + else { + url = proxyPath + hashedFileURI; + } + + if (!Validator.isBlank(queryString)) { + url += "?" + queryString; + } + + if (_isTranslatable(unhashedFileURI)) { + url = HttpComponentsUtil.addParameter( + url, "languageId", themeDisplay.getLanguageId()); + url = HttpComponentsUtil.addParameter(url, "translate", true); + } + + return url; + } + private static List _getStaticURLs( HttpServletRequest httpServletRequest, Collection portletResourceAccessors, @@ -327,15 +367,8 @@ private static List _getStaticURLs( contextPath + portletResource, themeDisplay); } else if (urlType == URLType.JAVASCRIPT) { - portletResource = contextPath + portletResource; - - String hashedFileURI = - HashedFilesRegistryUtil.getHashedFileURI( - portletResource); - - if (hashedFileURI != null) { - portletResource = hashedFileURI; - } + portletResource = _getStaticJSResourceURL( + contextPath + portletResource, themeDisplay); } else { throw new UnsupportedOperationException( @@ -437,6 +470,35 @@ private static boolean _isTokenized(String resourceURI) { return _tokenized.get(resourceURI); } + private static boolean _isTranslatable(String resourceURI) { + if (!_translatable.containsKey(resourceURI)) { + URL resourceURL = HashedFilesRegistryUtil.getResource(resourceURI); + + String content; + + try { + content = StreamUtil.toString(resourceURL.openStream()); + } + catch (Exception exception) { + if (_log.isDebugEnabled()) { + _log.debug( + "Assuming " + resourceURI + + " is not translatable because it could not be read", + exception); + } + + _translatable.putIfAbsent(resourceURI, false); + + return false; + } + + _translatable.putIfAbsent( + resourceURI, content.contains("Liferay.Language.get")); + } + + return _translatable.get(resourceURI); + } + private static void _writeCSSPath( PrintWriter printWriter, String cssPath, Map attributes) { @@ -603,6 +665,8 @@ public Collection get(Portlet portlet) { "module:", "nocombo:"); private static final Map _tokenized = new ConcurrentHashMap<>(); + private static final Map _translatable = + new ConcurrentHashMap<>(); private enum URLType { From 0e55ea43d0883708f41d0a30caa9dbbeea472bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 9 Dec 2025 12:05:09 +0100 Subject: [PATCH 08/12] LPD-52709 Support URL parameters in portlet CSS files This need was discovered (for JS files) while implementing the previous commit so I'm adding it for CSS files too. --- .../kernel/portlet/render/PortletRenderUtil.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java index 8c82791d99107e..64583ef2eddf40 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java +++ b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java @@ -240,7 +240,14 @@ private static String _getStaticCSSResourceURL( HttpServletRequest httpServletRequest, String originalURL, ThemeDisplay themeDisplay) { - String url = null; + String queryString = HttpComponentsUtil.getQueryString(originalURL); + + if (!Validator.isBlank(queryString)) { + originalURL = originalURL.substring( + 0, originalURL.length() - queryString.length() - 1); + } + + String url; String proxyPath = PortalUtil.getPathProxy(); @@ -268,6 +275,9 @@ private static String _getStaticCSSResourceURL( } } + if (!Validator.isBlank(queryString)) { + url += "?" + queryString; + } if (_isTokenized(unhashedFileURI)) { url = HttpComponentsUtil.addParameter( From 6694c0a5d82d0631224868be930a38fed17c18f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 9 Dec 2025 12:09:45 +0100 Subject: [PATCH 09/12] LPD-52709 We don't need "nocombo:" special protocol for portlet resources any more We never published anything related to supporting it, so it can be considered an internal. --- .../portlet/CustomElementCETPortlet.java | 4 ---- .../portlet/render/PortletRenderUtil.java | 22 +++---------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java b/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java index 1a0e05d8b90775..37267cecc1f23a 100644 --- a/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java +++ b/modules/apps/client-extension/client-extension-web/src/main/java/com/liferay/client/extension/web/internal/portlet/CustomElementCETPortlet.java @@ -166,10 +166,6 @@ private String[] _prepareURLs(long lastModified, String[] urls) { if (urls[i].contains(contextPath + "/o/")) { urls[i] = urls[i].replace(contextPath + "/o/", "/o/"); } - - if (!urls[i].startsWith("module:")) { - urls[i] = "nocombo:" + urls[i]; - } } return urls; diff --git a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java index 64583ef2eddf40..ae97c0a3944c0e 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java +++ b/portal-kernel/src/com/liferay/portal/kernel/portlet/render/PortletRenderUtil.java @@ -21,7 +21,6 @@ import com.liferay.portal.kernel.util.HttpComponentsUtil; import com.liferay.portal.kernel.util.ListUtil; import com.liferay.portal.kernel.util.PortalUtil; -import com.liferay.portal.kernel.util.SetUtil; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.kernel.util.WebKeys; @@ -359,15 +358,10 @@ private static List _getStaticURLs( for (String portletResource : portletResources) { String prefix = null; - for (String specialProtocol : _specialPrefixes) { - if (portletResource.startsWith(specialProtocol)) { - portletResource = portletResource.substring( - specialProtocol.length()); + if (portletResource.startsWith("module:")) { + portletResource = portletResource.substring(7); - prefix = specialProtocol; - - break; - } + prefix = "module:"; } if (!HttpComponentsUtil.hasProtocol(portletResource)) { @@ -437,14 +431,6 @@ private static Collection _getURLs( "Unsupported URL type " + urlType); } - for (int i = 0; i < urls.size(); i++) { - String url = urls.get(i); - - if (url.startsWith("nocombo:")) { - urls.set(i, url.substring(8)); - } - } - return urls; } @@ -671,8 +657,6 @@ public Collection get(Portlet portlet) { private static final Log _log = LogFactoryUtil.getLog( PortletRenderUtil.class); - private static final Set _specialPrefixes = SetUtil.fromArray( - "module:", "nocombo:"); private static final Map _tokenized = new ConcurrentHashMap<>(); private static final Map _translatable = From 8f7161b8b3a76c8a10b75448f446d09f4b0d807d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 9 Dec 2025 16:08:47 +0100 Subject: [PATCH 10/12] LPD-52709 Add/expand tests --- ...iptFrontendResourceRequestHandlerTest.java | 323 ++++++++++++++++++ .../portlet/render/PortletRenderUtilTest.java | 153 ++++----- 2 files changed, 389 insertions(+), 87 deletions(-) create mode 100644 modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java diff --git a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java new file mode 100644 index 00000000000000..25dbb619bd19c9 --- /dev/null +++ b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/JavaScriptFrontendResourceRequestHandlerTest.java @@ -0,0 +1,323 @@ +/** + * SPDX-FileCopyrightText: (c) 2025 Liferay, Inc. https://liferay.com + * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06 + */ + +package com.liferay.frontend.js.web.internal.resource.handler; + +import com.liferay.frontend.js.web.internal.configuration.FrontendCachingConfiguration; +import com.liferay.frontend.js.web.internal.resource.FrontendResource; +import com.liferay.frontend.js.web.test.util.FrontendJSWebTestUtil; +import com.liferay.petra.io.StreamUtil; +import com.liferay.portal.kernel.frontend.hashed.files.HashedFilesRegistry; +import com.liferay.portal.kernel.language.Language; +import com.liferay.portal.kernel.portlet.PortletConfigFactory; +import com.liferay.portal.kernel.test.util.RandomTestUtil; +import com.liferay.portal.kernel.util.ContentTypes; +import com.liferay.portal.kernel.util.StringUtil; +import com.liferay.portal.language.LanguageResources; +import com.liferay.portal.test.rule.LiferayUnitTestRule; + +import jakarta.servlet.http.HttpServletRequest; + +import java.io.ByteArrayInputStream; + +import java.net.URL; + +import java.nio.charset.StandardCharsets; + +import java.util.ResourceBundle; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * @author Iván Zaera Avellón + */ +public class JavaScriptFrontendResourceRequestHandlerTest { + + public static final LiferayUnitTestRule liferayUnitTestRule = + LiferayUnitTestRule.INSTANCE; + + @Before + public void setUp() { + _hashedFilePath = StringUtil.replace( + _UNHASHED_FILE_PATH, ".js", ".(" + _HASH + ").js"); + } + + @After + public void tearDown() { + if (_languageResourcesMockedStatic != null) { + _languageResourcesMockedStatic.close(); + + _languageResourcesMockedStatic = null; + } + } + + @Test + public void testCanHandleRequest() throws Exception { + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(86400, false), + _mockHashedFilesRegistry(true, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + Assert.assertFalse( + javaScriptFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest("/nonsense/request/main.js", false))); + Assert.assertTrue( + javaScriptFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _UNHASHED_FILE_PATH, false))); + Assert.assertTrue( + javaScriptFrontendResourceRequestHandler.canHandleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _hashedFilePath, false))); + } + + @Test + public void testHandleRequestForTranslatableFile() throws Exception { + _mockLanguageResources(); + + long maxAge = RandomTestUtil.randomLong(); + boolean sendNoCache = true; + + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(maxAge, sendNoCache), + _mockHashedFilesRegistry(true, true), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _hashedFilePath, true)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertNull(frontendResource.getETag()); + Assert.assertEquals( + StringUtil.replace( + _TRANSLATABLE_JS_CONTENT, "Liferay.Language.get('portlet')", + "'Portlet'"), + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(maxAge, frontendResource.getMaxAge()); + Assert.assertFalse(frontendResource.isImmutable()); + Assert.assertEquals(sendNoCache, frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequestWithHash() throws Exception { + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(86400, true), + _mockHashedFilesRegistry(true, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _hashedFilePath, false)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertEquals(_HASH, frontendResource.getETag()); + Assert.assertEquals( + _JS_CONTENT, + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(31536000L, frontendResource.getMaxAge()); + Assert.assertTrue(frontendResource.isImmutable()); + Assert.assertFalse(frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequestWithoutHashForHashedFile() throws Exception { + long maxAge = RandomTestUtil.randomLong(); + boolean sendNoCache = true; + + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(maxAge, sendNoCache), + _mockHashedFilesRegistry(true, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _UNHASHED_FILE_PATH, false)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertEquals(_HASH, frontendResource.getETag()); + Assert.assertEquals( + _JS_CONTENT, + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(maxAge, frontendResource.getMaxAge()); + Assert.assertFalse(frontendResource.isImmutable()); + Assert.assertEquals(sendNoCache, frontendResource.isSendNoCache()); + } + + @Test + public void testHandleRequestWithoutHashForUnhashedFile() throws Exception { + long maxAge = RandomTestUtil.randomLong(); + boolean sendNoCache = true; + + JavaScriptFrontendResourceRequestHandler + javaScriptFrontendResourceRequestHandler = + new JavaScriptFrontendResourceRequestHandler( + _mockFrontendCachingConfiguration(maxAge, sendNoCache), + _mockHashedFilesRegistry(false, false), _mockLanguage(), + Mockito.mock(PortletConfigFactory.class)); + + FrontendResource frontendResource = + javaScriptFrontendResourceRequestHandler.handleRequest( + _mockHttpServletRequest( + "/o/frontend-js-web" + _UNHASHED_FILE_PATH, false)); + + Assert.assertEquals( + ContentTypes.APPLICATION_JAVASCRIPT, + frontendResource.getContentType()); + Assert.assertNull(frontendResource.getETag()); + Assert.assertEquals( + _JS_CONTENT, + StreamUtil.toString(frontendResource.getInputStream())); + Assert.assertEquals(maxAge, frontendResource.getMaxAge()); + Assert.assertFalse(frontendResource.isImmutable()); + Assert.assertEquals(sendNoCache, frontendResource.isSendNoCache()); + } + + private FrontendCachingConfiguration _mockFrontendCachingConfiguration( + long esModulesMaxAge, boolean sendNoCacheForESModules) { + + FrontendCachingConfiguration frontendCachingConfiguration = + Mockito.mock(FrontendCachingConfiguration.class); + + Mockito.when( + frontendCachingConfiguration.esModulesMaxAge() + ).thenReturn( + esModulesMaxAge + ); + + Mockito.when( + frontendCachingConfiguration.sendNoCacheForESModules() + ).thenReturn( + sendNoCacheForESModules + ); + + return frontendCachingConfiguration; + } + + private HashedFilesRegistry _mockHashedFilesRegistry( + boolean hashedFile, boolean translatableFile) + throws Exception { + + HashedFilesRegistry hashedFilesRegistry = Mockito.mock( + HashedFilesRegistry.class); + + if (hashedFile) { + Mockito.when( + hashedFilesRegistry.getHashedFileURI( + Mockito.eq("/o/frontend-js-web" + _UNHASHED_FILE_PATH)) + ).thenReturn( + "/o/frontend-js-web" + _hashedFilePath + ); + } + + URL url = Mockito.mock(URL.class); + + String content = + translatableFile ? _TRANSLATABLE_JS_CONTENT : _JS_CONTENT; + + Mockito.when( + url.openStream() + ).thenReturn( + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)) + ); + + Mockito.when( + hashedFilesRegistry.getResource( + Mockito.eq( + "/o/frontend-js-web" + + (hashedFile ? _hashedFilePath : _UNHASHED_FILE_PATH))) + ).thenReturn( + url + ); + + return hashedFilesRegistry; + } + + private HttpServletRequest _mockHttpServletRequest( + String requestURI, boolean translatableRequest) { + + MockHttpServletRequest mockHttpServletRequest = + new MockHttpServletRequest(); + + mockHttpServletRequest.setRequestURI(requestURI); + + if (translatableRequest) { + mockHttpServletRequest.addParameter("languageId", "en_US"); + mockHttpServletRequest.addParameter("translate", "true"); + } + + return mockHttpServletRequest; + } + + private Language _mockLanguage() { + Language language = Mockito.mock(Language.class); + + Mockito.when( + language.process(Mockito.any(), Mockito.any(), Mockito.any()) + ).thenAnswer( + (Answer)invocationOnMock -> { + String content = invocationOnMock.getArgument(2); + + return StringUtil.replace( + content, "Liferay.Language.get('portlet')", "'Portlet'"); + } + ); + + return language; + } + + private void _mockLanguageResources() { + _languageResourcesMockedStatic = Mockito.mockStatic( + LanguageResources.class); + + ResourceBundle resourceBundle = Mockito.mock(ResourceBundle.class); + + _languageResourcesMockedStatic.when( + () -> LanguageResources.getResourceBundle(Mockito.any()) + ).thenReturn( + resourceBundle + ); + } + + private static final String _HASH = + FrontendJSWebTestUtil.randomHashedFileHash(); + + private static final String _JS_CONTENT = "function x(){return 'Portlet';}"; + + private static final String _TRANSLATABLE_JS_CONTENT = + "function x(){return Liferay.Language.get('portlet');}"; + + private static final String _UNHASHED_FILE_PATH = "/js/main.js"; + + private String _hashedFilePath; + private MockedStatic _languageResourcesMockedStatic; + +} \ No newline at end of file diff --git a/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java b/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java index 2d51d75392a81e..710846008c2d79 100644 --- a/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java +++ b/portal-kernel/test/unit/com/liferay/portal/kernel/portlet/render/PortletRenderUtilTest.java @@ -34,7 +34,6 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.mock.web.MockHttpServletRequest; @@ -68,20 +67,20 @@ public void testGetPortletRenderParts() throws Exception { "/header-portal.(" + _HASH + ").css", "/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/nocombo-header-portal.(" + _HASH + ").css", "/o/portlet-web/header-portlet.(" + _HASH + ").css", "/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/o/portlet-web/nocombo-header-portlet.(" + _HASH + ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/header-portal.(" + _HASH + ").js", - "/nocombo-header-portal.(" + _HASH + ").js", + "/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/o/portlet-web/nocombo-header-portlet.(" + _HASH + ").js", + "/o/portlet-web/header-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/module-header-portal.(" + _HASH + ").js", @@ -93,9 +92,11 @@ public void testGetPortletRenderParts() throws Exception { _assertEquals( Arrays.asList( "/footer-portal.(" + _HASH + ").js", - "/nocombo-footer-portal.(" + _HASH + ").js", + "/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/o/portlet-web/nocombo-footer-portlet.(" + _HASH + ").js", + "/o/portlet-web/footer-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/module-footer-portal.(" + _HASH + ").js", @@ -109,11 +110,9 @@ public void testGetPortletRenderParts() throws Exception { "/footer-portal.(" + _HASH + ").css", "/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/nocombo-footer-portal.(" + _HASH + ").css", "/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/o/portlet-web/nocombo-footer-portlet.(" + _HASH + ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); @@ -137,22 +136,20 @@ public void testGetPortletRenderPartsWithContext() throws Exception { "/portal/header-portal.(" + _HASH + ").css", "/portal/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/nocombo-header-portal.(" + _HASH + ").css", "/portal/o/portlet-web/header-portlet.(" + _HASH + ").css", "/portal/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/portal/header-portal.(" + _HASH + ").js", - "/portal/nocombo-header-portal.(" + _HASH + ").js", + "/portal/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/portal/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").js", + "/portal/o/portlet-web/header-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/portal/module-header-portal.(" + _HASH + ").js", @@ -166,22 +163,20 @@ public void testGetPortletRenderPartsWithContext() throws Exception { "/portal/footer-portal.(" + _HASH + ").css", "/portal/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/nocombo-footer-portal.(" + _HASH + ").css", "/portal/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/portal/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); _assertEquals( Arrays.asList( "/portal/footer-portal.(" + _HASH + ").js", - "/portal/nocombo-footer-portal.(" + _HASH + ").js", + "/portal/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/portal/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").js", + "/portal/o/portlet-web/footer-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/portal/module-footer-portal.(" + _HASH + ").js", @@ -212,24 +207,22 @@ public void testGetPortletRenderPartsWithContextAndProxy() "/proxy/portal/header-portal.(" + _HASH + ").css", "/proxy/portal/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/nocombo-header-portal.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/header-portlet.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/proxy/portal/header-portal.(" + _HASH + ").js", - "/proxy/portal/nocombo-header-portal.(" + _HASH + ").js", + "/proxy/portal/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/portal/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/proxy/portal/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").js", + "/proxy/portal/o/portlet-web/header-portlet.translatable.(" + + _HASH + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/proxy/portal/module-header-portal.(" + _HASH + ").js", @@ -243,24 +236,22 @@ public void testGetPortletRenderPartsWithContextAndProxy() "/proxy/portal/footer-portal.(" + _HASH + ").css", "/proxy/portal/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/nocombo-footer-portal.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/proxy/portal/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); _assertEquals( Arrays.asList( "/proxy/portal/footer-portal.(" + _HASH + ").js", - "/proxy/portal/nocombo-footer-portal.(" + _HASH + ").js", + "/proxy/portal/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/portal/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/proxy/portal/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").js", + "/proxy/portal/o/portlet-web/footer-portlet.translatable.(" + + _HASH + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/proxy/portal/module-footer-portal.(" + _HASH + ").js", @@ -289,22 +280,20 @@ public void testGetPortletRenderPartsWithProxy() throws Exception { "/proxy/header-portal.(" + _HASH + ").css", "/proxy/header-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/nocombo-header-portal.(" + _HASH + ").css", "/proxy/o/portlet-web/header-portlet.(" + _HASH + ").css", "/proxy/o/portlet-web/header-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").css", "http://example.com/header-portal.css", "http://example.com/header-portlet.css"), portletRenderParts.getHeaderCssPaths()); _assertEquals( Arrays.asList( "/proxy/header-portal.(" + _HASH + ").js", - "/proxy/nocombo-header-portal.(" + _HASH + ").js", + "/proxy/header-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/o/portlet-web/header-portlet.(" + _HASH + ").js", - "/proxy/o/portlet-web/nocombo-header-portlet.(" + _HASH + - ").js", + "/proxy/o/portlet-web/header-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/header-portal.js", "http://example.com/header-portlet.js", "module:/proxy/module-header-portal.(" + _HASH + ").js", @@ -318,22 +307,20 @@ public void testGetPortletRenderPartsWithProxy() throws Exception { "/proxy/footer-portal.(" + _HASH + ").css", "/proxy/footer-portal.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/nocombo-footer-portal.(" + _HASH + ").css", "/proxy/o/portlet-web/footer-portlet.(" + _HASH + ").css", "/proxy/o/portlet-web/footer-portlet.tokenized.(" + _HASH + ").css?themeId=classic_WAR_classictheme&tokenize=true", - "/proxy/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").css", "http://example.com/footer-portal.css", "http://example.com/footer-portlet.css"), portletRenderParts.getFooterCssPaths()); _assertEquals( Arrays.asList( "/proxy/footer-portal.(" + _HASH + ").js", - "/proxy/nocombo-footer-portal.(" + _HASH + ").js", + "/proxy/footer-portal.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "/proxy/o/portlet-web/footer-portlet.(" + _HASH + ").js", - "/proxy/o/portlet-web/nocombo-footer-portlet.(" + _HASH + - ").js", + "/proxy/o/portlet-web/footer-portlet.translatable.(" + _HASH + + ").js?languageId=en_US&translate=true", "http://example.com/footer-portal.js", "http://example.com/footer-portlet.js", "module:/proxy/module-footer-portal.(" + _HASH + ").js", @@ -362,8 +349,7 @@ private void _assertEquals( "Nonempty expected set " + expectedSet, expectedSet.isEmpty()); } - private void _setUpMocks(String contextPath, String proxyPath) - throws Exception { + private void _setUpMocks(String contextPath, String proxyPath) { // HashedFilesRegistryUtil @@ -388,6 +374,10 @@ private void _setUpMocks(String contextPath, String proxyPath) content = "@theme_image_path@"; } + if (fileURI.contains(".translatable.")) { + content = "Liferay.Language.get('portlet')"; + } + Mockito.when( url.openStream() ).thenReturn( @@ -404,14 +394,7 @@ private void _setUpMocks(String contextPath, String proxyPath) _htmlUtilMockedStatic.when( () -> HtmlUtil.escapeURL(Mockito.anyString()) ).thenAnswer( - new Answer() { - - @Override - public String answer(InvocationOnMock invocationOnMock) { - return invocationOnMock.getArgument(0); - } - - } + (Answer)invocationOnMock -> invocationOnMock.getArgument(0) ); // PortalUtil @@ -433,21 +416,15 @@ public String answer(InvocationOnMock invocationOnMock) { Mockito.any(HttpServletRequest.class), Mockito.anyString(), Mockito.anyLong()) ).thenAnswer( - new Answer() { - - @Override - public String answer(InvocationOnMock invocationOnMock) { - String uri = invocationOnMock.getArgument(1, String.class); - long timestamp = invocationOnMock.getArgument( - 2, Long.class); - - if (timestamp < 0) { - return uri; - } + (Answer)invocationOnMock -> { + String uri = invocationOnMock.getArgument(1, String.class); + long timestamp = invocationOnMock.getArgument(2, Long.class); - return uri + "?t=" + timestamp; + if (timestamp < 0) { + return uri; } + return uri + "?t=" + timestamp; } ); @@ -470,6 +447,12 @@ public String answer(InvocationOnMock invocationOnMock) { "" ); + Mockito.when( + themeDisplay.getLanguageId() + ).thenReturn( + "en_US" + ); + Mockito.when( themeDisplay.getThemeId() ).thenReturn( @@ -505,18 +488,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/footer-portal.css", "/footer-portal.tokenized.css", - "http://example.com/footer-portal.css", - "nocombo:/nocombo-footer-portal.css") + "http://example.com/footer-portal.css") ); Mockito.when( _portlet.getFooterPortalJavaScript() ).thenReturn( Arrays.asList( - "/footer-portal.js", "http://example.com/footer-portal.js", + "/footer-portal.js", "/footer-portal.translatable.js", + "http://example.com/footer-portal.js", "module:/module-footer-portal.js", - "module:http://example.com/module-footer-portal.js", - "nocombo:/nocombo-footer-portal.js") + "module:http://example.com/module-footer-portal.js") ); Mockito.when( @@ -524,18 +506,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/footer-portlet.css", "/footer-portlet.tokenized.css", - "http://example.com/footer-portlet.css", - "nocombo:/nocombo-footer-portlet.css") + "http://example.com/footer-portlet.css") ); Mockito.when( _portlet.getFooterPortletJavaScript() ).thenReturn( Arrays.asList( - "/footer-portlet.js", "http://example.com/footer-portlet.js", + "/footer-portlet.js", "/footer-portlet.translatable.js", + "http://example.com/footer-portlet.js", "module:/module-footer-portlet.js", - "module:http://example.com/module-footer-portlet.js", - "nocombo:/nocombo-footer-portlet.js") + "module:http://example.com/module-footer-portlet.js") ); Mockito.when( @@ -543,18 +524,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/header-portal.css", "/header-portal.tokenized.css", - "http://example.com/header-portal.css", - "nocombo:/nocombo-header-portal.css") + "http://example.com/header-portal.css") ); Mockito.when( _portlet.getHeaderPortalJavaScript() ).thenReturn( Arrays.asList( - "/header-portal.js", "http://example.com/header-portal.js", + "/header-portal.js", "/header-portal.translatable.js", + "http://example.com/header-portal.js", "module:/module-header-portal.js", - "module:http://example.com/module-header-portal.js", - "nocombo:/nocombo-header-portal.js") + "module:http://example.com/module-header-portal.js") ); Mockito.when( @@ -562,18 +542,17 @@ public String answer(InvocationOnMock invocationOnMock) { ).thenReturn( Arrays.asList( "/header-portlet.css", "/header-portlet.tokenized.css", - "http://example.com/header-portlet.css", - "nocombo:/nocombo-header-portlet.css") + "http://example.com/header-portlet.css") ); Mockito.when( _portlet.getHeaderPortletJavaScript() ).thenReturn( Arrays.asList( - "/header-portlet.js", "http://example.com/header-portlet.js", + "/header-portlet.js", "/header-portlet.translatable.js", + "http://example.com/header-portlet.js", "module:/module-header-portlet.js", - "module:http://example.com/module-header-portlet.js", - "nocombo:/nocombo-header-portlet.js") + "module:http://example.com/module-header-portlet.js") ); Mockito.when( From d25776e68d241c8a5054e4178b63cdf02ec3b207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Tue, 9 Dec 2025 16:08:58 +0100 Subject: [PATCH 11/12] LPD-52709 Uncomment test --- .../StyleSheetFrontendResourceRequestHandlerTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java index 5a7a960f4440b1..b13fe0ac13eeca 100644 --- a/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java +++ b/modules/apps/frontend-js/frontend-js-web/src/test/java/com/liferay/frontend/js/web/internal/resource/handler/StyleSheetFrontendResourceRequestHandlerTest.java @@ -29,7 +29,6 @@ import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; @@ -50,12 +49,8 @@ public void setUp() { _UNHASHED_FILE_PATH, ".css", ".(" + _HASH + ").css"); } - @Ignore @Test public void testCanHandleRequest() throws Exception { - - // LPD-57326 - StyleSheetFrontendResourceRequestHandler styleSheetFrontendResourceRequestHandler = new StyleSheetFrontendResourceRequestHandler( From 0cdc47c6cf91c0ec4bb65a5942a669340dbdc976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Zaera=20Avell=C3=B3n?= Date: Wed, 17 Dec 2025 09:38:03 +0100 Subject: [PATCH 12/12] LPD-52709 baseline --- .../src/com/liferay/portal/kernel/frontend/esm/packageinfo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo index 682b435622a0aa..2c9afe82e39e46 100644 --- a/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo +++ b/portal-kernel/src/com/liferay/portal/kernel/frontend/esm/packageinfo @@ -1 +1 @@ -version 2.0.0 \ No newline at end of file +version 2.1.0 \ No newline at end of file