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