Skip to content

Commit

Permalink
[Feature] Support langCodeAliases (#141)
Browse files Browse the repository at this point in the history
`langCodeAliases` lets you specify the language identifier for your supported languages.

> For example, the default language identifier for English is `en`, such that a URL for your page in English language may look like `http://site.com/en/page`.
> With `langCodeAliases`, you can change the language identifier to `us`, for example. The resulting URL would look like `http://site.com/us/page` instead.

See README for more details.

### Comments
This feature is relevant for path, query, and subdomain url patterns. As a result, the methods `getLang`, `convertToDefaultLanguage`, and `convertToTargetLanguage` has changed quite a bit for these pattern handlers.

`langCodeAliases` is a two-in-one feature from an implementation perspective. Allowing alias for a target language is a simple change, we only need to replace the default language code for identifying the language.

When an alias is set for default language, it changes the rules for interpreting requests. Request URLs without an explicit language identifier will now be ignored.

`getLang` has been changed to return `null` _only for input URLs that cannot be intercepted_. For any URL that WovnServletFilter should process, `getLang` will either return default language or a target language. (The last pattern handler, `CustomDomainUrlLanguagePatternHandler` already conforms to this behavior.)

With the new behavior of `getLang` , there is no longer need for the method `canInterceptUrl`. It has been removed.
  • Loading branch information
torkelbe authored Feb 4, 2020
1 parent bd1b1d1 commit 2de86ab
Show file tree
Hide file tree
Showing 24 changed files with 791 additions and 260 deletions.
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ originalQueryStringHeader | |
ignoreClasses | |
enableFlushBuffer | | false
sitePrefixPath | |
langCodeAliases | |
customDomainLangs | |
debugMode | | false

Expand Down Expand Up @@ -266,7 +267,42 @@ Furthermore, it is highly recommended to also configure your `web.xml` with a co
</filter-mapping>
```

### 2.10. customDomainLangs
### 2.10. langCodeAliases

This setting lets you specify the language identifier for your supported languages.

For example, the default language identifier for English is `en`, such that a URL for your page in English language may look like `http://site.com/en/page`.
With `langCodeAliases`, you can change the language identifier to `us`, for example. The resulting URL would look like `http://site.com/us/page` instead.

This setting is only valid for url patterns `path`, `query`, and `subdomain`.

The format is as follows
```
FORMAT: <langCode>:<alias>,<langCode>:<alias>,...
EXAMPLE: ja:japan,en:us
```
In `web.xml`, the configuration will look like this
```xml
<init-param>
<param-name>langCodeAliases</param-name>
<param-value>ja:japan,en:us</param-value>
</init-param>
```

#### Alias for default language

If your original content exists at a location that already includes a form of language code, you can make the WovnServletFilter treat this path or subdomain as a language code by configuring a language alias for your default language.

To illustrate, here is an example:

> Your content already exists at `http://site.com/jp/*`, and your default language is Japanese.
>
> You want the URLs for translated content in English to change the `/jp/` to `/en/`, such that `http://site.com/jp/home.html` becomes `http://site.com/en/home.html`.
Achieve this result by configuring `jp` as an alias for Japanese.

### 2.11. customDomainLangs

This setting lets you define the domain and path that corresponds to each of your supported languages.

Expand Down Expand Up @@ -307,7 +343,7 @@ If this setting is used, each language declared in `supportedLangs` must be give
Lastly, the path declared for your original language must match the structure of the underlying web server.
In other words, you cannot use this setting to change the request path of your content in original language.

### 2.11. debugMode
### 2.12. debugMode

A flag to enable extra debugging features.

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<groupId>com.github.wovnio</groupId>
<artifactId>wovnjava</artifactId>
<name>wovnjava</name>
<version>1.0.1</version>
<version>1.1.0</version>
<url>https://github.com/WOVNio/wovnjava</url>

<licenses>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/github/wovnio/wovnjava/Api.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ private String getApiBody(String lang, String body) throws UnsupportedEncodingEx
appendKeyValue(sb, "&lang_code=", lang);
appendKeyValue(sb, "&url_pattern=", settings.urlPattern);
appendKeyValue(sb, "&site_prefix_path=", settings.sitePrefixPath);
appendKeyValue(sb, "&custom_lang_aliases=", LanguageAliasSerializer.serializeToJson(settings.langCodeAliases));
appendKeyValue(sb, "&custom_domain_langs=", CustomDomainLanguageSerializer.serializeToJson(settings.customDomainLanguages));
appendKeyValue(sb, "&product=", "wovnjava");
appendKeyValue(sb, "&version=", Settings.VERSION);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,6 @@ String convertToTargetLanguage(String urlString, Lang lang) {
}
}

public boolean canInterceptUrl(String urlString) {
URL url = getUrlObject(urlString);
if (url == null) return false;

CustomDomainLanguage customDomainLanguage = this.customDomainLanguages.getCustomDomainLanguageByUrl(url);
return customDomainLanguage != null;
}

private URL getUrlObject(String url) {
try {
return new URL(url);
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/com/github/wovnio/wovnjava/Headers.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ class Headers {
this.urlLanguagePatternHandler = urlLanguagePatternHandler;

String clientRequestUrl = UrlResolver.computeClientRequestUrl(request, settings);
Lang urlLang = this.urlLanguagePatternHandler.getLang(clientRequestUrl);
this.requestLang = urlLang == null ? settings.defaultLang : urlLang;
this.requestLang = this.urlLanguagePatternHandler.getLang(clientRequestUrl);
this.clientRequestUrlInDefaultLanguage = this.urlLanguagePatternHandler.convertToDefaultLanguage(clientRequestUrl);

String currentContextUrl = request.getRequestURL().toString();
Expand All @@ -53,10 +52,12 @@ class Headers {

this.shouldRedirectExplicitDefaultLangUrl = this.urlLanguagePatternHandler.shouldRedirectExplicitDefaultLangUrl(clientRequestUrl);

this.isValidRequest = this.urlContext != null && this.urlLanguagePatternHandler.canInterceptUrl(clientRequestUrl);
this.isValidRequest = this.requestLang != null && this.urlContext != null;
}

/*
* Convert a redirect URL into the language of the current request
*
* Take as input a location string of any form (relative path, absolute path, absolute URL).
* If the location needs a Wovn language code, return an absolute URL string of that location
* with language code of the current request language. Else return the location as-is.
Expand All @@ -70,8 +71,7 @@ public String locationWithLangCode(String location) {

boolean shouldAddLanguageCode = url != null
&& this.urlContext.isSameHost(url)
&& this.urlLanguagePatternHandler.getLang(url.toString()) == null
&& this.urlLanguagePatternHandler.canInterceptUrl(url.toString());
&& this.urlLanguagePatternHandler.getLang(url.toString()) != null;

if (!shouldAddLanguageCode) return location;

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/github/wovnio/wovnjava/HtmlConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,16 @@ private void appendSnippet(String lang) {
sb.append(settings.defaultLang.code);
sb.append("&urlPattern=");
sb.append(settings.urlPattern);
sb.append("&langCodeAliases={}&version=");
sb.append("&version=");
sb.append(Settings.VERSION);
if (!settings.sitePrefixPath.isEmpty()) {
sb.append("&sitePrefixPath=");
sb.append(settings.sitePrefixPath.replaceFirst("/", ""));
}
if (settings.langCodeAliases.size() > 0) {
sb.append("&langCodeAliases=");
sb.append(LanguageAliasSerializer.serializeToJson(settings.langCodeAliases));
}
if (settings.customDomainLanguages != null) {
sb.append("&customDomainLangs=");
sb.append(CustomDomainLanguageSerializer.serializeToJson(settings.customDomainLanguages));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.github.wovnio.wovnjava;

import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;

class LanguageAliasSerializer {
private LanguageAliasSerializer() {}

public static Map<Lang, String> deserializeFilterConfig(String rawLangCodeAliases) throws ConfigurationError {
Map<Lang, String> langCodeAliases = new LinkedHashMap<Lang, String>();

if (rawLangCodeAliases == null || rawLangCodeAliases.isEmpty()) {
return langCodeAliases;
}

for (String rawPair : rawLangCodeAliases.split(",")) {
if (rawPair == null || rawPair.isEmpty()) {
continue;
}
String[] splitPair = rawPair.split(":");
if (splitPair.length != 2) {
throw new ConfigurationError("Invalid configuration format for \"langCodeAliases\": " + rawLangCodeAliases);
}

Lang lang = Lang.get(splitPair[0].trim());
if (lang == null) {
throw new ConfigurationError("Invalid source language for \"langCodeAliases\", each left-hand value must match a supported language code.");
}

String alias = splitPair[1].trim();
langCodeAliases.put(lang, alias);
}

return langCodeAliases;
}

public static String serializeToJson(Map<Lang, String> langCodeAliases) {
if (langCodeAliases == null || langCodeAliases.size() < 1) return "{}";

ArrayList<String> items = new ArrayList<String>();
for (Map.Entry<Lang, String> langCodeAlias : langCodeAliases.entrySet()) {
Lang lang = langCodeAlias.getKey();
String alias = langCodeAlias.getValue();
items.add(lang.code + "\":\"" + alias);
}
return "{\"" + String.join("\",\"", items) + "\"}";
}
}
36 changes: 36 additions & 0 deletions src/main/java/com/github/wovnio/wovnjava/LanguageAliases.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.github.wovnio.wovnjava;

import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.net.URL;

class LanguageAliases {
public final boolean hasAliasForDefaultLang;

private final Map<Lang, String> langMap;

public LanguageAliases(ArrayList<Lang> supportedLangs, Map<Lang, String> langCodeAliases, Lang defaultLang) {
this.hasAliasForDefaultLang = langCodeAliases.get(defaultLang) != null;

HashMap<Lang, String> langMap = new HashMap<Lang, String>();
for (Lang lang : supportedLangs) {
String alias = langCodeAliases.get(lang);
langMap.put(lang, (alias != null) ? alias : lang.code);
}
this.langMap = langMap;
}

public String getAliasFromLanguage(Lang lang) {
return this.langMap.get(lang);
}

public Lang getLanguageFromAlias(String alias) {
for (Map.Entry<Lang, String> entry : this.langMap.entrySet()) {
if (entry.getValue().equals(alias)) {
return entry.getKey();
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,90 @@

class PathUrlLanguagePatternHandler extends UrlLanguagePatternHandler {
private Lang defaultLang;
private ArrayList<Lang> supportedLangs;
private LanguageAliases languageAliases;
private String sitePrefixPath;
private Pattern getLangPattern;
private Pattern matchSitePrefixPathPattern;

PathUrlLanguagePatternHandler(Lang defaultLang, ArrayList<Lang> supportedLangs, String sitePrefixPath) {
PathUrlLanguagePatternHandler(Lang defaultLang, LanguageAliases languageAliases, String sitePrefixPath) {
this.defaultLang = defaultLang;
this.supportedLangs = supportedLangs;
this.languageAliases = languageAliases;
this.sitePrefixPath = sitePrefixPath;
this.getLangPattern = this.buildGetLangPattern(sitePrefixPath);
this.matchSitePrefixPathPattern = this.buildMatchSitePrefixPathPattern(sitePrefixPath);
}

Lang getLang(String url) {
Lang lang = this.getLangMatch(url, this.getLangPattern);
return (lang != null && this.supportedLangs.contains(lang)) ? lang : null;
if (!this.matchSitePrefixPathPattern.matcher(url).lookingAt()) {
return null;
}

String languageIdentifier = this.findLanguageIdentifier(url, this.getLangPattern);
Lang lang = this.languageAliases.getLanguageFromAlias(languageIdentifier);
if (lang != null) {
return lang;
} else if (this.languageAliases.hasAliasForDefaultLang) {
// Default language has a language alias but the input URL path does not
// include a language identifier, so we cannot identify the request language.
// (That also means that we cannot intercept a request for the resource.)
return null;
} else {
return this.defaultLang;
}
}

String convertToDefaultLanguage(String url) {
Lang currentLang = this.getLang(url);
if (currentLang == null) {
return url;
}

String newUrl = this.removeLang(url, currentLang);
if (this.languageAliases.hasAliasForDefaultLang) {
return this.insertLang(newUrl, this.defaultLang);
} else {
return this.removeLang(url, currentLang.code);
return newUrl;
}
}

String convertToTargetLanguage(String url, Lang lang) {
Lang currentLang = this.getLangMatch(url, this.getLangPattern);
if (currentLang != null && this.supportedLangs.contains(currentLang)) {
url = this.removeLang(url, currentLang.code);
String convertToTargetLanguage(String url, Lang targetLang) {
if (targetLang == this.defaultLang) {
return this.convertToDefaultLanguage(url);
}
return this.insertLang(url, lang.code);
}

private String removeLang(String url, String lang) {
if (lang.isEmpty()) return url;
String languageIdentifier = this.findLanguageIdentifier(url, this.getLangPattern);
Lang currentLang = this.languageAliases.getLanguageFromAlias(languageIdentifier);
if (currentLang != null) {
String newUrl = this.removeLang(url, currentLang);
return this.insertLang(newUrl, targetLang);
} else if (this.languageAliases.hasAliasForDefaultLang) {
// Default language has a language alias but the input URL path does not
// include a language identifier, so we cannot convert the URL language.
return url;
} else {
return this.insertLang(url, targetLang);
}
}

Pattern removeLangPattern = buildRemoveLangPattern(lang);
private String removeLang(String url, Lang lang) {
String langCode = this.languageAliases.getAliasFromLanguage(lang);
Pattern removeLangPattern = buildRemoveLangPattern(langCode);
Matcher matcher = removeLangPattern.matcher(url);
return matcher.replaceFirst("$1$2$3$5");
}

private String insertLang(String url, String lang) {
return this.matchSitePrefixPathPattern.matcher(url).replaceFirst("$1$2$3/" + lang + "$4");
}

public boolean canInterceptUrl(String url) {
return this.matchSitePrefixPathPattern.matcher(url).lookingAt();
private String insertLang(String url, Lang lang) {
String langCode = this.languageAliases.getAliasFromLanguage(lang);
return this.matchSitePrefixPathPattern.matcher(url).replaceFirst("$1$2$3/" + langCode + "$4");
}

/*
* Redirect to same URL without language code if the language code
* found in the URL path is for default language
*/
public boolean shouldRedirectExplicitDefaultLangUrl(String url) {
Lang pathLang = this.getLangMatch(url, this.getLangPattern);
return pathLang == this.defaultLang;
String languageIdentifier = this.findLanguageIdentifier(url, this.getLangPattern);
return !this.languageAliases.hasAliasForDefaultLang && this.defaultLang.code.equals(languageIdentifier);
}

private Pattern buildGetLangPattern(String sitePrefixPath) {
Expand Down
Loading

0 comments on commit 2de86ab

Please sign in to comment.