Skip to content

Commit

Permalink
Preload language (#1192) - cherry-picked from 69d3638
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone committed Mar 30, 2023
1 parent 0580cab commit 7d12c36
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 31 deletions.
73 changes: 54 additions & 19 deletions src/main/java/alfio/controller/IndexController.java
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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));
Expand All @@ -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<String> 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();

Expand Down
21 changes: 11 additions & 10 deletions src/main/java/alfio/util/RequestUtils.java
Expand Up @@ -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;

Expand Down Expand Up @@ -61,6 +58,14 @@ public static boolean isSocialMediaShareUA(String ua) {
}


public static Locale getMatchingLocale(ServletWebRequest request, List<String> allowedLanguages) {
var l = requireNonNull(request.getNativeRequest(HttpServletRequest.class)).getLocales();
List<Locale> 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
*
Expand All @@ -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<Locale> 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) {
Expand Down
132 changes: 132 additions & 0 deletions 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 <http://www.gnu.org/licenses/>.
*/
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");
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -89,6 +91,7 @@ public IndexController indexController(ConfigurationManager configurationManager
eventLoader,
purchaseContextManager,
csrfTokenRepository,
cspConfigurer);
cspConfigurer,
json);
}
}

0 comments on commit 7d12c36

Please sign in to comment.