Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ob 7091 mailservice and weblate integration #103

Merged
merged 19 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ VOLUME ["/tmp","/log"]
EXPOSE 8080
ARG JAR_FILE
COPY ./MailService.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
ENTRYPOINT ["java","-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
38 changes: 38 additions & 0 deletions ehcache.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<config
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="
http://www.ehcache.org/v3
http://www.ehcache.org/schema/ehcache-core-3.7.xsd">

<cache alias="translations">
<expiry>
<ttl unit="seconds">60</ttl>
</expiry>

<resources>
<offheap unit="MB">10</offheap>
</resources>
</cache>

<cache-template name="default">
<expiry>
<ttl unit="seconds">60</ttl>
</expiry>
<listeners>
<listener>
<class>de.caritas.cob.mailservice.api.cache.CacheEventLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap>1000</heap>
<offheap unit="MB">10</offheap>
<disk persistent="true" unit="MB">20</disk>
</resources>
</cache-template>
</config>
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.caritas.cob.mailservice.api;

import de.caritas.cob.mailservice.api.service.TranslationService;
import java.util.Locale;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class RestApiMessageSource implements MessageSource {


public final @NonNull TranslationService translationService;

@Override
public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
log.info("getMessage called with code: {}, args: {}, defaultMessage: {}, locale: {}", code,
args, defaultMessage, locale);
return translationService.fetchTranslations(locale.getLanguage()).get(code);
}

@Override
public String getMessage(String code, Object[] args, Locale locale)
throws NoSuchMessageException {
return getMessage(code, args, null, locale);
}

@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale)
throws NoSuchMessageException {
if (resolvable == null) {
log.warn("getMessage called with null resolvable");
return null;
}
return getMessage(resolvable.getCodes()[0], null, locale);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.caritas.cob.mailservice.api.cache;

import lombok.extern.slf4j.Slf4j;
import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;

@Slf4j
public class CacheEventLogger implements CacheEventListener<Object, Object> {

@Override
public void onEvent(CacheEvent<? extends Object, ? extends Object> cacheEvent) {
log.info(
"caching event: ", cacheEvent.getKey(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.caritas.cob.mailservice.api.controller;

import de.caritas.cob.mailservice.api.service.TranslationService;
import java.util.Map;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
@RequiredArgsConstructor
public class TranslationController {

private final @NonNull TranslationService translationService;

@GetMapping(value = "/translations")
public ResponseEntity<Map<String, String>> getTranslations() {
var result = translationService.fetchTranslations("de");
return new ResponseEntity<>(result, org.springframework.http.HttpStatus.OK);
}

@GetMapping(value = "/translations/evict")
@ResponseBody
public String evictTranslationCache() {
translationService.evictCache();
return "Cache evicted";
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.caritas.cob.mailservice.api.helper;

import de.caritas.cob.mailservice.api.model.LanguageCode;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import javax.annotation.PostConstruct;
Expand All @@ -21,9 +23,11 @@ void init() {
templateEngine = tempTemplateEngine;
}

public static Optional<String> getProcessedHtml(Map<String, Object> data, String templateName) {
public static Optional<String> getProcessedHtml(Map<String, Object> data, LanguageCode languageCode, String templateName) {

Context context = new Context();
Locale locale = Locale.forLanguageTag(languageCode.getValue());
context.setLocale(locale);

if (data != null) {
data.forEach(context::setVariable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ private void loadRequiredMailDataAndSendMail(MailDTO mail, TemplateDescription d

private void renderAndSend(MailDTO mail, TemplateDescription desc, Map<String, Object> data) {
var subject = templateService.getRenderedSubject(desc, data, mail.getLanguage());

try {
templateService
.render(desc, mail.getTemplate(), data, mail.getLanguage())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package de.caritas.cob.mailservice.api.service;

import static de.caritas.cob.mailservice.api.helper.ThymeleafHelper.getProcessedHtml;

import de.caritas.cob.mailservice.api.exception.TemplateServiceException;
import de.caritas.cob.mailservice.api.helper.ThymeleafHelper;
import de.caritas.cob.mailservice.api.mailtemplate.TemplateDescription;
import de.caritas.cob.mailservice.api.model.LanguageCode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.text.StringSubstitutor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
Expand All @@ -18,6 +21,7 @@
* Service for template processing
**/
@Service
@RequiredArgsConstructor
public class TemplateService {

@Value("${app.imprint.url}")
Expand All @@ -26,6 +30,9 @@ public class TemplateService {
@Value("${app.dataprivacy.url}")
private String dataPrivacyUrl;

@NonNull
private final TranslationService translationService;

/**
* Get the processed html template with replaced placeholders
*
Expand All @@ -41,6 +48,7 @@ public Optional<String> render(TemplateDescription desc, String name,
data.put("urlimpressum", imprintUrl);
data.put("urldatenschutz", dataPrivacyUrl);

data.putAll(getTranslationMapAndDefaultToGermanIfNotFound(language));
List<String> missingFieldList = getMissingTemplateFields(desc, data);

if (!CollectionUtils.isEmpty(missingFieldList)) {
Expand All @@ -51,7 +59,20 @@ public Optional<String> render(TemplateDescription desc, String name,

var templateFilename = desc.getTemplateFilenameOrFallback(language);

return ThymeleafHelper.getProcessedHtml(data, templateFilename);
return translationsArePresentAndNotEmpty(language) ? getProcessedHtml(data, language, templateFilename) :
getProcessedHtml(data, LanguageCode.DE, templateFilename);
}

private boolean translationsArePresentAndNotEmpty(LanguageCode language) {
var translations = translationService.tryFetchTranslations(
language.getValue());
return translations.isPresent() && !translations.get().isEmpty();
}

private Map<String, String> getTranslationMapAndDefaultToGermanIfNotFound(LanguageCode language) {
return translationService.tryFetchTranslations(language.getValue()).orElse(
translationService.tryFetchTranslations(LanguageCode.DE.getValue())
.orElse(new HashMap<>()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package de.caritas.cob.mailservice.api.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.caritas.cob.mailservice.config.apiclient.TranlationMangementServiceApiClient;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;

@Service
@Slf4j
public class TranslationService {

@Value("${weblate.api.url}")
private String apiUrl;

@Value("${weblate.api.key}")
private String apiKey;

@Value("${weblate.project}")
private String project;

@Value("${weblate.component}")
private String component;

private final @NonNull TranlationMangementServiceApiClient tranlationMangementServiceApiClient;

public TranslationService(TranlationMangementServiceApiClient tranlationMangementServiceApiClient) {
this.tranlationMangementServiceApiClient = tranlationMangementServiceApiClient;
}

@Cacheable(value = "translations")
public Map<String, String> fetchTranslations(String languageCode) {
try {

return fetchTranslationAsMap(languageCode);
} catch (JsonProcessingException ex) {
throw new TranslationServiceException(String.format(
"Json file with translations could not be parsed, translation component name: %s",
component), ex);
}
}

@CacheEvict(value = "translations", allEntries = true)
public void evictCache() {
log.info("Evicting translations cache");
}

private Map<String, String> fetchTranslationAsMap(String languageCode) throws JsonProcessingException {
String translations = fetchTranslationsAsString(languageCode);
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(translations, Map.class);
}

public Optional<Map<String, String>> tryFetchTranslations(String languageCode) {

try {
var result = fetchTranslationAsMap(languageCode);
return Optional.of(result);
} catch (JsonProcessingException e) {
log.warn("Error while processing json file with translations. Returning empty translations", e);
return Optional.empty();
}

}

private String fetchTranslationsAsString(String languageCode) {
try {
return tranlationMangementServiceApiClient.tryFetchTranslationsFromTranslationManagementService(project, component,
languageCode);
} catch (HttpClientErrorException e) {
if (HttpStatus.NOT_FOUND.equals(e.getStatusCode())) {
log.warn("Translations for component {}, language {} not found in weblate, returning default translations", component,
languageCode);
return fetchDefaultTranslations(component, languageCode);
} else {
log.error("Error while fetching translations from translation management service", e);
throw e;
}
}
}



private String fetchDefaultTranslations(String translationComponentName, String languageCode) {
var inputStream = TranslationService.class.getResourceAsStream(
getTranslationFilename(translationComponentName + "." + languageCode));
if (inputStream == null) {
return "{}";
}
try {
final List<String> fileLines = IOUtils
.readLines(inputStream, StandardCharsets.UTF_8.displayName());
return String.join("", fileLines);
} catch (IOException ex) {
throw new IllegalStateException(String.format(
"Json file with translations could not be loaded, translation component name: %s",
translationComponentName), ex);
}
}

private String getTranslationFilename(String templateName) {
return "/i18n/" + templateName.toLowerCase() + ".json";
}

private class TranslationServiceException extends RuntimeException {

public TranslationServiceException(String format, JsonProcessingException ex) {
super(format, ex);
}
}
}
Loading
Loading