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

[#2064] Multilangual routes files support #1012

Open
wants to merge 6 commits into
base: 1.4.x
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion framework/pym/play/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
import shutil
import socket
import glob

from play.utils import *

Expand Down Expand Up @@ -46,7 +47,7 @@ def __init__(self, application_path, env, ignoreMissingModules = False):

def check(self):
try:
assert os.path.exists(os.path.join(self.path, 'conf', 'routes'))
assert (os.path.exists(os.path.join(self.path, 'conf', 'routes')) or len(glob.glob(os.path.join(self.path, 'conf', 'routes.??')))>0 or len(glob.glob(os.path.join(self.path, 'conf', 'routes.??_??')))>0)
assert os.path.exists(os.path.join(self.path, 'conf', 'application.conf'))
except AssertionError:
print "~ Oops. conf/routes or conf/application.conf missing."
Expand Down
18 changes: 18 additions & 0 deletions framework/src/play/Play.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ public boolean isProd() {
* Main routes file
*/
public static VirtualFile routes;

/**
* Main routes file
*/
public static List<VirtualFile> internationalizedRoutes;

/**
* Plugin routes files
*/
Expand Down Expand Up @@ -277,6 +283,7 @@ public static void init(File root, String id) {

// Main route file
routes = appRoot.child("conf/routes");
internationalizedRoutes = loadMultilanguageRoutesFiles(appRoot);

// Plugin route files
modulesRoutes = new HashMap<>(16);
Expand Down Expand Up @@ -321,6 +328,17 @@ public static void init(File root, String id) {
Play.initialized = true;
}

public static List<VirtualFile> loadMultilanguageRoutesFiles(VirtualFile appRoot) {
List<VirtualFile> routes = new ArrayList<VirtualFile>();
for (VirtualFile vf: appRoot.child("conf").list()) {
String virtualFileName = vf.getName();
if(virtualFileName !=null && virtualFileName.matches("routes\\.[A-Za-z]{2}(_[A-Za-z]{2})?")){
routes.add(vf);
}
}
return routes;
}

public static void guessFrameworkPath() {
// Guess the framework path
try {
Expand Down
75 changes: 61 additions & 14 deletions framework/src/play/mvc/Router.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
package play.mvc;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.lang.StringUtils;

import jregex.Matcher;
import jregex.Pattern;
import jregex.REFlags;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import play.Logger;
import play.Play;
import play.Play.Mode;
import play.exceptions.NoRouteFoundException;
import play.i18n.Lang;
import play.mvc.results.NotFound;
import play.mvc.results.RenderStatic;
import play.templates.TemplateLoader;
import play.utils.Default;
import play.utils.Utils;
import play.vfs.VirtualFile;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
* The router matches HTTP requests to action invocations
*/
Expand Down Expand Up @@ -55,6 +53,9 @@ public static void load(String prefix) {
routes.clear();
actionRoutesCache.clear();
parse(Play.routes, prefix);
for (VirtualFile routeFile : Play.internationalizedRoutes) {
parse(routeFile, prefix);
}
lastLoading = System.currentTimeMillis();
// Plugins
Play.pluginCollection.onRoutesLoaded();
Expand Down Expand Up @@ -140,6 +141,7 @@ public static Route getRoute(String method, String path, String action, String p
route.routesFileLine = line;
route.addFormat(headers);
route.addParams(params);
route.setLocaleBasedOnMultilangualRoutesFile(sourceFile);
route.compute();
if (Logger.isTraceEnabled()) {
Logger.trace("Adding [" + route.toString() + "] with params [" + params + "] and headers [" + headers + "]");
Expand Down Expand Up @@ -237,6 +239,12 @@ public static void detectChanges(String prefix) {
}
}
}
for (VirtualFile route : Play.internationalizedRoutes) {
if (route.lastModified() > lastLoading) {
load(prefix);
return;
}
}
}

/**
Expand Down Expand Up @@ -292,6 +300,9 @@ public static Route route(Http.Request request) {
if (request.action.equals("404")) {
throw new NotFound(route.path);
}
if(CollectionUtils.isNotEmpty(Play.internationalizedRoutes) && StringUtils.isNotEmpty(route.locale) && !route.locale.equals(Lang.get())){
Lang.change(route.locale);
}
return route;
}
}
Expand Down Expand Up @@ -588,6 +599,9 @@ private static List<ActionRoute> getActionRoutes(String action) {
matchingRoutes = findActionRoutes(action);
actionRoutesCache.put(action, matchingRoutes);
}
if(CollectionUtils.isNotEmpty(Play.internationalizedRoutes)){
matchingRoutes = prioritizeActionRoutesBasedOnActiveLocale(matchingRoutes);
}
return matchingRoutes;
}

Expand All @@ -614,6 +628,28 @@ private static List<ActionRoute> findActionRoutes(String action) {
return matchingRoutes;
}

/**
* Prioritize action routes based on active locale and Play.langs properties. Active lang has highest priority, then prioritized according to Play.langs order.
*
* @param matchingRoutes
*/
private static List<ActionRoute> prioritizeActionRoutesBasedOnActiveLocale(List<ActionRoute> matchingRoutes) {
if(matchingRoutes.size()==0) return matchingRoutes;
final String locale = Lang.get();
if(StringUtils.isEmpty(locale)) return matchingRoutes;
List<Router.ActionRoute> sortedMatchingRoutes = new ArrayList<>(matchingRoutes);
sortedMatchingRoutes.sort(new Comparator<ActionRoute>() {
@Override
public int compare(ActionRoute ar1, ActionRoute ar2) {
if(locale.equals(ar1.route.locale)) return -1;
if(locale.equals(ar2.route.locale)) return 1;
return Integer.compare(Play.langs.indexOf(ar1.route.locale), Play.langs.indexOf(ar2.route.locale));
}
});
return sortedMatchingRoutes;
}


private static final class ActionRoute {
private Route route;
private Map<String, String> args = new HashMap<>(2);
Expand Down Expand Up @@ -732,6 +768,7 @@ public static class Route {
static Pattern customRegexPattern = new Pattern("\\{([a-zA-Z_][a-zA-Z_0-9]*)\\}");
static Pattern argsPattern = new Pattern("\\{<([^>]+)>([a-zA-Z_0-9]+)\\}");
static Pattern paramPattern = new Pattern("([a-zA-Z_0-9]+):'(.*)'");
String locale;

public void compute() {
this.host = "";
Expand Down Expand Up @@ -975,5 +1012,15 @@ static class Arg {
public String toString() {
return method + " " + path + " -> " + action;
}

private void setLocaleBasedOnMultilangualRoutesFile(String absolutePath){
if(StringUtils.isEmpty(absolutePath)){
return;
}
String fileName = Paths.get(absolutePath).getFileName().toString();
if(StringUtils.isNotEmpty(fileName) && fileName.matches("routes\\.[A-Za-z]{2}(_[A-Za-z]{2})?")){
this.locale = fileName.split("\\.")[1];
}
}
}
}
162 changes: 160 additions & 2 deletions framework/test-src/play/mvc/RouterTest.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package play.mvc;

import org.junit.Test;

import org.junit.*;
import play.Play;
import play.i18n.Lang;
import play.mvc.Http.Request;
import play.mvc.results.NotFound;
import play.mvc.results.RenderStatic;
import play.vfs.VirtualFile;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;

import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class RouterTest {

@org.junit.Before public void initialize() {
Router.routes.clear();
Play.internationalizedRoutes = null;
Play.routes = null;
Play.configuration = null;
}

@Test
public void test_getBaseUrl() {

Expand Down Expand Up @@ -136,4 +149,149 @@ public boolean canRenderFile(Request request){
}
return false;
}

@Test
public void test_loadRoutesFiles() {

Play.internationalizedRoutes = mock(ArrayList.class);
when(Play.internationalizedRoutes.size()).thenReturn(3);

VirtualFile appRoot = mock(VirtualFile.class);
List<VirtualFile> routes = new ArrayList<>();
VirtualFile routesFile = mock(VirtualFile.class);
when(routesFile.getName()).thenReturn("routes");
routes.add(routesFile);
VirtualFile appConf = mock(VirtualFile.class);
when(appConf.getName()).thenReturn("application.conf");
routes.add(appConf);
VirtualFile multilangRoutes = mock(VirtualFile.class);
when(multilangRoutes.getName()).thenReturn("routes.en_GB");
routes.add(multilangRoutes);

VirtualFile confFolder = mock(VirtualFile.class);
when(confFolder.list()).thenReturn(routes);

when(appRoot.child("conf")).thenReturn(confFolder);

assertEquals(1,Play.loadMultilanguageRoutesFiles(appRoot).size());

routes = new ArrayList<>();
VirtualFile routesEnFile = mock(VirtualFile.class);
when(routesEnFile.getName()).thenReturn("routes.en");
routes.add(routesEnFile);
VirtualFile routesRuFile = mock(VirtualFile.class);
when(routesRuFile.getName()).thenReturn("routes.RU_ru");
routes.add(routesRuFile);
routes.add(appConf);
when(confFolder.list()).thenReturn(routes);

assertEquals(2,Play.loadMultilanguageRoutesFiles(appRoot).size());
assertEquals(true,Play.internationalizedRoutes.size()>0);

}

@Test
public void test_detectNoChanges() {
long now = System.currentTimeMillis();

Router.lastLoading = now;

VirtualFile routesNotModifiedFile = mock(VirtualFile.class);
when(routesNotModifiedFile.getName()).thenReturn("routes");
when(routesNotModifiedFile.lastModified()).thenReturn(now-2000);
Play.routes = routesNotModifiedFile;

List<VirtualFile> internationalizedRoutes = new ArrayList<>();
VirtualFile routesNotModifiedFile2 = mock(VirtualFile.class);
when(routesNotModifiedFile2.getName()).thenReturn("routes.en");
when(routesNotModifiedFile2.lastModified()).thenReturn(now-1000);
internationalizedRoutes.add(routesNotModifiedFile2);

VirtualFile routesNotModifiedFile1 = mock(VirtualFile.class);
when(routesNotModifiedFile1.getName()).thenReturn("routes.ru_RU");
when(routesNotModifiedFile1.lastModified()).thenReturn(now);
internationalizedRoutes.add(routesNotModifiedFile1);

Play.internationalizedRoutes = internationalizedRoutes;

HashMap<String, VirtualFile> modulesRoutes = new HashMap<>();
VirtualFile moduleRoute1 = mock(VirtualFile.class);
when(moduleRoute1.lastModified()).thenReturn(now-1000);
VirtualFile moduleRoute2 = mock(VirtualFile.class);
when(moduleRoute2.lastModified()).thenReturn(now);
modulesRoutes.put("1",moduleRoute1);
modulesRoutes.put("2",moduleRoute2);

Play.modulesRoutes=modulesRoutes;

Router.detectChanges("");
}

@Test
public void test_reverseMultiLangRoutes(){
Play.configuration = new Properties();
List<String> applicationLangs = new ArrayList<>();
applicationLangs.add("ru");
applicationLangs.add("fr_FR");
applicationLangs.add("en_GB");
Play.langs=applicationLangs;

Play.internationalizedRoutes = mock(ArrayList.class);
when(Play.internationalizedRoutes.size()).thenReturn(3);

Router.appendRoute("GET","/test/action","testAction","","","conf/routes.en_GB",0);
Router.appendRoute("GET","/test/deistvie","testAction","","","conf/routes.ru",1);
Router.appendRoute("GET","/test/activite","testAction","","","conf/routes.fr_FR",2);
Router.appendRoute("GET","/test/act","testAnotherAction","","","conf/routes.fr_FR",3);
Router.appendRoute("GET","/test/akt","testAnotherAction","","","conf/routes.ru",4);
Router.appendRoute("GET","/test/active","testAnotherAction","","","conf/routes.en_GB",5);

Lang.change("ru");
Router.ActionDefinition testAction = Router.reverse("testAction", new HashMap<String, Object>());
assertEquals("/test/deistvie",testAction.url);

Lang.change("en_GB");
testAction = Router.reverse("testAction", new HashMap<String, Object>());
assertEquals("/test/action",testAction.url);

Lang.change("fr_FR");
testAction = Router.reverse("testAction", new HashMap<String, Object>());
assertEquals("/test/activite",testAction.url);

Router.routes.clear();
Lang.change("en_GB");
Router.appendRoute("GET","/test/do","doAction","","","conf/routes.en_GB",0);
Router.appendRoute("GET","/test/delo","doAction","","","conf/routes.ru",1);
testAction = Router.reverse("doAction", new HashMap<String, Object>());
assertEquals("/test/do",testAction.url);
}

@Test
public void test_routeMultilangActivatesLang(){
Play.configuration = new Properties();
List<String> applicationLangs = new ArrayList<>();
applicationLangs.add("ru");
applicationLangs.add("fr_FR");
applicationLangs.add("en_GB");
Play.langs=applicationLangs;
Play.internationalizedRoutes = mock(ArrayList.class);
when(Play.internationalizedRoutes.size()).thenReturn(3);

Router.appendRoute("GET","/test/action","testAction","","","conf/routes.en_GB",0);
Router.appendRoute("GET","/test/deistvie","testAction","","","conf/routes.ru",1);
Router.appendRoute("GET","/test/activite","testAction","","","conf/routes.fr_FR",2);
Router.appendRoute("GET","/test/act","testAnotherAction","","","conf/routes.fr_FR",3);
Router.appendRoute("GET","/test/akt","testAnotherAction","","","conf/routes.ru",4);
Router.appendRoute("GET","/test/active","testAnotherAction","","","conf/routes.en_GB",5);

Lang.change("en_GB");
assertEquals("en_GB",Lang.get());
Http.Request request = mock(Http.Request.class);
request.method="GET";
request.path="/test/activite";
request.format="text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
request.domain="github.com";
Router.route(request);
assertEquals("fr_FR",Lang.get());
}
}