diff --git a/framework/pym/play/application.py b/framework/pym/play/application.py index 9d5a4cb94a..04b2ef7e3e 100644 --- a/framework/pym/play/application.py +++ b/framework/pym/play/application.py @@ -4,6 +4,7 @@ import re import shutil import socket +import glob from play.utils import * @@ -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." diff --git a/framework/src/play/Play.java b/framework/src/play/Play.java index 8ee0b4d886..a83efd1dcf 100644 --- a/framework/src/play/Play.java +++ b/framework/src/play/Play.java @@ -115,6 +115,12 @@ public boolean isProd() { * Main routes file */ public static VirtualFile routes; + + /** + * Main routes file + */ + public static List internationalizedRoutes; + /** * Plugin routes files */ @@ -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); @@ -321,6 +328,17 @@ public static void init(File root, String id) { Play.initialized = true; } + public static List loadMultilanguageRoutesFiles(VirtualFile appRoot) { + List routes = new ArrayList(); + 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 { diff --git a/framework/src/play/mvc/Router.java b/framework/src/play/mvc/Router.java index b3577fcc01..d6e72bd946 100644 --- a/framework/src/play/mvc/Router.java +++ b/framework/src/play/mvc/Router.java @@ -1,26 +1,15 @@ 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; @@ -28,6 +17,15 @@ 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 */ @@ -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(); @@ -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 + "]"); @@ -237,6 +239,12 @@ public static void detectChanges(String prefix) { } } } + for (VirtualFile route : Play.internationalizedRoutes) { + if (route.lastModified() > lastLoading) { + load(prefix); + return; + } + } } /** @@ -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; } } @@ -588,6 +599,9 @@ private static List getActionRoutes(String action) { matchingRoutes = findActionRoutes(action); actionRoutesCache.put(action, matchingRoutes); } + if(CollectionUtils.isNotEmpty(Play.internationalizedRoutes)){ + matchingRoutes = prioritizeActionRoutesBasedOnActiveLocale(matchingRoutes); + } return matchingRoutes; } @@ -614,6 +628,28 @@ private static List 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 prioritizeActionRoutesBasedOnActiveLocale(List matchingRoutes) { + if(matchingRoutes.size()==0) return matchingRoutes; + final String locale = Lang.get(); + if(StringUtils.isEmpty(locale)) return matchingRoutes; + List sortedMatchingRoutes = new ArrayList<>(matchingRoutes); + sortedMatchingRoutes.sort(new Comparator() { + @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 args = new HashMap<>(2); @@ -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 = ""; @@ -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]; + } + } } } diff --git a/framework/test-src/play/mvc/RouterTest.java b/framework/test-src/play/mvc/RouterTest.java index 268f3946c6..91b454f111 100644 --- a/framework/test-src/play/mvc/RouterTest.java +++ b/framework/test-src/play/mvc/RouterTest.java @@ -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() { @@ -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 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 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 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 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()); + assertEquals("/test/deistvie",testAction.url); + + Lang.change("en_GB"); + testAction = Router.reverse("testAction", new HashMap()); + assertEquals("/test/action",testAction.url); + + Lang.change("fr_FR"); + testAction = Router.reverse("testAction", new HashMap()); + 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()); + assertEquals("/test/do",testAction.url); + } + + @Test + public void test_routeMultilangActivatesLang(){ + Play.configuration = new Properties(); + List 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()); + } }