View Javadoc
1   /*
2    * Copyright (C) 2003-2012 eXo Platform SAS.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program. If not, see <http://www.gnu.org/licenses/>.
16   */
17  package org.exoplatform.social.common.router;
18  
19  import java.io.UnsupportedEncodingException;
20  import java.net.URLEncoder;
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.concurrent.CopyOnWriteArrayList;
26  import java.util.regex.Pattern;
27  
28  import org.exoplatform.services.log.ExoLogger;
29  import org.exoplatform.services.log.Log;
30  import org.exoplatform.social.common.router.regex.ExoMatcher;
31  import org.exoplatform.social.common.router.regex.ExoPattern;
32  import org.picocontainer.Startable;
33  
34  public class ExoRouter implements Startable {
35  
36    /**
37     * The Logger.
38     */
39    private static final Log LOG = ExoLogger.getLogger(ExoRouter.class);
40    
41    static ExoPattern defaultRoutePattern = ExoPattern.compile("^({path}.*/[^\\s]*)\\s+({action}[^\\s(]+)({params}.+)?(\\s*)$");
42  
43    /**
44     * All the loaded routes.
45     */
46    public static List<Route> routes = new CopyOnWriteArrayList<Route>();
47  
48    public static void reset() {
49      routes.clear();
50    }
51    
52    public ExoRouter() {}
53  
54    public void addRoutes(ExoRouterConfig routerConfig) {
55      Map<String, String> routeMapping = routerConfig.getRouteMapping();
56      
57      for(Map.Entry<String, String> entry : routeMapping.entrySet()) {
58        addRoute(entry.getValue(), entry.getKey());
59      }
60    }
61    
62    /**
63     * Add new route which loaded from route configuration file.
64     * 
65     * @param path /{pageID}/ForumService
66     * @param action the action which appends to patch after "ForumService"
67     *          string.
68     */
69    public static void addRoute(String path, String action) {
70      addRoute(path, action, null);
71    }
72  
73    /**
74     * Add new route which loaded from route configuration file.
75     * 
76     * @param path /{pageID}/ForumService
77     * @param action /{pageID}/ForumService
78     * @param params the action which appends to patch after "ForumService" string
79     *          ex: {@literal /{pageID}/{ForumService|}/{action} =>/{pageID}/ForumService/{}}
80     */
81    public static void addRoute(String path, String action, String params) {
82      appendRoute(path, action, params);
83    }
84  
85    public static void appendRoute(String path, String action, String params) {
86      int position = routes.size();
87      routes.add(position, getRoute(path, action, params));
88    }
89  
90    public static Route getRoute(String path, String action, String params) {
91      return getRoute(path, action, params, null, 0);
92    }
93  
94    public static Route getRoute(String path, String action) {
95      return getRoute(path, action, null, null, 0);
96    }
97  
98    public static Route getRoute(String path, String action, String params, String sourceFile, int line) {
99      Route route = new Route();
100     route.path = path.replace("//", "/");
101     route.action = action;
102     route.routesFile = sourceFile;
103     route.routesFileLine = line;
104     route.addParams(params);
105     route.compute();
106     return route;
107   }
108 
109   /**
110    * Add a new route at the beginning of the route list
111    */
112   public static void prependRoute(String path, String action, String params) {
113     routes.add(0, getRoute(path, action, params));
114   }
115 
116   /**
117    * Add a new route at the beginning of the route list
118    */
119   public static void prependRoute(String path, String action) {
120     routes.add(0, getRoute(path, action));
121   }
122 
123   public static Route route(String path) {
124     for (Route route : routes) {
125       Map<String, String> args = route.matches(path);
126       if (args != null) {
127         route.localArgs = args;
128         return route;
129       }
130     }
131 
132     return null;
133   }
134 
135   /**
136    * Generates ActionBuilder base on the action name and arguments list.
137    * Example: invokes {@code reverse("show.topic", new HashMap<String, Object>{topicId, "topicId321"})} method.
138    *           
139    * @param action
140    * @param args
141    * @return
142    */
143   public static ActionBuilder reverse(String action, Map<String, Object> args) {
144     Map<String, Object> argsbackup = new HashMap<String, Object>(args);
145     // Add routeArgs
146     for (Route route : routes) {
147       if (route.actionPattern != null) {
148         ExoMatcher matcher = route.actionPattern.matcher(action);
149         if (matcher.matches()) {
150           for (String group : route.actionArgs) {
151             String v = matcher.group(group);
152             if (v == null) {
153               continue;
154             }
155             args.put(group, v.toLowerCase());
156           }
157           List<String> inPathArgs = new ArrayList<String>(16);
158           boolean allRequiredArgsAreHere = true;
159 
160           for (Route.ParamArg arg : route.args) {
161             inPathArgs.add(arg.name);
162             Object value = args.get(arg.name);
163             if (value != null) {
164               if (!value.toString().startsWith(":") && !arg.constraint.matches(value.toString())) {
165                 allRequiredArgsAreHere = false;
166                 break;
167               }
168             }
169           }
170           if (allRequiredArgsAreHere) {
171             StringBuilder queryString = new StringBuilder();
172             String path = route.path;
173             if (path.endsWith("/?")) {
174               path = path.substring(0, path.length() - 2);
175             }
176             for (Map.Entry<String, Object> entry : args.entrySet()) {
177               String key = entry.getKey();
178               Object value = entry.getValue();
179               if (inPathArgs.contains(key) && value != null) {
180                 path = path.replaceAll("\\{(<[^>]+>)?" + key + "\\}", value.toString().replace("$", "\\$").replace("%3A", ":").replace("%40", "@"));
181               } else if (value != null) {
182                 try {
183                   queryString.append(URLEncoder.encode(key, "UTF-8"));
184                   queryString.append("=");
185                   if (value.toString().startsWith(":")) {
186                     queryString.append(value.toString());
187                   } else {
188                     queryString.append(URLEncoder.encode(value.toString() + "", "UTF-8"));
189                   }
190                   queryString.append("&");
191                 } catch (UnsupportedEncodingException ex) {
192                   LOG.debug("Unsupported encoding error: " + ex);
193                 }
194 
195               }
196             }
197             String qs = queryString.toString();
198             if (qs.endsWith("&")) {
199               qs = qs.substring(0, qs.length() - 1);
200             }
201             ActionBuilder actionDefinition = new ActionBuilder();
202             actionDefinition.url = qs.length() == 0 ? path : path + "?" + qs;
203             actionDefinition.action = action;
204             actionDefinition.args = argsbackup;
205             return actionDefinition;
206           }
207         }
208       }
209     }
210     return null;
211   }
212 
213   public static class ActionBuilder {
214     public String url;
215 
216     /**
217      * TODO - what is this? does it include the class and package?
218      */
219     public String action;
220 
221     /**
222      * TODO - are these the required args in the routing file, or the query
223      *       string in a request?
224      */
225     public Map<String, Object> args;
226 
227     public ActionBuilder add(String key, Object value) {
228       args.put(key, value);
229       return reverse(action, args);
230     }
231 
232     public ActionBuilder remove(String key) {
233       args.remove(key);
234       return reverse(action, args);
235     }
236 
237     public ActionBuilder addRef(String fragment) {
238       url += "#" + fragment;
239       return this;
240     }
241 
242     @Override
243     public String toString() {
244       return url;
245     }
246 
247   }
248   
249   /**
250    * Route class which contains path, action and argument list.
251    * 
252    * @author thanhvc
253    *
254    */
255   public static class Route {
256 
257     public String path;
258 
259     public String action;
260 
261     ExoPattern actionPattern;
262 
263     List<String> actionArgs = new ArrayList<String>(3);
264 
265     ExoPattern pattern;
266 
267     public String routesFile;
268 
269     List<ParamArg> args = new ArrayList<ParamArg>(3);
270 
271     Map<String, String> staticArgs = new HashMap<String, String>(3);
272 
273     public Map<String, String> localArgs = null;
274 
275     public int routesFileLine;
276 
277     static ExoPattern customRegexPattern =  ExoPattern.compile("\\{([a-zA-Z_][a-zA-Z_0-9]*)\\}");
278 
279     static ExoPattern argsPattern = ExoPattern.compile("\\{<([^>]+)>([a-zA-Z_0-9]+)\\}");
280 
281     static ExoPattern paramPattern = ExoPattern.compile("([a-zA-Z_0-9]+):'(.*)'");
282 
283     public void compute() {
284       String patternString = this.path;
285       patternString = customRegexPattern.matcher(patternString).replaceAll("\\{<[^/]+>$1\\}");
286       ExoMatcher matcher = argsPattern.matcher(patternString);
287       while (matcher.find()) {
288         ParamArg arg = new ParamArg();
289         arg.name = matcher.group(2);
290         arg.constraint = ExoPattern.compile(matcher.group(1));
291         args.add(arg);
292       }
293 
294       patternString = argsPattern.matcher(patternString).replaceAll("({$2}$1)");
295       this.pattern = ExoPattern.compile(patternString);
296       // Action pattern
297       patternString = action;
298       patternString = patternString.replace(".", "[.]");
299       for (ParamArg arg : args) {
300         if (patternString.contains("{" + arg.name + "}")) {
301           patternString = patternString.replace("{" + arg.name + "}", "({" + arg.name + "}" + arg.constraint.toString() + ")");
302           actionArgs.add(arg.name);
303         }
304       }
305       actionPattern = ExoPattern.compile(patternString, Pattern.CASE_INSENSITIVE);
306     }
307 
308     public void addParams(String params) {
309       if (params == null || params.length() < 1) {
310         return;
311       }
312       params = params.substring(1, params.length() - 1);
313       for (String param : params.split(",")) {
314         ExoMatcher matcher = paramPattern.matcher(param);
315         if (matcher.matches()) {
316           staticArgs.put(matcher.group(1), matcher.group(2));
317         } else {
318           LOG.warn("Ignoring %s (static params must be specified as key:'value',...)");
319         }
320       }
321     }
322     /**
323      * Base on defined Pattern, when provided URI path, 
324      * this method will extract all of parameters path value 
325      * in given path which reflects in defined Pattern
326      * 
327      * Example: 
328      * defined Pattern = "/{pageID}/topic/{topicID}"
329      * invokes:: matches("1256/topic/topic544343");
330      * result: {@code Map<String, String> = {"pageID" -> "1256"}, {"topicID" -> "topic544343"}}
331      * 
332      * @param path : given URI path
333      * @return
334      */
335     public Map<String, String> matches(String path) {
336       ExoMatcher matcher = pattern.matcher(path);
337       if (matcher.matches()) {
338         Map<String, String> localArgs = new HashMap<String, String>();
339         for (ParamArg arg : args) {
340           if (arg.defaultValue == null) {
341             localArgs.put(arg.name, matcher.group(arg.name));
342           }
343         }
344         return localArgs;
345       }
346 
347       return null;
348     }
349 
350     static class ParamArg {
351       String name;
352 
353       ExoPattern constraint;
354 
355       String defaultValue;
356     }
357   }
358 
359   @Override
360   public void start() {
361     
362   }
363 
364   @Override
365   public void stop() {
366     
367   }
368 
369 }