You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by be...@apache.org on 2010/02/01 00:00:37 UTC

svn commit: r905134 - in /couchdb/trunk: etc/couchdb/default.ini.tpl.in share/www/script/couch_tests.js share/www/script/test/rewrite.js src/couchdb/Makefile.am src/couchdb/couch_httpd_rewrite.erl

Author: benoitc
Date: Sun Jan 31 23:00:37 2010
New Revision: 905134

URL: http://svn.apache.org/viewvc?rev=905134&view=rev
Log:
add url rewriting support.

Added:
    couchdb/trunk/share/www/script/test/rewrite.js
    couchdb/trunk/src/couchdb/couch_httpd_rewrite.erl
Modified:
    couchdb/trunk/etc/couchdb/default.ini.tpl.in
    couchdb/trunk/share/www/script/couch_tests.js
    couchdb/trunk/src/couchdb/Makefile.am

Modified: couchdb/trunk/etc/couchdb/default.ini.tpl.in
URL: http://svn.apache.org/viewvc/couchdb/trunk/etc/couchdb/default.ini.tpl.in?rev=905134&r1=905133&r2=905134&view=diff
==============================================================================
--- couchdb/trunk/etc/couchdb/default.ini.tpl.in (original)
+++ couchdb/trunk/etc/couchdb/default.ini.tpl.in Sun Jan 31 23:00:37 2010
@@ -87,6 +87,7 @@
 _show = {couch_httpd_show, handle_doc_show_req}
 _list = {couch_httpd_show, handle_view_list_req}
 _info = {couch_httpd_db,   handle_design_info_req}
+_rewrite = {couch_httpd_rewrite, handle_rewrite_req}
 _update = {couch_httpd_show, handle_doc_update_req}
 
 [uuids]

Modified: couchdb/trunk/share/www/script/couch_tests.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch_tests.js?rev=905134&r1=905133&r2=905134&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch_tests.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/couch_tests.js [utf-8] Sun Jan 31 23:00:37 2010
@@ -70,6 +70,7 @@
 loadTest("reduce_false_temp.js");
 loadTest("replication.js");
 loadTest("rev_stemming.js");
+loadTest("rewrite.js");
 loadTest("security_validation.js");
 loadTest("show_documents.js");
 loadTest("stats.js");

Added: couchdb/trunk/share/www/script/test/rewrite.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/rewrite.js?rev=905134&view=auto
==============================================================================
--- couchdb/trunk/share/www/script/test/rewrite.js (added)
+++ couchdb/trunk/share/www/script/test/rewrite.js Sun Jan 31 23:00:37 2010
@@ -0,0 +1,241 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+ 
+ 
+ 
+couchTests.rewrite = function(debug) {
+  // this test _rewrite handler
+  
+  
+  var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"});
+  db.deleteDb();
+  db.createDb();
+  
+  
+  if (debug) debugger;
+  run_on_modified_server(
+    [{section: "httpd",
+      key: "authentication_handlers",
+      value: "{couch_httpd_auth, special_test_authentication_handler}"},
+     {section:"httpd",
+      key: "WWW-Authenticate",
+      value: "X-Couch-Test-Auth"}],
+      
+      function(){
+        var designDoc = {
+          _id:"_design/test",
+          language: "javascript",
+           _attachments:{
+              "foo.txt": {
+                content_type:"text/plain",
+                data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
+              }
+            },
+          rewrites: [
+            {
+              "from": "foo",
+              "to": "foo.txt"
+            },
+            {
+              "from": "foo2",
+              "to": "foo.txt",
+              "method": "GET"
+            },
+            {
+              "from": "hello/:id",
+              "to": "_update/hello/:id",
+              "method": "PUT"
+            },
+            {
+              "from": "/welcome",
+              "to": "_show/welcome"
+            },
+            {
+              "from": "/welcome/:name",
+              "to": "_show/welcome",
+              "query": {
+                "name": ":name"
+              }
+            },
+            {
+              "from": "/welcome2/:name",
+              "to": "_update/welcome2/:name",
+              "method": "PUT"
+            },
+            {
+              "from": "/welcome2/:name",
+              "to": "_show/welcome2/:name",
+              "method": "GET"
+            },
+            {
+              "from": "simpleForm/basicView",
+              "to": "_list/simpleForm/basicView",
+            },
+            {
+              "from": "simpleForm/basicViewFixed",
+              "to": "_list/simpleForm/basicView",
+              "query": {
+                "startkey": 3,
+                "endkey": 8
+              }
+            },
+            {
+              "from": "simpleForm/basicViewPath/:start/:end",
+              "to": "_list/simpleForm/basicView",
+              "query": {
+                "startkey": ":start",
+                "endkey": ":end"
+              }
+            }
+            
+          ],
+          lists: {
+            simpleForm: stringFun(function(head, req) {
+              log("simpleForm");
+              send('<ul>');
+              var row, row_number = 0, prevKey, firstKey = null;
+              while (row = getRow()) {
+                row_number += 1;
+                if (!firstKey) firstKey = row.key;
+                prevKey = row.key;
+                send('\n<li>Key: '+row.key
+                +' Value: '+row.value
+                +' LineNo: '+row_number+'</li>');
+              }
+              return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>';
+            })
+          },
+          shows: {
+            "welcome": stringFun(function(doc,req) {
+              return "Welcome " + req.query["name"];
+            }),
+            "welcome2": stringFun(function(doc, req) {
+              return "Welcome " + doc.name;
+            }),
+          },
+          updates: {
+            "hello" : stringFun(function(doc, req) {
+              if (!doc) {
+                if (req.id) {
+                  return [{
+                    _id : req.id
+                  }, "New World"]
+                }
+                return [null, "Empty World"];
+              }
+              doc.world = "hello";
+              doc.edited_by = req.userCtx;
+              return [doc, "hello doc"];
+            }),
+            "welcome2": stringFun(function(doc, req) {
+              if (!doc) {
+                if (req.id) {
+                  return [{
+                    _id: req.id,
+                    name: req.id
+                  }, "New World"]
+                }
+                return [null, "Empty World"];
+              }
+              return [doc, "hello doc"];
+            })
+          },
+          views : {
+            basicView : {
+              map : stringFun(function(doc) {
+                emit(doc.integer, doc.string);
+              })
+            }
+          }
+        }
+ 
+        db.save(designDoc);
+        
+        var docs = makeDocs(0, 10);
+        db.bulkSave(docs);
+ 
+        // test simple rewriting
+ 
+        req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo");
+        T(req.responseText == "This is a base64 encoded text");
+        T(req.getResponseHeader("Content-Type") == "text/plain");
+        
+        req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/foo2");
+        T(req.responseText == "This is a base64 encoded text");
+        T(req.getResponseHeader("Content-Type") == "text/plain");
+        
+       
+        // test POST
+        // hello update world
+        
+        var doc = {"word":"plankton", "name":"Rusty"}
+        var resp = db.save(doc);
+        T(resp.ok);
+        var docid = resp.id;
+        
+        xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/hello/"+docid);
+        T(xhr.status == 201);
+        T(xhr.responseText == "hello doc");
+        T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type")))
+ 
+        doc = db.open(docid);
+        T(doc.world == "hello");
+        
+        xhr = CouchDB.request("PUT", "/test_suite_db/_design/test/_rewrite/welcome2/test");
+        T(xhr.status == 201);
+        T(xhr.responseText == "New World");
+        T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type")));
+        
+        xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome2/test");
+        T(xhr.responseText == "Welcome test");
+        
+        req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome?name=user");
+        T(req.responseText == "Welcome user");
+        
+        req = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/welcome/user");
+        T(req.responseText == "Welcome user");
+        
+        
+        
+        
+        // get with query params
+        xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicView?startkey=3&endkey=8");
+        T(xhr.status == 200, "with query params");
+        T(!(/Key: 1/.test(xhr.responseText)));
+        T(/FirstKey: 3/.test(xhr.responseText));
+        T(/LastKey: 8/.test(xhr.responseText));
+        
+        xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed");
+        T(xhr.status == 200, "with query params");
+        T(!(/Key: 1/.test(xhr.responseText)));
+        T(/FirstKey: 3/.test(xhr.responseText));
+        T(/LastKey: 8/.test(xhr.responseText));
+        
+        // get with query params
+        xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewFixed?startkey=4");
+        T(xhr.status == 200, "with query params");
+        T(!(/Key: 1/.test(xhr.responseText)));
+        T(/FirstKey: 3/.test(xhr.responseText));
+        T(/LastKey: 8/.test(xhr.responseText));
+        
+        // get with query params
+        xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_rewrite/simpleForm/basicViewPath/3/8");
+        T(xhr.status == 200, "with query params");
+        T(!(/Key: 1/.test(xhr.responseText)));
+        T(/FirstKey: 3/.test(xhr.responseText));
+        T(/LastKey: 8/.test(xhr.responseText));
+        
+        
+        
+  });
+  
+}
\ No newline at end of file

Modified: couchdb/trunk/src/couchdb/Makefile.am
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/Makefile.am?rev=905134&r1=905133&r2=905134&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/Makefile.am (original)
+++ couchdb/trunk/src/couchdb/Makefile.am Sun Jan 31 23:00:37 2010
@@ -48,6 +48,7 @@
     couch_httpd_show.erl \
     couch_httpd_view.erl \
     couch_httpd_misc_handlers.erl \
+	couch_httpd_rewrite.erl \
     couch_httpd_stats_handlers.erl \
     couch_key_tree.erl \
     couch_log.erl \
@@ -103,6 +104,7 @@
     couch_httpd_show.beam \
     couch_httpd_view.beam \
     couch_httpd_misc_handlers.beam \
+	couch_httpd_rewrite.beam \
     couch_httpd_stats_handlers.beam \
     couch_key_tree.beam \
     couch_log.beam \

Added: couchdb/trunk/src/couchdb/couch_httpd_rewrite.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_rewrite.erl?rev=905134&view=auto
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_rewrite.erl (added)
+++ couchdb/trunk/src/couchdb/couch_httpd_rewrite.erl Sun Jan 31 23:00:37 2010
@@ -0,0 +1,389 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+%
+% bind_path is based on bind method from Webmachine
+
+
+%% @doc Module for URL rewriting by pattern matching.
+
+-module(couch_httpd_rewrite).
+-export([handle_rewrite_req/3]).
+-include("couch_db.hrl").
+
+-define(SEPARATOR, $\/).
+-define(MATCH_ALL, '*').
+
+
+%% doc The http rewrite handler. All rewriting is done from 
+%% /dbname/_design/ddocname/_rewrite by default.
+%%
+%% each rules should be in rewrites member of the design doc.
+%% Ex of a complete rule :
+%%
+%%  { 
+%%      .... 
+%%      "rewrite": [ 
+%%      { 
+%%          "from": "", 
+%%          "to": "index.html", 
+%%          "method": "GET", 
+%%          "query": {} 
+%%      } 
+%%      ] 
+%%  } 
+%%
+%%  from: is the path rule used to bind current uri to the rule. It 
+%% use pattern matching for that.
+%%
+%%  to: rule to rewrite an url. It can contain variables depending on binding
+%% variables discovered during pattern matching and query args (url args and from
+%% the query member.)
+%%
+%%  method: method to bind the request method to the rule. by default "*"
+%%  query: query args you want to define they can contain dynamic variable
+%% by binding the key to the bindings
+%% 
+%%
+%% to and from are path with  patterns. pattern can be string starting with ":" or
+%% "*". ex:
+%% /somepath/:var/*
+%%
+%% This path are converted in erlang list by splitting "/". Each var are 
+%% converted in atom. "*" is converted to '*' atom. The pattern matching is done
+%% by splitting "/" in request url in a list of token. A string pattern will 
+%% match equal token. The star atom ('*' in single quotes) will match any number 
+%% of tokens, but may only be present as the last pathtern in a pathspec. If all 
+%% tokens are matched and all pathterms are used, then the pathspec matches. It works
+%% like webmachine. Each identified token will be reused in to rule and in query
+%%
+%% The pattern matching is done by first matching the request method to a rule. by 
+%% default all methods match a rule. (method is equal to "*" by default). Then
+%% It will try to match the path to one rule. If no rule match, then a 404 error 
+%% is displayed.
+%% 
+%% Once a rule is found we rewrite the request url using the "to" and
+%% "query" members. The identified token are matched to the rule and 
+%% will replace var. if '*' is found in the rule it will contain the remaining
+%% part if it exists.
+%%
+%% Examples:
+%%
+%% Dispatch rule            URL             TO                  Tokens
+%%
+%% {"from": "/a/b",         /a/b?k=v        /some/b?k=v         var =:= b
+%% "to": "/some/"}                                              k = v
+%%
+%% {"from": "/a/b",         /a/b            /some/b?var=b       var =:= b
+%% "to": "/some/:var"}          
+%%
+%% {"from": "/a",           /a              /some  
+%% "to": "/some/*"}
+%%
+%% {"from": "/a/*",         /a/b/c          /some/b/c  
+%% "to": "/some/*"}
+%%
+%% {"from": "/a",           /a              /some  
+%% "to": "/some/*"}
+%%
+%% {"from": "/a/:foo/*",    /a/b/c          /some/b/c?foo=b     foo =:= b
+%% "to": "/some/:foo/*"}
+%%
+%% {"from": "/a/:foo",     /a/b             /some/?k=b&foo=b    foo =:= b
+%% "to": "/some",
+%%  "query": { 
+%%      "k": ":foo"
+%%  }}
+%%
+%% {"from": "/a",           /a?foo=b        /some/b             foo =:= b
+%% "to": "/some/:foo",
+%%  }}
+
+
+
+handle_rewrite_req(#httpd{
+        path_parts=[DbName, <<"_design">>, DesignName, _Rewrite|PathParts],
+        method=Method,
+        mochi_req=MochiReq}=Req, _Db, DDoc) ->
+    
+    % we are in a design handler
+    DesignId = <<"_design/", DesignName/binary>>,
+    Prefix = <<"/", DbName/binary, "/", DesignId/binary>>,
+    QueryList = couch_httpd:qs(Req),
+    
+    #doc{body={Props}} = DDoc,
+    
+    % get rules from ddoc
+    case proplists:get_value(<<"rewrites">>, Props) of
+        undefined ->
+            couch_httpd:send_error(Req, 404, <<"rewrite_error">>, 
+                            <<"Invalid path.">>);
+        Rules ->
+            % create dispatch list from rules
+            DispatchList =  [make_rule(Rule) || {Rule} <- Rules],
+            
+            %% get raw path by matching url to a rule.
+            RawPath = case try_bind_path(DispatchList, Method, PathParts, 
+                                    QueryList) of
+                no_dispatch_path ->
+                    throw(not_found);
+                {NewPathParts, Bindings} -> 
+                    Parts = [mochiweb_util:quote_plus(X) || X <- NewPathParts],
+                    
+                    % build new path, reencode query args, eventually convert 
+                    % them to json
+                    Path = lists:append(
+                        string:join(Parts, [?SEPARATOR]),
+                        case Bindings of
+                            [] -> [];
+                            _ -> [$?, encode_query(Bindings)]
+                        end),
+                    
+                    % if path is relative detect it and rewrite path
+                    case mochiweb_util:safe_relative_path(Path) of
+                        undefined -> 
+                            ?b2l(Prefix) ++ "/" ++ Path;
+                        P1 -> 
+                            ?b2l(Prefix) ++ "/" ++ P1
+                    end
+                   
+                end,
+
+            % normalize final path (fix levels "." and "..")
+            RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))),
+            
+            %% get path parts, needed for CouchDB dispatching
+            {"/" ++ NewPath2, _, _} = mochiweb_util:urlsplit_path(RawPath1),
+            NewPathParts1 = [?l2b(couch_httpd:unquote(Part))
+                            || Part <- string:tokens(NewPath2, "/")],
+                            
+            ?LOG_DEBUG("rewrite to ~p ~n", [RawPath1]),
+
+            % build a new mochiweb request
+            MochiReq1 = mochiweb_request:new(MochiReq:get(socket),
+                                             MochiReq:get(method),
+                                             RawPath1,
+                                             MochiReq:get(version),
+                                             MochiReq:get(headers)),
+                                             
+            % cleanup, It force mochiweb to reparse raw uri.
+            MochiReq1:cleanup(),
+                        
+            % send to couchdb the rewritten request
+            couch_httpd_db:handle_request(Req#httpd{
+                        path_parts=NewPathParts1,
+                        mochi_req=MochiReq1})
+        end.
+            
+
+
+%% @doc Try to find a rule matching current url. If none is found
+%% 404 error not_found is raised
+try_bind_path([], _Method, _PathParts, _QueryList) ->
+    no_dispatch_path;        
+try_bind_path([Dispatch|Rest], Method, PathParts, QueryList) ->
+    [{PathParts1, Method1}, RedirectPath, QueryArgs] = Dispatch,
+    case bind_method(Method1, Method) of
+        true ->
+            case bind_path(PathParts1, PathParts, []) of
+                {ok, Remaining, Bindings} ->
+                    Bindings1 = Bindings ++ QueryList,
+                    
+                    % we parse query args from the rule and fill 
+                    % it eventually with bindings vars                    
+                    QueryArgs1 = make_query_list(QueryArgs, Bindings1, []),
+
+                    % remove params in QueryLists1 that are already in 
+                    % QueryArgs1
+                    Bindings2 = lists:foldl(fun({K, V}, Acc) ->
+                        K1 = to_atom(K),
+                        KV = case proplists:get_value(K1, QueryArgs1) of
+                            undefined -> [{K1, V}];
+                            _V1 -> []
+                        end,
+                        Acc ++ KV
+                    end, [], Bindings1),
+
+                    FinalBindings = Bindings2 ++ QueryArgs1,
+                    NewPathParts = make_new_path(RedirectPath, FinalBindings, 
+                                    Remaining, []),                                    
+                    {NewPathParts, FinalBindings};         
+                fail ->
+                    try_bind_path(Rest, Method, PathParts, QueryList)
+            end;    
+        false ->
+            try_bind_path(Rest, Method, PathParts, QueryList)
+    end.
+    
+%% rewriting dynamically the quey list given as query member in 
+%% rewrites. Each value is replaced by one binding or an argument
+%% passed in url.
+make_query_list([], _Bindings, Acc) ->
+    Acc;
+make_query_list([{Key, {Value}}|Rest], Bindings, Acc) ->
+    Value1 = make_query_list(Value, Bindings, []),
+    make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]);
+make_query_list([{Key, Value}|Rest], Bindings, Acc) when is_binary(Value) ->
+    Value1 = replace_var(Value, Bindings),
+    make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]);
+make_query_list([{Key, Value}|Rest], Bindings, Acc) when is_list(Value) ->
+
+    Value1 = replace_vars(Value, Bindings, []),
+    make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]);
+make_query_list([{Key, Value}|Rest], Bindings, Acc) ->
+    make_query_list(Rest, Bindings, [{to_atom(Key), Value}|Acc]).
+    
+replace_vars([], _Bindings, Acc) ->
+    lists:reverse(Acc);
+replace_vars([V|R], Bindings, Acc) ->
+    V1 = replace_var(V, Bindings),
+    replace_vars(R, Bindings, [V1|Acc]).
+    
+replace_var(Value, Bindings) ->
+    case Value of
+        <<":", Var/binary>> ->
+            Var1 = list_to_atom(binary_to_list(Var)),
+            proplists:get_value(Var1, Bindings, Value);
+        _ -> Value
+    end.
+
+
+%% doc: build new patch from bindings. bindings are query args
+%% (+ dynamic query rewritten if needed) and bindings found in
+%% bind_path step.
+make_new_path([], _Bindings, _Remaining, Acc) ->
+    lists:reverse(Acc);
+make_new_path([?MATCH_ALL], _Bindings, Remaining, Acc) ->
+    Acc1 = lists:reverse(Acc) ++ Remaining,
+    Acc1;
+make_new_path([?MATCH_ALL|_Rest], _Bindings, Remaining, Acc) ->
+    Acc1 = lists:reverse(Acc) ++ Remaining,
+    Acc1;
+make_new_path([P|Rest], Bindings, Remaining, Acc) when is_atom(P) ->
+    P2 = case proplists:get_value(P, Bindings) of 
+        undefined -> << "undefined">>;
+        P1 -> P1
+    end,
+    make_new_path(Rest, Bindings, Remaining, [P2|Acc]);
+make_new_path([P|Rest], Bindings, Remaining, Acc) ->
+    make_new_path(Rest, Bindings, Remaining, [P|Acc]).
+            
+
+%% @doc If method of the query fith the rule method. If the 
+%% method rule is '*', which is the default, all
+%% request method will bind. It allows us to make rules
+%% depending on HTTP method.
+bind_method(?MATCH_ALL, _Method) ->
+    true;
+bind_method(Method, Method) ->
+    true;
+bind_method(_, _) ->
+    false. 
+
+
+%% @doc bind path. Using the rule from we try to bind variables given
+%% to the current url by pattern matching
+bind_path([], [], Bindings) ->
+    {ok, [], Bindings};
+bind_path([?MATCH_ALL], Rest, Bindings) when is_list(Rest) ->
+    {ok, Rest, Bindings};
+bind_path(_, [], _) ->
+    fail;
+bind_path([Token|RestToken],[Match|RestMatch],Bindings) when is_atom(Token) ->
+    bind_path(RestToken, RestMatch, [{Token, Match}|Bindings]);
+bind_path([Token|RestToken], [Token|RestMatch], Bindings) ->
+    bind_path(RestToken, RestMatch, Bindings);
+bind_path(_, _, _) ->
+    fail.
+    
+
+%% normalize path.
+normalize_path(Path)  ->
+    "/" ++ string:join(normalize_path1(string:tokens(Path, 
+                "/"), []), [?SEPARATOR]).
+    
+    
+normalize_path1([], Acc) ->
+    lists:reverse(Acc);
+normalize_path1([".."|Rest], Acc) ->
+    Acc1 = case Acc of
+        [] -> [".."|Acc];
+        [T|_] when T =:= ".." -> [".."|Acc];
+        [_|R] -> R
+    end,
+    normalize_path1(Rest, Acc1);
+normalize_path1(["."|Rest], Acc) ->
+    normalize_path1(Rest, Acc);
+normalize_path1([Path|Rest], Acc) ->
+    normalize_path1(Rest, [Path|Acc]).
+
+ 
+%% @doc transform json rule in erlang for pattern matching   
+make_rule(Rule) ->
+    Method = case proplists:get_value(<<"method">>, Rule) of
+        undefined -> '*';
+        M -> list_to_atom(?b2l(M))
+    end,
+    QueryArgs = case proplists:get_value(<<"query">>, Rule) of
+        undefined -> [];
+        {Args} -> Args
+        end,
+    FromParts  = case proplists:get_value(<<"from">>, Rule) of
+        undefined -> ['*'];
+        From ->
+            parse_path(From)
+        end,
+    ToParts  = case proplists:get_value(<<"to">>, Rule) of
+        undefined ->  
+            throw({error, invalid_rewrite_target});
+        To ->
+            parse_path(To)
+        end,
+    [{FromParts, Method}, ToParts, QueryArgs].
+    
+parse_path(Path) ->
+    {ok, SlashRE} = re:compile(<<"\\/">>),
+    path_to_list(re:split(Path, SlashRE), []).
+    
+%% @doc convert a path rule (from or to) to an erlang list
+%% * and path variable starting by ":" are converted
+%% in erlang atom.
+path_to_list([], Acc) ->
+    lists:reverse(Acc);
+path_to_list([<<>>|R], Acc) ->
+    path_to_list(R, Acc);
+path_to_list([<<"*">>|R], Acc) ->
+    path_to_list(R, [?MATCH_ALL|Acc]);
+path_to_list([P|R], Acc) ->
+    P1 = case P of
+        <<":", Var/binary>> ->
+            list_to_atom(binary_to_list(Var));
+        _ -> P
+    end,
+    path_to_list(R, [P1|Acc]).
+
+encode_query(Props) ->
+    RevPairs = lists:foldl(fun ({K, V}, Acc) ->
+        V1 = case is_list(V) of
+            true -> V;
+            false ->
+                mochiweb_util:quote_plus(V)
+        end,              
+        [{K, V1} | Acc]
+    end, [], Props),
+    lists:flatten(mochiweb_util:urlencode(RevPairs)).
+
+to_atom(V) when is_atom(V) ->
+    V; 
+to_atom(V) when is_binary(V) ->
+    to_atom(?b2l(V));
+to_atom(V) ->
+    list_to_atom(V).