Last active
January 31, 2020 09:07
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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