Skip to content

Instantly share code, notes, and snippets.

@michalmela
Last active January 31, 2020 09:07
Show Gist options
  • Save michalmela/bc085f2a5d4b7f5963da to your computer and use it in GitHub Desktop.
Save michalmela/bc085f2a5d4b7f5963da to your computer and use it in GitHub Desktop.
[spring i18n uris] I18nRequestMappingHandlerMapping -- a Spring Framework HandlerMapping which supports translation of @RequestMapping values, so that you can have multiple, localized paths to a single controller based on one annotation value and easily add further translations to your mappings without going through all the @RequestMapping value…
# author: jmelon (https://gist.github.com/michalmela)
#
# a sample translation properties source file
#
# given a sample mapping: @RequestMapping("/doctors/{doctor_id}/appointments/{appointment_id"),
# a I18nRequestMappingHandlerMapping with SimplePatternTranslator, "en" and "pl" supported locales and "pl" default
# locale and "addDefaultLocaleTranslationWithPrefix" property set to true, this translations file will produce following
# mappings:
# - /en/doctors/{doctor_id}/appointments/{appointment_id}
# - /pl/doktorzy/{doctor_id}/wizyty/{appointment_id}
# - /doktorzy/{doctor_id}/wizyty/{appointment_id}
#
pl.doctors=doktorzy
en.doctors=doctors
pl.appointments=wizyty
en.appointments=appointments
package com.blogspot.jmelon.si18nrmhmt.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.*;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* A {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping} subclass which makes it
* possible to translate a mapped {@link org.springframework.stereotype.Controller Controller}'s {@link
* org.springframework.web.bind.annotation.RequestMapping RequestMapping} annotation value to any number of {@link
* java.util.Locale locale}-specific mappings using provided {@link com.blogspot.jmelon.si18nrmhmt.controller.PatternTranslator}
* instance
*
* @author jmelon
*/
public class I18nRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private PatternTranslator patternTranslator;
/**
* @inheritDoc
*/
@Override
protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation,
RequestCondition<?> customCondition) {
String[] rawPatterns = annotation.value();
String[] patternsWithoutEmbeddedValues = resolveEmbeddedValuesInPatterns(rawPatterns);
String[] i18nPatterns;
if (patternTranslator == null) {
i18nPatterns = patternsWithoutEmbeddedValues;
} else {
i18nPatterns = this.patternTranslator.translatePath(patternsWithoutEmbeddedValues);
}
return new RequestMappingInfo(
annotation.name(),
new PatternsRequestCondition(i18nPatterns, getUrlPathHelper(), getPathMatcher(),
this.useSuffixPatternMatch(), this.useTrailingSlashMatch(), this.getFileExtensions()),
new RequestMethodsRequestCondition(annotation.method()),
new ParamsRequestCondition(annotation.params()),
new HeadersRequestCondition(annotation.headers()),
new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
new ProducesRequestCondition(annotation.produces(), annotation.headers(),
this.getContentNegotiationManager()),
customCondition);
}
/**
* @return a {@link com.blogspot.jmelon.si18nrmhmt.controller.PatternTranslator} used to translate mappings to all the local mappings
*/
public PatternTranslator getPatternTranslator() {
return patternTranslator;
}
/**
* @param patternTranslator a {@link com.blogspot.jmelon.si18nrmhmt.controller.PatternTranslator} used to translate mappings to all the local mappings
*/
public void setPatternTranslator(PatternTranslator patternTranslator) {
this.patternTranslator = patternTranslator;
}
}
package com.blogspot.jmelon.si18nrmhmt.controller;
/**
* An interface for {@link com.blogspot.jmelon.si18nrmhmt.controller.I18nRequestMappingHandlerMapping} for translating
* the input patterns
*
* @author jmelon
*/
public interface PatternTranslator {
/**
* @param rawPatterns input patterns, as passed as values to {@link org.springframework.web.bind.annotation.RequestMapping}
* @return a set of translated patterns
*/
String[] translatePath(String[] rawPatterns);
}
package com.blogspot.jmelon.si18nrmhmt.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import java.util.*;
/**
* A base {@link com.blogspot.jmelon.si18nrmhmt.controller.PatternTranslator} implementation.
*
* @author jmelon
*/
public class SimplePatternTranslator implements PatternTranslator {
private Locale defaultLocale;
private boolean addDefaultLocaleTranslationWithPrefix;
private List<Locale> supportedLocales;
private Properties translations;
/**
* @return A list of locales for which paths are to be generated.
* <p/>
* These must not contain country part unless the country part is desired in the prefix.
*/
public List<Locale> getSupportedLocales() {
return supportedLocales;
}
/**
* A list of locales for which paths are to be generated.
* <p/>
* These must not contain country part unless the country part is desired in the prefix.
*/
public void setSupportedLocales(List<Locale> supportedLocales) {
this.supportedLocales = supportedLocales;
}
/**
* A list of locales for which paths are to be generated.
* <p/>
* These must not contain country part unless the country part is desired in the prefix.
*/
public void setSupportedLocales(Locale... supportedLocales) {
this.supportedLocales = Arrays.asList(supportedLocales);
}
/**
* @return Whether or not to also add a pattern for the default locale which does contain the language prefix. If false,
* only the prefix-less mapping for default locale will be added
*/
public boolean isAddDefaultLocaleTranslationWithPrefix() {
return addDefaultLocaleTranslationWithPrefix;
}
/**
* Sets whether or not to also add a pattern for the default locale which does contain the language prefix. If false,
* only the prefix-less mapping for default locale will be added
*/
public void setAddDefaultLocaleTranslationWithPrefix(boolean addDefaultLocaleTranslationWithPrefix) {
this.addDefaultLocaleTranslationWithPrefix = addDefaultLocaleTranslationWithPrefix;
}
/**
* @return The locale for which a pattern will be generated containing no language prefix. May be null if all paths are to
* be prefixed.
*/
public Locale getDefaultLocale() {
return defaultLocale;
}
/**
* Sets the locale for which a pattern will be generated containing no language prefix. May be null if all paths are to
* be prefixed.
*/
public void setDefaultLocale(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
public Properties getTranslations() {
return translations;
}
/**
* Sets properties with translations, which are supposed to be in the following format: {locale}.{patternPart}.
* <p/>
* So to translate, e.g., following mapping: {@code /doctors/appointments/} to locales {@code pl} and {@code en} you may want these
* properties to contain following keys:
* <ul>
* <li>{@code pl.doctors}</li>
* <li>{@code pl.appointments}</li>
* <li>{@code en.doctors}</li>
* <li>{@code en.appointments}</li>
* </ul>
*/
public void setTranslations(Properties translations) {
this.translations = translations;
}
@Override
public String[] translatePath(String[] rawPatterns) {
Assert.notNull(supportedLocales, "supported locales cannot be null");
List<String> translatedPatterns = new ArrayList<>();
for (String rawPattern : rawPatterns) {
for (Locale locale : supportedLocales) {
String localPattern = getLocalPattern(rawPattern, locale);
if (locale.equals(defaultLocale)) {
addPattern(translatedPatterns, localPattern);
} else {
}
if (!locale.equals(defaultLocale) || addDefaultLocaleTranslationWithPrefix) {
addPrefixedPattern(translatedPatterns, locale, localPattern);
}
}
}
return translatedPatterns.toArray(new String[translatedPatterns.size()]);
}
/**
* note: you may want to Override this to change the way the locale is formatted (like en-us or en_us or en/us
* etc.)
*
* @return translated pattern with prefix
*/
protected String getPrefixedPattern(Locale locale, String localPattern) {
return (localPattern.startsWith("/") ? "/" + locale : locale + "/") + localPattern;
}
protected String getLocalPattern(String rawPattern, Locale locale) {
String[] explodedPattern = rawPattern.split("/");
for (int i = 0; i < explodedPattern.length; i++) {
if (partIsProcessable(explodedPattern[i])) {
String translationKey = getTranslationKey(locale, explodedPattern[i]);
String translation = translations.getProperty(translationKey);
if (translation != null && translation.length() > 0) {
explodedPattern[i] = translation;
}
}
}
return StringUtils.join(explodedPattern, "/");
}
protected String getTranslationKey(Locale locale, String patternPart) {
return locale.toString() + "." + patternPart;
}
protected boolean partIsProcessable(String s) {
return !(s.contains("{") || s.contains("*") || s.length() == 0);
}
private void addPattern(List<String> translatedPatterns, String localPattern) {
translatedPatterns.add(localPattern);
}
private void addPrefixedPattern(List<String> translatedPatterns, Locale locale, String localPattern) {
addPattern(translatedPatterns, getPrefixedPattern(locale, localPattern));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment