From 7d12c3636c33a8698072388c33f592b95308fd4c Mon Sep 17 00:00:00 2001 From: Celestino Bellone <3385346+cbellone@users.noreply.github.com> Date: Sun, 26 Mar 2023 16:43:05 +0200 Subject: [PATCH] Preload language (#1192) - cherry-picked from 69d363880ed70b2961156c9f29ef3b2ea507589a --- .../alfio/controller/IndexController.java | 73 +++++++--- src/main/java/alfio/util/RequestUtils.java | 21 +-- .../alfio/controller/IndexControllerTest.java | 132 ++++++++++++++++++ .../api/ControllerConfiguration.java | 7 +- 4 files changed, 202 insertions(+), 31 deletions(-) create mode 100644 src/test/java/alfio/controller/IndexControllerTest.java diff --git a/src/main/java/alfio/controller/IndexController.java b/src/main/java/alfio/controller/IndexController.java index 60bda8932d..2b6ee9e674 100644 --- a/src/main/java/alfio/controller/IndexController.java +++ b/src/main/java/alfio/controller/IndexController.java @@ -16,6 +16,7 @@ */ package alfio.controller; +import alfio.controller.api.v2.model.Language; import alfio.controller.api.v2.user.support.EventLoader; import alfio.controller.support.CSPConfigurer; import alfio.manager.PurchaseContextManager; @@ -27,10 +28,7 @@ import alfio.model.EventDescription; import alfio.model.FileBlobMetadata; import alfio.model.TicketReservationStatusAndValidation; -import alfio.model.TicketReservation.TicketReservationStatus; import alfio.model.system.ConfigurationKeys; -import alfio.model.transaction.PaymentProxy; -import alfio.model.user.Role; import alfio.repository.*; import alfio.repository.user.OrganizationRepository; import alfio.util.Json; @@ -41,7 +39,6 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Profile; import org.springframework.core.io.ClassPathResource; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.stereotype.Controller; @@ -56,15 +53,13 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.regex.Pattern; import static alfio.config.Initializer.PROFILE_LIVE; import static alfio.controller.Constants.*; import static alfio.model.system.ConfigurationKeys.BASE_CUSTOM_CSS; +import static alfio.util.HttpUtils.APPLICATION_JSON; import static java.util.Objects.requireNonNull; @Controller @@ -86,6 +81,7 @@ public class IndexController { private final SubscriptionRepository subscriptionRepository; private final EventLoader eventLoader; private final PurchaseContextManager purchaseContextManager; + private final Json json; private final CsrfTokenRepository csrfTokenRepository; private final CSPConfigurer cspConfigurer; @@ -100,7 +96,8 @@ public IndexController(ConfigurationManager configurationManager, EventLoader eventLoader, PurchaseContextManager purchaseContextManager, CsrfTokenRepository csrfTokenRepository, - CSPConfigurer cspConfigurer) { + CSPConfigurer cspConfigurer, + Json json) { this.configurationManager = configurationManager; this.eventRepository = eventRepository; this.fileUploadRepository = fileUploadRepository; @@ -113,6 +110,7 @@ public IndexController(ConfigurationManager configurationManager, this.purchaseContextManager = purchaseContextManager; this.csrfTokenRepository = csrfTokenRepository; this.cspConfigurer = cspConfigurer; + this.json = json; try (var idxIs = new ClassPathResource("alfio-public-frontend-index.html").getInputStream(); var idxOpenIs = new ClassPathResource("alfio/web-templates/event-open-graph-page.html").getInputStream(); var idxIsR = new InputStreamReader(idxIs, StandardCharsets.UTF_8); @@ -236,8 +234,7 @@ public void replyToIndex(@PathVariable(value = EVENT_SHORT_NAME, required = fals } idx.getElementsByTagName("script").forEach(element -> element.setAttribute(NONCE, nonce)); var head = idx.getElementsByTagName("head").get(0); - head.appendChild(buildScripTag(Json.toJson(configurationManager.getInfo(session)), MediaType.APPLICATION_JSON.toString(), "preload-info", null)); - head.appendChild(buildScripTag(Json.toJson(messageSourceManager.getBundleAsMap("alfio.i18n.public", true, "en", MessageSourceManager.PUBLIC_FRONTEND)), MediaType.APPLICATION_JSON.toString(), "preload-bundle", "en")); + head.appendChild(buildScripTag(json.asJsonString(configurationManager.getInfo(session)), APPLICATION_JSON, "preload-info", null)); var httpServletRequest = requireNonNull(request.getNativeRequest(HttpServletRequest.class)); head.appendChild(buildMetaTag("GID", request.getSessionId())); var csrf = csrfTokenRepository.loadToken(httpServletRequest); @@ -251,10 +248,7 @@ public void replyToIndex(@PathVariable(value = EVENT_SHORT_NAME, required = fals style.appendChild(new Text(baseCustomCss)); head.appendChild(style); } - if (eventShortName != null) { - eventLoader.loadEventInfo(eventShortName, session) - .ifPresent(ev -> head.appendChild(buildScripTag(Json.toJson(ev), MediaType.APPLICATION_JSON.toString(), "preload-event", eventShortName))); - } + preloadTranslations(eventShortName, request, session, eventLoader, head, messageSourceManager, idx, json, lang); JFiveParse.serialize(idx, osw); } } @@ -291,6 +285,33 @@ public String redirectSubscriptionToReservation(@PathVariable("subscriptionId") } } + static void preloadTranslations(String eventShortName, + ServletWebRequest request, + HttpSession session, + EventLoader eventLoader, + Element head, + MessageSourceManager messageSourceManager, + Node idx, + Json json, + String lang) { + String preloadLang = Objects.requireNonNullElse(lang, "en"); + if (eventShortName != null) { + var eventInfoOptional = eventLoader.loadEventInfo(eventShortName, session); + if (eventInfoOptional.isPresent()) { + var ev = eventInfoOptional.get(); + head.appendChild(buildScripTag(json.asJsonString(ev), APPLICATION_JSON, "preload-event", eventShortName)); + preloadLang = getMatchingLocale(request, ev.getContentLanguages().stream().map(Language::getLocale).toList(), lang).getLanguage(); + } + } + head.appendChild(buildScripTag(json.asJsonString(messageSourceManager.getBundleAsMap("alfio.i18n.public", true, preloadLang, MessageSourceManager.PUBLIC_FRONTEND)), "application/json", "preload-bundle", preloadLang)); + // add fallback in english + if (!"en".equals(preloadLang)) { + head.appendChild(buildScripTag(json.asJsonString(messageSourceManager.getBundleAsMap("alfio.i18n.public", true, "en", MessageSourceManager.PUBLIC_FRONTEND)), "application/json", "preload-bundle", "en")); + } + var htmlElement = IterableUtils.get(idx.getElementsByTagName("html"), 0); + htmlElement.setAttribute("lang", preloadLang); + } + private static Element buildScripTag(String content, String type, String id, String param) { var e = new Element("script"); e.appendChild(new Text(content)); @@ -314,14 +335,28 @@ private static String reservationStatusToUrlMapping(TicketReservationStatusAndVa }; } + + /** + * Return the best matching locale. + * + * @param request + * @param contextLanguages list of languages configured for the event (o other contexts) + * @param lang override passed as parameter + * @return + */ + private static Locale getMatchingLocale(ServletWebRequest request, List contextLanguages, String lang) { + var locale = RequestUtils.getMatchingLocale(request, contextLanguages); + if (lang != null && contextLanguages.stream().anyMatch(lang::equalsIgnoreCase)) { + locale = Locale.forLanguageTag(lang); + } + return locale; + } + // see https://github.com/alfio-event/alf.io/issues/708 // use ngrok to test the preview private Document getOpenGraphPage(Document eventOpenGraph, String eventShortName, ServletWebRequest request, String lang) { var event = eventRepository.findByShortName(eventShortName); - var locale = RequestUtils.getMatchingLocale(request, event); - if (lang != null && event.getContentLanguages().stream().map(ContentLanguage::getLanguage).anyMatch(lang::equalsIgnoreCase)) { - locale = Locale.forLanguageTag(lang); - } + var locale = getMatchingLocale(request, event.getContentLanguages().stream().map(ContentLanguage::getLanguage).toList(), lang); var baseUrl = configurationManager.getForSystem(ConfigurationKeys.BASE_URL).getRequiredValue(); diff --git a/src/main/java/alfio/util/RequestUtils.java b/src/main/java/alfio/util/RequestUtils.java index dbe169789c..936b7ee3b1 100644 --- a/src/main/java/alfio/util/RequestUtils.java +++ b/src/main/java/alfio/util/RequestUtils.java @@ -29,10 +29,7 @@ import javax.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.security.Principal; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Optional; +import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -61,6 +58,14 @@ public static boolean isSocialMediaShareUA(String ua) { } + public static Locale getMatchingLocale(ServletWebRequest request, List allowedLanguages) { + var l = requireNonNull(request.getNativeRequest(HttpServletRequest.class)).getLocales(); + List locales = l != null ? IteratorUtils.toList(l.asIterator()) : Collections.emptyList(); + var selectedLocale = locales.stream().map(Locale::getLanguage).filter(allowedLanguages::contains).findFirst() + .orElseGet(() -> allowedLanguages.stream().findFirst().orElseThrow()); + return LocaleUtil.forLanguageTag(selectedLocale); + } + /** * From a given request, return the best locale for the user * @@ -69,12 +74,8 @@ public static boolean isSocialMediaShareUA(String ua) { * @return */ public static Locale getMatchingLocale(ServletWebRequest request, Event event) { - var allowedLanguages = event.getContentLanguages().stream().map(ContentLanguage::getLanguage).collect(Collectors.toSet()); - var l = requireNonNull(request.getNativeRequest(HttpServletRequest.class)).getLocales(); - List locales = l != null ? IteratorUtils.toList(l.asIterator()) : Collections.emptyList(); - var selectedLocale = locales.stream().map(Locale::getLanguage).filter(allowedLanguages::contains).findFirst() - .orElseGet(() -> event.getContentLanguages().stream().findFirst().orElseThrow().getLanguage()); - return LocaleUtil.forLanguageTag(selectedLocale); + var allowedLanguages = event.getContentLanguages().stream().map(ContentLanguage::getLanguage).collect(Collectors.toList()); + return getMatchingLocale(request, allowedLanguages); } public static boolean isAdmin(Principal principal) { diff --git a/src/test/java/alfio/controller/IndexControllerTest.java b/src/test/java/alfio/controller/IndexControllerTest.java new file mode 100644 index 0000000000..59eb038350 --- /dev/null +++ b/src/test/java/alfio/controller/IndexControllerTest.java @@ -0,0 +1,132 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.controller; + +import alfio.controller.api.v2.model.EventWithAdditionalInfo; +import alfio.controller.api.v2.model.Language; +import alfio.controller.api.v2.user.support.EventLoader; +import alfio.manager.i18n.MessageSourceManager; +import alfio.util.Json; +import ch.digitalfondue.jfiveparse.Element; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class IndexControllerTest { + + private EventLoader eventLoader; + private Element head; + private Element index; + private Element html; + private EventWithAdditionalInfo event; + private HttpSession session; + + private ServletWebRequest request; + private MessageSourceManager messageSourceManager; + private Json json; + + @BeforeEach + void setUp() { + eventLoader = mock(EventLoader.class); + request = mock(ServletWebRequest.class); + head = mock(Element.class); + index = mock(Element.class); + html = mock(Element.class); + event = mock(EventWithAdditionalInfo.class); + session = mock(HttpSession.class); + json = mock(Json.class); + messageSourceManager = mock(MessageSourceManager.class); + when(messageSourceManager.getBundleAsMap(anyString(), anyBoolean(), anyString(), same(MessageSourceManager.PUBLIC_FRONTEND))).thenReturn(Map.of()); + when(eventLoader.loadEventInfo(anyString(), eq(session))).thenReturn(Optional.of(event)); + when(index.getElementsByTagName("html")).thenReturn(List.of(html)); + when(json.asJsonString(any())).thenReturn("{}"); + when(request.getNativeRequest(HttpServletRequest.class)).thenReturn(new MockHttpServletRequest()); + } + + @Nested + @DisplayName("Event is present") + class EventIsPresent { + @Test + void singleLanguage() { + when(event.getContentLanguages()).thenReturn(List.of(new Language("it", ""))); + IndexController.preloadTranslations("shortName", request, session, eventLoader, head, messageSourceManager, index, json, null); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("it"), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", "it"); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("en"), same(MessageSourceManager.PUBLIC_FRONTEND)); //for non en language we preload also the fallback + } + + @Test + void singleLanguageWithWrongParam() { + when(event.getContentLanguages()).thenReturn(List.of(new Language("it", ""))); + IndexController.preloadTranslations("shortName", request, session, eventLoader, head, messageSourceManager, index, json, "de"); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("it"), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", "it"); + } + + @Test + void singleLanguageWithParam() { + when(event.getContentLanguages()).thenReturn(List.of(new Language("de", ""))); + IndexController.preloadTranslations("shortName", request, session, eventLoader, head, messageSourceManager, index, json, "de"); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("de"), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", "de"); + } + + @Test + void multipleLanguages() { + when(event.getContentLanguages()).thenReturn(List.of(new Language("de", ""), new Language("it", ""))); + IndexController.preloadTranslations("shortName", request, session, eventLoader, head, messageSourceManager, index, json, null); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("de"), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", "de"); + } + + @ParameterizedTest + @ValueSource(strings = {"it", "de"}) + void multipleLanguagesWithParam(String param) { + when(event.getContentLanguages()).thenReturn(List.of(new Language("de", ""), new Language("it", ""))); + IndexController.preloadTranslations("shortName", request, session, eventLoader, head, messageSourceManager, index, json, param); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq(param), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", param); + } + + + } + + @Test + void preloadTranslationsEventNotPresent() { + IndexController.preloadTranslations(null, request, session, eventLoader, head, messageSourceManager, index, json, null); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("en"), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", "en"); + + IndexController.preloadTranslations(null, request, session, eventLoader, head, messageSourceManager, index, json, "it"); + verify(messageSourceManager).getBundleAsMap(anyString(), eq(true), eq("it"), same(MessageSourceManager.PUBLIC_FRONTEND)); + verify(html).setAttribute("lang", "it"); + } +} \ No newline at end of file diff --git a/src/test/java/alfio/controller/api/ControllerConfiguration.java b/src/test/java/alfio/controller/api/ControllerConfiguration.java index b1de45ac43..3e0ed7420c 100644 --- a/src/test/java/alfio/controller/api/ControllerConfiguration.java +++ b/src/test/java/alfio/controller/api/ControllerConfiguration.java @@ -24,6 +24,7 @@ import alfio.manager.system.ConfigurationManager; import alfio.repository.*; import alfio.repository.user.OrganizationRepository; +import alfio.util.Json; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -77,7 +78,8 @@ public IndexController indexController(ConfigurationManager configurationManager EventLoader eventLoader, PurchaseContextManager purchaseContextManager, CsrfTokenRepository csrfTokenRepository, - CSPConfigurer cspConfigurer) { + CSPConfigurer cspConfigurer, + Json json) { return new IndexController(configurationManager, eventRepository, fileUploadRepository, @@ -89,6 +91,7 @@ public IndexController indexController(ConfigurationManager configurationManager eventLoader, purchaseContextManager, csrfTokenRepository, - cspConfigurer); + cspConfigurer, + json); } }