Source: src/main/javascript/oracle/oj/ojrouter/Router.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2014, Oracle and/or its affiliates.
 * All rights reserved.
 */

/*jslint browser: true*/
/*global oj, ko, Promise, signals */

/**
 * The ojRouter module.
 */

// Wrap in a IIFE to prevents the possiblity of collision in a non-AMD scenario.
(function() {
"use strict";
/**
 * Hold the base URL.
 * static
 * @private
 * @type {string}
 */
var _ojBaseUrl;
/**
 * Hold the base path name.
 * @private
 * @type {string}
 */
var _basePathName;
/**
 * Hold the current page, usually 'index.html'
 * @private
 * @const
 * @type {string}
 */
var _thisPage = getThisPage();
/**
 * Hold the url adapter to be used.
 * @private
 * @type {Object}
 */
var _urlAdapter;

/**
 * The name of the request param for bookmarkable data.
 * @private
 * @const
 * @type {string}
 */
var _ROUTER_PARAM = 'oj_Router=';

/**
 * Maximum size of URL
 * @private
 * @const
 * @type {number}
 */
var _MAX_URL_LENGTH = 1024;

/**
 * Utility to compress JSON to store on the URL.
 * @private
 */
var LZString = {

   // private property
   _f : String.fromCharCode,
   _keyStrUriSafe : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$',
   _getBaseValue : function(alphabet, character) {
      var i;

      if (!LZString._baseReverseDic) {
         LZString._baseReverseDic = {};
      }
      if (!LZString._baseReverseDic[alphabet]) {
         LZString._baseReverseDic[alphabet] = {};
         for (i=0 ; i<alphabet.length ; i++) {
            LZString._baseReverseDic[alphabet][alphabet[i]] = i;
         }
      }
      return LZString._baseReverseDic[alphabet][character];
   },

   //compress into a string that is already URI encoded
   compressToEncodedURIComponent: function (input) {
      if (input == null) {
         return '';
      }
      return LZString._compress(input, 6, function(a) {
         return LZString._keyStrUriSafe.charAt(a);
      });
   },

   //decompress from an output of compressToEncodedURIComponent
   decompressFromEncodedURIComponent:function (input) {
      if (input == null) {
         return '';
      }
      if (input == '') {
         return null;
      }
      return LZString._decompress(input.length, 32, function(index) {
         return LZString._getBaseValue(LZString._keyStrUriSafe, input.charAt(index));
      });
   },

   _compress: function (uncompressed, bitsPerChar, getCharFromInt) {
      if (uncompressed == null) {
         return '';
      }
      var i, value,
         context_dictionary= {},
         context_dictionaryToCreate= {},
         context_c='',
         context_wc='',
         context_w='',
         context_enlargeIn= 2, // Compensate for the first entry which should not count
         context_dictSize= 3,
         context_numBits= 2,
         context_data_string='',
         context_data_val=0,
         context_data_position=0,
         ii,
         f=LZString._f;

    for (ii = 0; ii < uncompressed.length; ii += 1) {
      context_c = uncompressed[ii];
      if (!Object.prototype.hasOwnProperty.call(context_dictionary,context_c)) {
        context_dictionary[context_c] = context_dictSize++;
        context_dictionaryToCreate[context_c] = true;
      }

      context_wc = context_w + context_c;
      if (Object.prototype.hasOwnProperty.call(context_dictionary,context_wc)) {
        context_w = context_wc;
      } else {
        if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
          if (context_w.charCodeAt(0)<256) {
            for (i=0 ; i<context_numBits ; i++) {
              context_data_val = (context_data_val << 1);
              if (context_data_position == bitsPerChar-1) {
                context_data_position = 0;
                context_data_string += getCharFromInt(context_data_val);
                context_data_val = 0;
              } else {
                context_data_position++;
              }
            }
            value = context_w.charCodeAt(0);
            for (i=0 ; i<8 ; i++) {
              context_data_val = (context_data_val << 1) | (value&1);
              if (context_data_position == bitsPerChar-1) {
                context_data_position = 0;
                context_data_string += getCharFromInt(context_data_val);
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = value >> 1;
            }
          } else {
            value = 1;
            for (i=0 ; i<context_numBits ; i++) {
              context_data_val = (context_data_val << 1) | value;
              if (context_data_position ==bitsPerChar-1) {
                context_data_position = 0;
                context_data_string += getCharFromInt(context_data_val);
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = 0;
            }
            value = context_w.charCodeAt(0);
            for (i=0 ; i<16 ; i++) {
              context_data_val = (context_data_val << 1) | (value&1);
              if (context_data_position == bitsPerChar-1) {
                context_data_position = 0;
                context_data_string += getCharFromInt(context_data_val);
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = value >> 1;
            }
          }
          context_enlargeIn--;
          if (context_enlargeIn == 0) {
            context_enlargeIn = Math.pow(2, context_numBits);
            context_numBits++;
          }
          delete context_dictionaryToCreate[context_w];
        } else {
          value = context_dictionary[context_w];
          for (i=0 ; i<context_numBits ; i++) {
            context_data_val = (context_data_val << 1) | (value&1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data_string += getCharFromInt(context_data_val);
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = value >> 1;
          }


        }
        context_enlargeIn--;
        if (context_enlargeIn == 0) {
          context_enlargeIn = Math.pow(2, context_numBits);
          context_numBits++;
        }
        // Add wc to the dictionary.
        context_dictionary[context_wc] = context_dictSize++;
        context_w = String(context_c);
      }
    }

    // Output the code for w.
    if (context_w !== "") {
      if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) {
        if (context_w.charCodeAt(0)<256) {
          for (i=0 ; i<context_numBits ; i++) {
            context_data_val = (context_data_val << 1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data_string += getCharFromInt(context_data_val);
              context_data_val = 0;
            } else {
              context_data_position++;
            }
          }
          value = context_w.charCodeAt(0);
          for (i=0 ; i<8 ; i++) {
            context_data_val = (context_data_val << 1) | (value&1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data_string += getCharFromInt(context_data_val);
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = value >> 1;
          }
        } else {
          value = 1;
          for (i=0 ; i<context_numBits ; i++) {
            context_data_val = (context_data_val << 1) | value;
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data_string += getCharFromInt(context_data_val);
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = 0;
          }
          value = context_w.charCodeAt(0);
          for (i=0 ; i<16 ; i++) {
            context_data_val = (context_data_val << 1) | (value&1);
            if (context_data_position == bitsPerChar-1) {
              context_data_position = 0;
              context_data_string += getCharFromInt(context_data_val);
              context_data_val = 0;
            } else {
              context_data_position++;
            }
            value = value >> 1;
          }
        }
        context_enlargeIn--;
        if (context_enlargeIn == 0) {
          context_enlargeIn = Math.pow(2, context_numBits);
          context_numBits++;
        }
        delete context_dictionaryToCreate[context_w];
      } else {
        value = context_dictionary[context_w];
        for (i=0 ; i<context_numBits ; i++) {
          context_data_val = (context_data_val << 1) | (value&1);
          if (context_data_position == bitsPerChar-1) {
            context_data_position = 0;
            context_data_string += getCharFromInt(context_data_val);
            context_data_val = 0;
          } else {
            context_data_position++;
          }
          value = value >> 1;
        }


      }
      context_enlargeIn--;
      if (context_enlargeIn == 0) {
        context_enlargeIn = Math.pow(2, context_numBits);
        context_numBits++;
      }
    }

    // Mark the end of the stream
    value = 2;
    for (i=0 ; i<context_numBits ; i++) {
      context_data_val = (context_data_val << 1) | (value&1);
      if (context_data_position == bitsPerChar-1) {
        context_data_position = 0;
        context_data_string += getCharFromInt(context_data_val);
        context_data_val = 0;
      } else {
        context_data_position++;
      }
      value = value >> 1;
    }

    // Flush the last char
    while (true) {
      context_data_val = (context_data_val << 1);
      if (context_data_position == bitsPerChar-1) {
        context_data_string += getCharFromInt(context_data_val);
        break;
      }
      else context_data_position++;
    }
    return context_data_string;
  },

  _decompress: function (length, resetValue, getNextValue) {
    var dictionary = [],
        next,
        enlargeIn = 4,
        dictSize = 4,
        numBits = 3,
        entry = '',
        result = '',
        i,
        w,
        bits, resb, maxpower, power,
        c,
        f = LZString._f,
        data = {val:getNextValue(0), position:resetValue, index:1};

    for (i = 0; i < 3; i += 1) {
      dictionary[i] = i;
    }

    bits = 0;
    maxpower = Math.pow(2,2);
    power=1;
    while (power!=maxpower) {
      resb = data.val & data.position;
      data.position >>= 1;
      if (data.position == 0) {
        data.position = resetValue;
        data.val = getNextValue(data.index++);
      }
      bits |= (resb>0 ? 1 : 0) * power;
      power <<= 1;
    }

    switch (next = bits) {
      case 0:
          bits = 0;
          maxpower = Math.pow(2,8);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }
        c = f(bits);
        break;
      case 1:
          bits = 0;
          maxpower = Math.pow(2,16);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }
        c = f(bits);
        break;
      case 2:
        return "";
    }
    dictionary[3] = c;
    w = result = c;
    while (true) {
      if (data.index > length) {
        return "";
      }

      bits = 0;
      maxpower = Math.pow(2,numBits);
      power=1;
      while (power!=maxpower) {
        resb = data.val & data.position;
        data.position >>= 1;
        if (data.position == 0) {
          data.position = resetValue;
          data.val = getNextValue(data.index++);
        }
        bits |= (resb>0 ? 1 : 0) * power;
        power <<= 1;
      }

      switch (c = bits) {
        case 0:
          bits = 0;
          maxpower = Math.pow(2,8);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }

          dictionary[dictSize++] = f(bits);
          c = dictSize-1;
          enlargeIn--;
          break;
        case 1:
          bits = 0;
          maxpower = Math.pow(2,16);
          power=1;
          while (power!=maxpower) {
            resb = data.val & data.position;
            data.position >>= 1;
            if (data.position == 0) {
              data.position = resetValue;
              data.val = getNextValue(data.index++);
            }
            bits |= (resb>0 ? 1 : 0) * power;
            power <<= 1;
          }
          dictionary[dictSize++] = f(bits);
          c = dictSize-1;
          enlargeIn--;
          break;
        case 2:
          return result;
      }

      if (enlargeIn == 0) {
        enlargeIn = Math.pow(2, numBits);
        numBits++;
      }

      if (dictionary[c]) {
        entry = dictionary[c];
      } else {
        if (c === dictSize) {
          entry = w + w[0];
        } else {
          return null;
        }
      }
      result += entry;

      // Add w+entry[0] to the dictionary.
      dictionary[dictSize++] = w + entry[0];
      enlargeIn--;

      w = entry;

      if (enlargeIn == 0) {
        enlargeIn = Math.pow(2, numBits);
        numBits++;
      }

    }
  }
};

// Static private methods

/**
 * Calculate the base URL, the href without the page reference at the end.
 */
function getBaseUrl() {
   // Remove the hash for HTML4 case
   var base = window.location.href.split('#')[0];
   // Then extract the base URL
   return removeLastSegment(base);
};

function getUrl() {
   return window.location.href;
};

/**
 * Retrieve the name of the current page if exist (index.html, page.html)
 */
function getThisPage() {
   var result = '';
   var url = window.location.pathname;
   if (url.indexOf('.html', url.length - '.html'.length) !== -1) {
      result = url.split('/').pop();
   }

   return result;
};

/**
 * Retrieve the part of the URL up to the last '/'
 */
function removeLastSegment(url) {
   return url.substring(0, url.lastIndexOf('/'));
};

/**
 * Return key/value object of query parameters.
 * @return {!Object.<string, string>}
 */
function parseQueryParam(queryString) {
   var params = {};

   if (queryString) {
      var keyValPairs = [];
      keyValPairs = queryString.split('&');
      for (var pairNum in keyValPairs)
      {
         var parts = keyValPairs[pairNum].split(/=(.+)?/);
         var key = parts[0];

         if (!key.length) {
            continue;
         }
         if (typeof params[key] === 'undefined') {
            params[key] = [];
         }
         var value = parts[1] && decodeURIComponent(parts[1].replace(/\+/g, ' '));
         params[key].push(value);
      }
   }

   return params;
}

/**
 * Return the {@link oj.RouterState} object for a specific router given a state id.
 * @param {oj.Router} router The router object.
 * @param {string} stateId The state id.
 * @return {oj.RouterState | undefined} The state object.
 */
 //TODO: Add associative array for better performance.
function getStateFromId(router, stateId) {
   var state;

   if (stateId && router._states) {
      oj.Assert.assertString(stateId);

      for (var i = 0; i < router._states.length; i++) {
         var stateAt = router._states[i];
         if (stateAt.id === stateId) {
            state = stateAt;
            break;
         }
      }
   }
   return state;
};

function getShortUrl() {
   var shortUrl;
   // For HTML4 browser, the hash is used
   if (window.location.hash && window.location.hash.length > 0) {
      shortUrl = window.location.hash.replace(/[^#]*#/, '');
      shortUrl = shortUrl.replace(_basePathName + '/', '');
   } else {
      shortUrl = getUrl().replace(_ojBaseUrl + '/', '');
   }

   return shortUrl;
};

function getRouterFullName(router) {
   if (router.parent) {
      return getRouterFullName(router.parent) + '.' + router.name;
   }

   return router.name;
};

/**
 * Return the child router for a specific branch
 * @param {oj.Router} router
 * @param {string|undefined} value
 * @return {oj.Router|undefined}
 */
function getChildRouter(router, value) {
   var subRouter;

   //TODO: Merge functionality with handlePopState
   for (var i = 0; i < router._childRouters.length; i++) {
      var sr = router._childRouters[i].router;
      if (!sr._parentState || (sr._parentState === value)) {
         subRouter = sr;
         break;
      }
   }

   return subRouter;
}

/**
 * Encode and compress the a state object. This is used for bookmarkable data.
 * @param {!Object} extraState
 * @return {string}
 * @throws An error if bookmarkable state is too big.
 */
function encodeStateParam(extraState) {
   var jsonState = JSON.stringify(extraState);
   var encodedState = encodeURIComponent(jsonState);
   var compressedState = LZString.compressToEncodedURIComponent(jsonState);
   var useCompressed = false;
   var param = _ROUTER_PARAM;

   if (compressedState.length <= encodedState.length) {
      useCompressed = true;
   }

   if (useCompressed) {
      param += '1' + compressedState;
   }
   else {
      param += '0' + encodedState;
   }

   if (param.length > _MAX_URL_LENGTH) {
      throw new Error('Size of bookmarkable data is too big.');
   }

   return param;
}

/**
 * Decompress and decode the state param from the URL.  This is used for bookmarkable data.
 * @param {!string} param
 * @throws An error if parsing fails or format is invalid.
 */
function decodeStateParam(param) {
   // First character is the compression type. Right now only 0 and 1 are supported.
   // 0 for no compression, 1 for LZW
   var compressionType = param.charAt(0);

   param = param.slice(1);

   if (compressionType === '0') {
      param = decodeURIComponent(param);
   }
   else if (compressionType === '1') {
      param = LZString.decompressFromEncodedURIComponent(param);
   }
   else {
      throw new Error('Error retrieving bookmarkable data. Format is invalid');
   }

   return JSON.parse(param);
}

/**
 * Build the state param and add it to the URL.
 * @param {!string} url the url to which the param will be added
 * @param {Object} extraState the object to be stored in the param
 * @return {string} the URL with the state param
 */
function addStateParam(url, extraState) {
   if (extraState && Object.getOwnPropertyNames(extraState).length > 0) {
      var sep;
      if (url.indexOf('?') == -1) {
         sep = '?';
      }
      else {
         sep = '&';
      }

      url +=  sep + encodeStateParam(extraState);
   }

   return url;
}

/**
 * Function use to handle the popstate event.
 */
function handlePopState(event) {
   oj.Logger.info('Handling popState event with URL: ' + window.location.href);

   // First retrieve the sub-router associated with the current state, if there is one.
   var subRouter = null;
   for (var i = 0; i < rootRouter._childRouters.length; i++) {
      var sr = rootRouter._childRouters[i].router;
      if (rootRouter._stateId() && rootRouter._stateId() === sr._parentState) {
         subRouter = sr;
         break;
      }
   }

   _canExit(subRouter).then(function (canExit) {
      if (canExit) {
         return parseAndUpdate(getShortUrl()).
            then(function(params) {
               dispatchTransitionedToState(params['hasChanged']);
            });
      }
      else {
         dispatchTransitionedToState(false);
         return Promise.resolve({ 'hasChanged': false });
      }
   }).then(null, function(error) {
      oj.Logger.error('Error while changing state in handlePopState: ' + error.message);
   });
};

/**
 * Dispatch the transitionedToState signal
 * @param {boolean} hasChanged
 */
function dispatchTransitionedToState(hasChanged) {
   oj.Router['transitionedToState'].dispatch({ 'hasChanged': hasChanged });
};

/**
 * Build an array of objects by visiting the parent hierarchy.
 * Each element of the array represent the state of a router.
 * @param {!oj.Router} router
 * @param {number=} level
 * @param {oj.RouterState=} state
 * @return {Array.<{router:oj.Router, stateId:string}>}
 */
function _buildState(router, level, state) {
   var newStates = [];

   if (router.parent) {
      newStates = _buildState(router.parent, level + 1);
   }

   var sId;
   if (typeof state === 'undefined') {
      var st = router._currentState();
      if (st) {
         sId = st.id;
      }
   } else {
      sId = state.id;
   }

   // If a default state is defined, simplify the URL by removing param
   if (sId === router._defaultStateId && level === 0) {
      sId = null;
   }

   newStates.push({
      router: router,
      stateId: sId
   });

   return newStates;
};

/**
 * Execute a lifecycle callback given its name. Used for enter and exit.
 * @private
 * @param {oj.Router} router - the router for the context
 * @param {!string} name - name of the callback to execute
 * @param {oj.RouterState=} state
 */
function _executeCallback(router, name, state) {
   //TODO: handle promise
   //TODO: invoke callback with query parameters as a parameter.
   if (state) {
      // Callback on router state has already been checked for validity in constructor.
      var callback = state[name];
      if (callback) {
         // Use the router as context
         callback.call(router);
      }
   }
}

/**
 * Traverse the child router and build and array of promise for each canExit callback.
 * If the router doesn't have a canExit callback, make a promise resolved to true.
 * If the callback is not a promise, make a promise resolved to the value returned by the callback.
 * @private
 * @param {oj.Router} router
 * @param {!Array} promisesArray - Array of promises.
 * @return {boolean} - false if one of the callback returned false.
 */
function _buildAllCanExitPromises(router, promisesArray) {
   var canExit = true;
   var promise = Promise.resolve(true);
   var currentState = router.stateFromIdCallback(router._stateId());

   if (currentState) {
      // Traverse each child router and ask for canExit
      for (var i = 0; i < router._childRouters.length; i++) {
         //TODO: Indicate to canExit which state is transitioning
         canExit = _buildAllCanExitPromises(router._childRouters[i].router, promisesArray);
         // Quick way out
         if (!canExit) {
            return false;
         }
      }

      var canExitCallback;

      // A callback defined on bound viewModel has precedence.
      if (currentState.viewModel && currentState.viewModel['canExit']) {
         canExitCallback = currentState.viewModel['canExit'];
      } else {
         canExitCallback = currentState['canExit'];
      }

      if (typeof canExitCallback === 'function') {
         //TODO: Might need to invoke with different context using apply.
         var result;

         try {
            result = canExitCallback();
         }
         catch (err) {
            oj.Logger.error('Error when executing canExit callback: ' + err.message);
            return false;
         }

         if (result && result.then) {
            promise = result;
         }
         else {
            if (!result) {
               oj.Logger.info('canExit is false for state ' + currentState.id);
            }
            canExit = result;
         }
      }
   }

   promisesArray.push(promise)

   return canExit;
};

/**
 * Invoke canExit callbacks in a a deferred way.
 * @param {oj.Router} router
 */
function _canExit(router) {
   if (!router) {
      return Promise.resolve(true);
   }
   var allPromises = [];
   var canExit = _buildAllCanExitPromises(router, allPromises);

   if (!canExit) {
      return Promise.resolve(canExit);
   }

   return Promise.all(allPromises).then(function(results) {
      for (var i = 0; i < results.length; i++) {
         if (!results[i]) {
            oj.Logger.info('CanExit promise at position ' + i + ' returned false.');
            return false;
         }
      }

      return true;
   });
};

/**
 * Return a promise resolving to true if can transition to new state in allChanges
 * @param {!Array.<{value:string, router:oj.Router}>} allChanges
 */
var _canEnter = function(allChanges) {
   oj.Logger.info('Start _canEnter.');

   var canEnter = true;
   var promise = Promise.resolve(true);
   var allPromises = [];
   for (var i = 0; i < allChanges.length; i++) {
      var change = allChanges[i];
      var canEnterCallback;
      var newState = change.router.stateFromIdCallback(change.value);

      // It is allowed to transition to an undefined state, but no state
      // callback need to be executed.
      if (newState) {
         canEnterCallback = newState['canEnter'];

         // Check if we can enter this new state by executing the callback.
         // If it is a promise, add it to the array to be resolved later.
         // If it is a boolean, break if it is false.
         if (typeof canEnterCallback === 'function') {
            //TODO: Might need to invoke with different context using apply.
            var result;

            try {
               result = canEnterCallback();
            }
            catch (err) {
               oj.Logger.error('Error when executing canEnter callback: ' + err.message);
               return Promise.resolve([]);
            }
            if (result && result.then) {
               promise = result;
            }
            else {
               canEnter = result;
               if (!canEnter) {
                  oj.Logger.info('canEnter is false for state: ' + newState.id);
                  break;
               }
            }
         }
      }

      allPromises.push(promise);
   }

   if (!canEnter) {
      return Promise.resolve([]);
   }

   return Promise.all(allPromises).then(function(results) {
      for (var i = 0; i < results.length; i++) {
         if (!results[i]) {
            oj.Logger.info('CanEnter promise at position ' + i + ' returned false.');
            return [];
         }
      }

      return allChanges;
   });
}

/**
 * Update the state of all routers in the change array.
 * @param {!Array.<{value:string, router:oj.Router}>} allChanges
 * @return { {'hasChanged': boolean} }
 */
var _updateAll = function(allChanges) {
   oj.Logger.info('Entering _updateAll.');
   var hasChanged = false;

   // Make all updates
   for (var i = 0; i < allChanges.length; i++) {
      var change = allChanges[i];
      hasChanged = change.router._update(change.value);
      if (!hasChanged) {
         break;
      }
   }

   oj.Logger.info('_updateAll returns ' + (hasChanged ? 'true' : 'false'));
   return { 'hasChanged': hasChanged };
}

/**
 *
 * @param {!string} url
 * @return {!Array.<{value:string, router:oj.Router}>}
 * @throws Error when parsing of query param fails.
 */
function _parseUrl(url) {
   var extra = {},
       search = url.split('?')[1] || '';

   oj.Logger.info('Parsing: ' + url);
   url = _urlAdapter.cleanUrl(url);

   // Retrieve the extra state from request param oj_Router
   var stateStr = search.split(_ROUTER_PARAM)[1];
   if (stateStr) {
      stateStr = stateStr.split('&')[0];
      extra = decodeStateParam(stateStr);
   }

   if (oj.Logger.option('level') === oj.Logger.LEVEL_INFO) {
      oj.Logger.info('Bookmarkable data: ');
      for (var name in extra) {
         oj.Logger.info('   { router: ' + name + ', value: ' + extra[name]);
      }
   }

   var allChanges = [];
   var reducedChanges = [];

   allChanges = _urlAdapter.parse(url);

   for (var index = 0; index < allChanges.length; index++) {
      var change = allChanges[index];

      // Update the bookmarkable data
      var ex = extra[change.router.name];
      if (ex) {
         change.router._extra = ex;
      }

      // Only keep changes where the value doesn't match the router state
      if (change.value !== change.router._stateId()) {
         reducedChanges.push(change);
      }
   }

   if (oj.Logger.option('level') === oj.Logger.LEVEL_INFO) {
      oj.Logger.info('Potential changes are: ');
      for (var i=0; i<reducedChanges.length; i++) {
         oj.Logger.info('   { router: ' + getRouterFullName(reducedChanges[i].router) +
                        ', value: ' + reducedChanges[i].value + '}');
      }
   }

   return reducedChanges;
}

/**
 * Use to update the state with the given URL.
 * Parse the new URL and update state.
 * @param {!string} url
 */
function parseAndUpdate(url) {
   var allChanges;

   try {
      allChanges = _parseUrl(url);
   }
   catch (error) {
      return Promise.reject(error);
   }

   return _canEnter(allChanges).then(_updateAll);
}

//TODO: Change constructor to take options.
/**
 * @class
 * @requires ojs/ojcore
 * @requires knockout
 * @since 1.1.0
 * @classdesc
 * <h3>JET Router</h3>
 * <p>The router is designed to simplify writing navigation for Single Page Applications.
 * The approach taken is to think of navigation in terms of states and transitions instead
 * of URLs and hashes. A router is always in one in a number of possible states and when
 * a UI action is taken in the application, a transition between states is executed. The
 * router is responsible to properly format the URL to reflect the current state and to
 * restore the application to the matching state when the URL changes.
 * <p>Building navigation is done in three steps:
 * <h6>Define the states that can be taken by the router:</h6>
 * <pre class="prettyprint"><code>
 * var router = oj.Router.rootInstance;
 * // Add three states to the router with id 'home', 'book' and 'tables
 * router.configure({
 *    'home':   { label: 'Home',   value: 'homeContent', isDefault: true },
 *    'book':   { label: 'Book',   value: 'bookContent' },
 *    'tables': { label: 'Tables', value: 'tablesContent' }
 * });
 *
 * var viewModel = {
 *    router: router
 * };
 *
 * oj.Router.sync().then(
 *    function() {
 *       ko.applyBindings(viewModel);
 *       $('#globalBody').show();
 *    },
 *    function(error) {
 *       oj.Logger.error('Error when starting router: ' + error.message);
 *    });
 * </code></pre>
 *
 * <h6>Trigger a state transition when user ask to navigate:</h6>
 * <pre class="prettyprint"><code>
 * <div id="routing-container">
 *    <div id='buttons-container' data-bind="foreach: router.states">
 *      <!-- Use the go function of the state as the handler for a click binding -->
 *      <input type="button"
 *             data-bind="click: go,  attr: {id: id},
 *             ojComponent: {component: 'ojButton', label: label}"/>
 *    </div>
 * </div>
 * </code></pre>
 *
 * <h6>Listen to the state change and updates the dependent parts:</h6>
 * <pre class="prettyprint"><code>
 * <!-- Display the content of the current state -->
 * <h2 id="pageContent" data-bind="text: router.currentValue"/>
 * </code></pre>
 *
 * @desc
 * A Router cannot be instantiated. A static Router is created when the module is loaded and can be
 * accessed using the method {@link oj.Router.rootInstance|rootInstance}.
 * A child router can be created using the method {@link oj.Router#createChildRouter|createChildRouter}.
 * @see oj.Router.rootInstance
 * @see oj.Router#createChildRouter
 * @constructor
 * @export
 */
oj.Router = function(key, parentRouter) {
   var router = this;

   /**
    * The state of the parent router when this router is current.
    * @private
    * @type {!string | undefined}
    */
   this._parentState = parentRouter ? parentRouter._stateId() : undefined;

   /**
    * Array of child router.
    * @private
    * @type {Array.<{name: string, router: oj.Router}>}
    */
   this._childRouters = [];

   /**
    * Used to store the bookmarkable data.
    * @private
    * @type {Object|undefined}
    */
   this._extra = undefined;

   /**
    * A Knockout observable that returns the id of the current state of the router.
    * @name oj.Router#stateId
    * @type {function(string=): string}
    * @readonly
    */
   this._stateId = ko.observable();

   /**
    * An array of all the possible states of the router. This array is null if the router is configured
    * using a callback.
    * @name oj.Router#states
    * @type {Array.<oj.RouterState>|null}
    * @readonly
    * @see oj.RouterState
    */
   this._states = null;

   /**
    * The state id of the default state for this router. The value is set when
    * {@link oj.Router#configure|configure} is called on the router and the state isDefault property is true.
    * If it is undefined, the router will start without a state selected.
    * This property is writable and can be used to set the default state id when the router is configured using a callback.
    * @name oj.Router#defaultStateId
    * @type {string|undefined}
    */
   this._defaultStateId = undefined;

   /**
    * A Knockout observable that returns the current {@link oj.RouterState|RouterState} if it is defined.
    * @name oj.Router#currentState
    * @type {function():(oj.RouterState|undefined)}
    * @readonly
    *
    * @example <caption>Hide a panel when the state of the router is not yet defined:</caption>
    *    <div data-bind="if: router.currentState()">
    *       <!-- content of the panel -->
    *    </div>
    */
   this._currentState = ko.pureComputed(function() {
         return ko.ignoreDependencies(router.stateFromIdCallback, router, [router._stateId()]);
      });

   /**
    * A Knockout observable that returns the value property of the current state.<br>
    * The state value property is the part of the state object that will be used in the application.
    * It is a shortcut for <code class="prettyprint">router.currentState().value;</code>
    * @name oj.Router#currentValue
    * @type {function()}
    * @readonly
    *
    * @example <caption>Display the content of the current state:</caption>
    * <h2 id="pageContent" data-bind="text: router.currentValue"/>
    */
   this._currentValue = ko.pureComputed(function() {
         var retValue;
         var currentState = ko.ignoreDependencies(router.stateFromIdCallback, router, [router._stateId()]);
         if (currentState) {
            retValue = currentState.value;
         }
         return retValue;
      });

   /**
    * An object to simplify integration between ojRouter and ojModule.
    * Use this object to configure an ojModule where the module name is the router
    * state. When the router changes state, ojModule will automatically load and
    * render the content of the module name specified in the {@link oj.RouterState#value|value}
    * of the current {@link oj.RouterState|RouterState} object.<br>
    * The object moduleConfig provide the following functionality to the ojModule binding:
    * <ol>
    *   <li>the name of ojModule binding will be the {@link oj.RouterState#value|value} property
    * of the current state of the router. If <code class="prettyprint">value</code> is not defined
    * or if it is not a string, the {@link oj.RouterState#id|id} property will be used</li>
    *   <li>The router object is passed as a parameter to the viewModel of the module</li>
    *   <li>The callback <code class="prettyprint">canExit</code> will be invoked on the viewModel.
    * If <code class="prettyprint">canExit</code> is not defined on the viewModel,
    * it will be invoked on the {@link oj.RouterState|RouterState}</li>
    * </ol>
    *
    * @name oj.Router#moduleConfig
    * @readonly
    *
    * @example <caption>Configure an ojModule binding with a router</caption>
    * <!-- This is where your main page content will be loaded -->
    * <div id="mainContainer" data-bind="ojModule: router.moduleConfig"></div>
    *
    * @example <caption>Creates a child router in the viewModel of a module</caption>
    * var viewModel = {
    *    initialize: function(params) {
    *       // Retrieve the parent router from the parameters
    *       var parentRouter = params.valueAccessor().params;
    *       // Create a child router for this viewModel
    *       this.router = parentRouter.createChildRouter('chapter')
    *          .configure({
    *             'preface':  { label: 'Preface',   value: storage['preface']  },
    *             'chapter1': { label: 'Chapter 1', value: storage['chapter1'] },
    *             'chapter2': { label: 'Chapter 2', value: storage['chapter2'] },
    *             'chapter3': { label: 'Chapter 3', value: storage['chapter3'] }
    *          });
    *       oj.Router.sync();
    *    },
    *
    *    // canExit callback will be called here
    *    canExit: function() {
    *       return (okToExit) ? true: false;
    *    }
    * };
    */
   this._moduleConfig = Object.create(null, {
      'name': {
         value: ko.pureComputed(function() {
            var retValue;
            var currentState = this.stateFromIdCallback(this._stateId());
            if (currentState) {
               retValue = currentState.value;
               if (!retValue || (typeof retValue !== 'string')) {
                  retValue = currentState.id;
               }
            }
            return retValue;
         }, router)
      },

      'params': { value: router },

      'lifecycleListener': {
         value: Object.create(null, {
            'attached': {
               value: function(params) {
                  // Retrieve router passed as a parameter to ojModule using params defined above.
                  var paramRouter = params['valueAccessor']()['params'];
                  var state = paramRouter['currentState']();
                  if (state) {
                     state.viewModel = params['viewModel'];
                  }
               }
            }
         })
      }
   });

   Object.defineProperties(this, {
      'parent': { value:
         /**
          * The parent router if it exits.
          * Only the 'root' router does not have a parent router.
          * @name oj.Router#parent
          * @member
          * @type {oj.Router|undefined}
          * @readonly
          */
         parentRouter, enumerable: true
      },
      'name': { value:
         /**
          * A string identifier of the router. It is required the name is unique within all the
          * sibling routers.
          * @name oj.Router#name
          * @member
          * @readonly
          * @type {!string}
          * @see oj.Router#createChildRouter
          */
         key,  enumerable: true
      }
   });

};

Object.defineProperties(oj.Router.prototype, {
   'states': { get: function () { return this._states; }, enumerable: true },
   'stateId': { get: function () { return this._stateId; }, enumerable: true },
   'currentState': { get: function () { return this._currentState; }, enumerable: true },
   'currentValue': { get: function () { return this._currentValue; }, enumerable: true },
   'defaultStateId': { get: function () { return this._defaultStateId; },
                       set: function(newValue) { this._defaultStateId = newValue; },
                       enumerable: true },
   'moduleConfig': { get: function () { return this._moduleConfig; }, enumerable: true }
});

/**
 * Create a child router with the given name.
 * @param {!string} name The unique name representing the router.
 * @return {oj.Router} the child router
 * @throws An error if a child router exist with the same name.
 * @export
 * @example <caption>Create a child router of the root:</caption>
 * router = oj.Router.rootInstance;
 * childRouter = router.createChildRouter('chapter');
 */
oj.Router.prototype.createChildRouter = function(name) {
   oj.Assert.assertString(name);
   // Trim the router name
   name = name.replace(oj.StringUtils._TRIM_ALL_RE, '');
   // Make sure it doesn't already exist.
   for (var i = 0; i < this._childRouters.length; i++) {
      var sr = this._childRouters[i].router;
      if (sr.name === name) {
         throw new Error('Invalid router name "' + name + '", it already exists.');
      }
      else if (sr._parentState === this._stateId()) {
         throw new Error('Cannot create more than one child router for parent state id "' + sr._parentState + '".');
      }
   }

   var childRouter = new oj.Router(name, this);

   this._childRouters.push({ name: name, router: childRouter });

   return childRouter;
};

/**
 * @private
 * @param {string} stateId The state id.
 * @return {oj.RouterState | undefined} The state object.
 */
oj.Router.prototype.stateFromIdCallback = function(stateId) {
   return getStateFromId(this, stateId);
};

/**
 * Update the state of a router with the new value.
 * @param {string=} newValue
 * @param {Object=} extra
 * @private
 */
oj.Router.prototype._update = function(newValue, extra) {
   var hasChanged = false;

   if (newValue && !this.stateFromIdCallback(newValue)) {
      oj.Logger.error('State id: "' + newValue + '" is not a valid state for ' + getRouterFullName(this));
   }
   else {
      oj.Logger.info('Updating state of ' + getRouterFullName(this) + ' to ' + newValue);

      // Execute exit on the current state
      _executeCallback(this, 'exit', this.stateFromIdCallback(this._stateId()));

      // Change the value of the stateId
      this._stateId(newValue);
      hasChanged = true;

      if (newValue) {
         // Execute enter on the new state
         _executeCallback(this, 'enter', this.stateFromIdCallback(newValue));
      }
   }

   return hasChanged;
};

/**
 * Configure the states of the router. The router can be configured in two ways:
 * <ul>
 *  <li>By describing all of the possible states that can be taken by this router.</li>
 *  <li>By providing a callback returning a {@link oj.RouterState|RouterState}
 *      object given a string state id.</li>
 * </ul>
 * This operation reset any previous configuration.<br>
 * This operation is chainable.
 * @param {!(Object.<string, {label: string, value, isDefault: boolean}> | function(string): (oj.RouterState | undefined)) } option
 * Either a callback or a dictionary of states.
 * <h6>A callback:</h6>
 * <h4 id="stateFromIdCallback" class="name">
 *    stateFromIdCallback
 *    <span class="signature">(stateId)</span>
 *    <span class="type-signature">
 *       → {<a href="oj.RouterState.html">oj.RouterState</a>|undefined}
 *    </span>
 * </h4>
 * A function returning a {@link oj.RouterState|RouterState} given a string state id.<br>
 * When using a callback, the {@link oj.Router.states|states} property will always be null since
 * states are defined on the fly.<br>See second example below.
 * <h6>A dictionary of states:</h6>
 * It is a dictionary in which the keys are state {@link oj.Router#id|id}s and values are objects
 * defining the state.<br>See first example below.
 * <h6>Key</h6>
 * <table class="params">
 *   <thead><tr>
 *     <th>Type</th>
 *     <th class="last">Description</th>
 *   </tr></thead>
 *   <tbody>
 *     <tr>
 *       <td class="type">
 *         <span class="param-type">string</span>
 *       </td>
 *       <td class="description last">the state id.
 *       See the RouterState <a href="oj.RouterState.html#id">id</a> property.</td>
 *    </tr>
 *   </tbody>
 * </table>
 * @param {string=} option.label the string for the link.
 * See the {@link oj.RouterState#label} property.
 * @param {*=} option.value the object associated with this state.
 * See the {@link oj.RouterState#value} property.
 * @param {boolean=} option.isDefault true if this state is the default.
 * See the Router {@link oj.Router#defaultStateId|defaultStateId} property.
 * @param {(function(): boolean) | (function(): Promise)=} option.canEnter A callback that either
 * returns a boolean or the Promise of a boolean. If the boolean is true the transition will continue.
 * The default value is a method that always returns true.
 * See the {@link oj.RouterState#canEnter} property.
 * @param {function()=} option.enter The callback executed when entering this state.
 * See the {@link oj.RouterState#enter} property.
 * @param {(function(): boolean)|(function(): Promise)=} option.canExit  A callback that either
 * returns a boolean or the Promise of a boolean. If the boolean is true the transition will continue.
 * The default value is a method that always returns true.
 * See the {@link oj.RouterState#canExit} property.
 * @param {function()=} option.exit The callback executed when exiting this state.
 * See the {@link oj.RouterState#exit} property.
 * @return {!oj.Router} the oj.Router object this method was called on.
 * @export
 * @see oj.RouterState
 * @example <caption>Add three states with id 'home', 'book' and 'tables':</caption>
 * router.configure({
 *    'home':   { label: 'Home',   value: 'homeContent', isDefault: true },
 *    'book':   { label: 'Book',   value: 'bookContent' },
 *    'tables': { label: 'Tables', value: 'tablesContent' }
 * });
 * @example <caption>Define a function to retrieve the state:</caption>
 * router.configure(function(stateId) {
 *    var state;
 *
 *    if (stateId) {
 *       var data = getStates(); // Return a array of RouterState keyed by stateId
 *       if (data) {
 *          state = data[stateId];
 *       }
 *    }
 *    return state;
 * });
 */
oj.Router.prototype.configure = function(option) {
   this._stateId(undefined);
   delete this._defaultStateId;

   if (typeof option === 'function') {
      this._states = null;
      // Override prototype
      this.stateFromIdCallback = option;
   }
   else {
      this._states = [];
      // Restore prototype
      delete this.stateFromIdCallback;

      Object.keys(option).forEach(function(key) {
         var rsOptions = option[key];
         this._states.push(new oj.RouterState(key, rsOptions, this));
         // Set the defaultStateId of the router from the isDefault property
         if ((typeof(rsOptions['isDefault']) === 'boolean') && rsOptions['isDefault']) {
            this._defaultStateId = key;
         }
      }, this);
   }

   return this;
};

/**
 * Return the {@link oj.RouterState} object which state id matches one of the possible states of the router.
 * @param {string} stateId - the id of the requested {@link oj.RouterState} object.
 * @return {oj.RouterState|undefined} the state object matching the id.
 * @export
 * @example <caption>Retrieve the RouterState for id 'home':</caption>
 * var homeState = router.getState('home');
 * var homeStateValue = homeState.value;
 */
oj.Router.prototype.getState = function(stateId) {
   return this.stateFromIdCallback(stateId);
};

/**
 * Go is used to transition to a new state using a state id.
 * If the state id is undefined, go to the default state of the router.<br>
 * A {@link oj.Router.transitionedToState|transitionedToState} signal is dispatched when the
 * state transition has completed.
 * @param {string=} stateId The id of the state to transition to.
 * @return {!Promise} A Promise that resolves when the router is done with
 * the state transition.<br>
 * When the promise is fullfilled, the parameter value is an object with the property
 * <code class="prettyprint">hasChanged</code>.<br>
 * The value of <code class="prettyprint">hasChanged</code> is:
 * <ul>
 *   <li>true: If the router state changed.</li>
 * </ul>
 * When the Promise is rejected, the parameter value is:
 * <ul>
 *   <li>An Error object stipulating the reason for the rejection when an error
 * occurred during the resolution.</li>
 * </ul>
 * @export
 * @example <caption>Transition a router to the state id 'home':</caption>
 * router.go('home');
 * @example <caption>Transition a router to its default state and handle errors:</caption>
 * router.go().then(
 *    function(result) {
 *       if (result.hasChanged) {
 *          oj.Logger.info('Router transitioned to default state.');
 *       }
 *       else {
 *          oj.Logger.info('No transition, Router was already in default state.');
 *       }
 *    },
 *    function(error) {
 *       oj.Logger.error('Transition to default state failed: ' + error.message);
 *    }
 * );
 */
oj.Router.prototype.go = function(stateId) {
   var state;
   var useDefault = true;
   if (stateId) {
      if (typeof stateId === 'string') {
         if (stateId.length > 0) {
            state = this.stateFromIdCallback(stateId);
            if (!state) {
               dispatchTransitionedToState(false);
               return Promise.reject(new Error('State id "' + stateId + '" does not exist.'));
            }

            useDefault = false;
            oj.Logger.info('Going to state ' + stateId + ' on router ' + getRouterFullName(this));
         }
      }
      else {
         dispatchTransitionedToState(false);
         return Promise.reject(new Error('Invalid object type for state id.'));
      }
   }

   if (useDefault) {
      if (this._defaultStateId) {
         state = this.stateFromIdCallback(this._defaultStateId);
         oj.Logger.info('Going to default state ' + state.id + ' on router ' + getRouterFullName(this));
      }
      else {
         // No default defined, so nowhere to go.
         oj.Logger.info('Undefined state id with no default id on router ' + getRouterFullName(this));
         dispatchTransitionedToState(false);
         return Promise.resolve({ 'hasChanged': false });
      }
   }

   var allStates = _buildState(this, 0, state);
   var newUrl = _urlAdapter.buildUrlFromStates(allStates);
   var shortUrl = '/' + _urlAdapter.cleanUrl(getShortUrl()).replace(_thisPage, '');

   var _changeState = function(canExit) {
      if (canExit) {
         // Remove first '/' if exist before parsing
         //TODO: Check if removing / is needed.
         return parseAndUpdate(newUrl.replace(/^\//, '')).
            then(function(params) {
               if (params['hasChanged']) {
                  var fullUrl = _ojBaseUrl + newUrl;
                  oj.Logger.info('Changing URL to ' + fullUrl);
                  history.pushState(null, '', fullUrl);
               }
               dispatchTransitionedToState(params['hasChanged']);
               return params;
            });
      }
      else {
         dispatchTransitionedToState(false);
         return Promise.resolve({ 'hasChanged': false });
      }
   };

   // Do not do anything if the new URL is the same.
   // This compare URLs without the bookmarkable data.
   // TODO: better match for empty page "" vs. "/" vs. "index.html"
   // TODO: match query param in any order
   if (_urlAdapter.cleanUrl(newUrl) !== shortUrl) {
      oj.Logger.info('New URL is different.');
      return _canExit(this).then(_changeState);
   }
   else {
      dispatchTransitionedToState(false);
      return Promise.resolve({ 'hasChanged': false });
   }
};

/**
 * Store additional data for this router that will be added in a compressed form to the URL
 * so it can be bookmarked. When calling this method, the URL is immediately modified.
 * @param {!Object} data the data to store with this state.
 * @throws An error if the bookmarkable state is too big.
 * @export
 * @example <caption>Store a color in the URL:</caption>
 * try {
 *    var color = '#99CCFF';
 *    router.store(color);
 *    $('#chapter').css('background', color);
 * }
 * catch (error) {
 *    oj.Logger.error('Error while storing data: ' + error.message);
 * }
 */
oj.Router.prototype.store = function(data, sep) {
   this._extra = data;

   var extraState = {},
       router = this;

   // Walk the parent routers
   while (router) {
      if (router._extra) {
         extraState[router.name] = router._extra;
      }
      router = router.parent;
   }

   // and the children routers
   router = this;
   var nextLevel;
   while (router) {
      for (var i = 0; i < router._childRouters.length; i++) {
         var sr = router._childRouters[i].router;
         if (router._stateId() && router._stateId() === sr._parentState) {
            if (sr._extra) {
               extraState[sr.name] = sr._extra;
            }
            nextLevel = sr;
            break;
         }
      }
      router = nextLevel;
      nextLevel = undefined;
   }

   var url = _ojBaseUrl + '/' + _urlAdapter.cleanUrl(getShortUrl());
   url = addStateParam(url, extraState);

   history.replaceState(null, '', url);
};

/**
 * Retrieve the additional data stored in the URL.
 * @return {*} the content stored in the URL
 * @export
 * @example <caption>Retrieve the value of the background color stored in the URL:</caption>
 *  oj.Router.sync().then(
 *     function() {
 *        var color = viewModel.router.retrieve();
 *        if (color) {
 *           $('#chapter').css('background', color);
 *        }
 *     },
 *     function(error) {
 *        oj.Logger.error('Error during sync: ' + error.message);
 *     }
 *  );
 */
oj.Router.prototype.retrieve = function() {
   return this._extra;
};

/**
 * Dispose the router.<br>
 * Erase all states of this router and its children.
 * Remove itself from parent router child list.<br>
 * When this method is invoked on the {@link oj.Router.rootInstance|rootInstance}, it
 * also remove internal event listeners and re-initialize the
 * {@link oj.Router.defaults|defaults}.
 * @export
 */
oj.Router.prototype.dispose = function() {
   // Depth first
   while (this._childRouters.length > 0) {
      this._childRouters[0].router.dispose();
   }

   // If this is the root, clean up statics
   if (!this['parent']) {
      _ojBaseUrl = '';
      _basePathName = '';
      _urlAdapter = {};

      window.removeEventListener('popstate', handlePopState);
      oj.Router['transitionedToState'].removeAll();
      oj.Router.initialized = false;
   }
   // Remove itself from parent children array.
   else {
      var parentChildren = this['parent']._childRouters;
      for (var i = 0; i < parentChildren.length; i++) {
         if (parentChildren[i].router.name === this.name) {
            parentChildren.splice(i, 1);
            break;
         }
      }

      delete this._parentState;
   }

   this._states = null;
   delete this._defaultStateId;
   delete this._extra;
};

/**
 * Create the instance of the root router.
 * @type {oj.Router}
 */
var rootRouter = new oj.Router('root', undefined);

Object.defineProperties(oj.Router, {
   'rootInstance': { value:
      /**
       * The static instance of {@link oj.Router} representing the unique root router.
       * This instance is created at the time the module is loaded.<br>
       * All other routers will be children of this object.
       * The name property of this router is 'root'. The parent property is null.
       * @name oj.Router.rootInstance
       * @type oj.Router
       * @readonly
       * @example <caption>Retrieve the root router and configure it:</caption>
       * var router = oj.Router.rootInstance;
       * router.configure({
       *    'home':   { label: 'Home',   value: 'homeContent', isDefault: true },
       *    'book':   { label: 'Book',   value: 'bookContent' },
       *    'tables': { label: 'Tables', value: 'tablesContent' }
       * });
       */
      rootRouter, enumerable: true
   },
   'transitionedToState': { value:
      /**
       * A {@link http://millermedeiros.github.io/js-signals/|signal} dispatched when the state transition
       * has completed either by successfully changing the state or cancelling.<br>
       * The parameter of the event handler is a boolean true when the state has changed.<br>
       * This is usefull when some post processing is needed or to test the result after a state change.
       * @name oj.Router.transitionedToState
       * @readonly
       * @example <caption>Creates promise that resolve when the state transition is complete.</caption>
       * var promise = new Promise(function(resolve, reject) {
       *       oj.Router.transitionedToState.add(function(result) {
       *          if (result.hasChanged) {
       *             oj.Logger.info('The state has changed');
       *          }
       *          resolve();
       *       });
       */
      new signals.Signal, enumerable: true
   }
});

/**
 * A set of Router defaults properties.<br>
 * <h6>Warning: </h6>Defaults can not be changed after the first call to {@link oj.Router.sync|sync()}
 * has been made. To re-initialize the router, you need to call {@link oj.Router#dispose|dispose()} on
 * the {@link oj.Router.rootInstance|rootInstance} first then change the defaults.
 * @property {Object} urlAdapter an instance of the url adapter to use.
 * Possible values are an instance of {@link oj.Router.urlPathAdapter} or {@link oj.Router.urlParamAdapter}.
 * @property {string} baseUrl the base URL to be used for relative URL addresses. If not defined,
 * it is the current URL without the document.
 * For example <code class="prettyprint">http://www.example.com/myApp</code>. This is needed
 * by the Router to properly parse the URL.
 * @export
 * @example <caption>Change the default URL adapter to the urlParamAdapter</caption>
 * oj.Router.defaults['urlAdapter'] = new oj.Router.urlParamAdapter();
 * @example <caption>Change the base URL</caption>
 * oj.Router.defaults['baseUrl'] = 'http://www.example.com/myApp';
 */
oj.Router.defaults = {};

Object.defineProperties(oj.Router.defaults, {
   'urlAdapter': {
      get: function() {
         if (!_urlAdapter) {
            _urlAdapter = new oj.Router.urlPathAdapter();
         }
         return _urlAdapter;
      },
      set: function(urlAdapter) {
         if (oj.Router.initialized) {
            throw new Error('Incorrect operation. Cannot change URL adapter after calling sync().');
         }
         _urlAdapter = urlAdapter
      },
      enumerable: true,
      readonly: false
   },
   'baseUrl': {
      get: function() {
         if (!_ojBaseUrl) {
            _ojBaseUrl = getBaseUrl();
            _basePathName = removeLastSegment(window.location.pathname);
         }
         return _ojBaseUrl;
      },
      set: function(baseUrl) {
         if (oj.Router.initialized) {
            throw new Error('Incorrect operation. Cannot change base URL after calling sync().');
         }
         // Assumption is _ojBaseUrl does not have a trailing /
         _ojBaseUrl = baseUrl.replace(/\/$/, '');
         _basePathName = _ojBaseUrl.replace(window.location.protocol + '//' + window.location.host, '');
      },
      enumerable: true,
      readonly: false
   }
});

/**
 * Synchronise the router with the current URL. The process parse the URL and
 * <ol>
 *   <li>transition the router to a new state matching the URL.</li>
 *   <li>initialize the bookmarkable storage.</li>
 *   <li>dispatch a {@link oj.Router.transitionedToState|transitionedToState} signal.</li>
 * </ol>
 * It has to be called after a router is configured, to synchronise the URL with the
 * router state.<br>
 * If a default state is defined, the router will transition to it, otherwise no transition will
 * occur and the router will be in an undefined state.<br>
 * Because the process of transitioning between two states invokes callbacks (canExit, canEnter)
 * that are promises, this function also returns a promise.
 * @return {!Promise.<{hasChanged: boolean}>} A Promise that resolves when the router is done with
 * the state transition.<br>
 * When the Promise is fullfilled, the parameter value is an object with the property
 * <code class="prettyprint">hasChanged</code>.<br>
 * The value of <code class="prettyprint">hasChanged</code> is:
 * <ul>
 *   <li>true: If the router state changed.</li>
 * </ul>
 * When the Promise is rejected, the parameter value is:
 * <ul>
 *   <li>An Error object stipulating the reason for the rejection when an error
 * occurred during the resolution.</li>
 * </ul>
 * @export
 * @example <caption>Start the root instance</caption>
 * var router = oj.Router.rootInstance;
 * // Add three states to the router with id 'home', 'book' and 'tables
 * router.configure({
 *    'home':   { label: 'Home',   value: 'homeContent', isDefault: true },
 *    'book':   { label: 'Book',   value: 'bookContent' },
 *    'tables': { label: 'Tables', value: 'tablesContent' }
 * });
 *
 * var viewModel = {
 *    router: router
 * };
 *
 * oj.Router.sync().then(
 *    function() {
 *       ko.applyBindings(viewModel);
 *       $('#globalBody').show();
 *    },
 *    function(error) {
 *       oj.Logger.error('Error when starting the router: ' + error.message);
 *    }
 * );
 * @example <caption>Synchronise a newly created child Router and retrieve the bookmarkable state</caption>
 *  oj.Router.sync().then(
 *     function() {
 *        var color = viewModel.router.retrieve();
 *        if (color) {
 *           $('#chapter').css('background', color);
 *        }
 *     },
 *     function(error) {
 *        oj.Logger.error('Error during sync: ' + error.message);
 *     }
 *  );
 *
 */
oj.Router.sync = function() {
   if (!oj.Router.initialized) {
      // Initialize the default values if needed.
      if (!_urlAdapter) {
         _urlAdapter = new oj.Router.urlPathAdapter();
      }
      if (!_ojBaseUrl) {
         _ojBaseUrl = getBaseUrl();
         _basePathName = removeLastSegment(window.location.pathname);
      }

      /**
       * Listen to URL changes caused by back/forward button
       * using the popstate event. Call handlePopState to dispatch the change of URL.
       */
      window.addEventListener('popstate', handlePopState, false);

      oj.Logger.info('Initializing rootInstance.');
      oj.Logger.info('Base URL is ' + _ojBaseUrl);
      oj.Logger.info('This page is ' + _thisPage);
      oj.Logger.info('Current URL is ' + window.location.href);

      oj.Router.initialized = true;
   }

   return parseAndUpdate(getShortUrl()).
      then(function(params) {
         dispatchTransitionedToState(params['hasChanged']);
         return params;
      });
};

/*------------------------------------------------------------------------------
  URL Apdaters section
  ------------------------------------------------------------------------------*/

/**
 *
 * @class
 * @since 1.1.0
 * @classdesc Url adapter used by the {@link oj.Router} to manage URL in the form of
 * <code class="prettyprint">/book/chapter2</code>. The UrlPathAdapter is the default
 * adapter used by the {@link oj.Router|router}.<br> There are two available adapters,
 * this one and the {@link oj.Router.urlParamAdapter|urlParamAdapter}.
 * @see oj.Router.urlParamAdapter
 * @see oj.Router.start
 * @constructor
 * @export
 */
oj.Router.urlPathAdapter = function () {
   /**
    * Construct an array of changes where each item is an object made of a router and
    * the new state for it.
    * @ignore
    * @param {string} url
    */
   this.parse = function(url) {
      var index = 0,
          router = rootRouter,
          segments = url.split('/'),
          changes = [];

      do {
         var value = segments[index++];
         if (value) {
            if (value.length === 0 || /\.html$/i.test(value)) {
               value = undefined;
            }
         }

         value = value || router._defaultStateId;
         changes.push({ value: value, router: router });

         router = getChildRouter(router, value);
      } while (router);

      return changes;
   };

   /**
    * Given an ordered array of states, build the URL representing all
    * the states.
    * Always starts with a '/': /index.html, /book/chapter2
    * @ignore
    * @param {Array.<{router:oj.Router, stateId:string}>} newStates
    * @return {!string} the URL representing the states
    */
   this.buildUrlFromStates = function(newStates) {
      var newUrl = '',
          extraState = {}; // Compound object of all extra states

      // Build the new URL
      newStates.forEach(function(ns) {
         if (ns.stateId) {
            newUrl += '/' + ns.stateId;
         }
         if (ns.router.extra) {
            extraState[ns.router.name] = ns.router._extra;
         }
      });

      // No page or this page are aliases.
      if (newUrl === '') {
         //TODO: Remove _thisPage
         newUrl = '/' + _thisPage;
      }

      try {
         newUrl = addStateParam(newUrl, extraState);
      }
      catch (err) {
         oj.Logger.error('Error while building URL: ' + err);
      }

      return newUrl;
   };

   /**
    * Return the significant part of an URL.
    * @ignore
    * @param {!string} url
    * @return {!string} the short URL
    */
   this.cleanUrl = function(url) {
      return url.split('?')[0];
   };

   /**
    * Return extra query param
    * @ignore
    * @param {string} url
    * @return {!Object.<string, string>}
    */
   this.getQueryParam = function(url) {
      var queryIndex = url.indexOf('?');
      var queryString = null;

      if (queryIndex != -1) {
         queryString = url.substr(queryIndex + 1);
      }

      return parseQueryParam(queryString);
   };
};

/**
 * @class
 * @since 1.1.0
 * @classdesc Url adapter used by the {@link oj.Router} to manage URL in the form of
 * <code class="prettyprint">/index.html?book=chapter2</code>. There are two available
 * adapters, this one and the {@link oj.Router.urlPathAdapter|urlPathAdapter}.
 * @see oj.Router.urlPathAdapter
 * @see oj.Router.defaults
 * @constructor
 * @export
 * @example <caption>Change the default URL adapter to urlParamAdapter instead of urlPathAdapter:</caption>
 * oj.Router.defaults['urlAdapter'] = new oj.Router.urlParamAdapter();
 */
oj.Router.urlParamAdapter = function () {
   /**
    * Construct an array of changes where each item is an object made of a router and
    * the new state for it.
    * @ignore
    * @param {string} url
    */
   this.parse = function(url) {
      var params = this.getQueryParam(url),
          router = rootRouter,
          changes = [];

      do {
         var value = params[router.name];
         if (value) {
            value = value[0];
            delete params[router.name];
         }

         value = value || router._defaultStateId;
         changes.push({ value: value, router: router });

         router = getChildRouter(router, value);
      } while (router);

      return changes;
   };

   /**
    * Given an ordered array of states, build the URL representing all
    * the states.
    * Always starts with a '/': /index.html, /book/chapter2
    * @ignore
    * @param {Array.<{router:oj.Router, stateId:string}>} newStates
    * @return {!string} the URL representing the states
    * @throws An error if bookmarkable state is too big.
    */
   this.buildUrlFromStates = function(newStates) {
      var newUrl = '/' + _thisPage,
          extraState = {}, // Compound object of all extra states
          sep = '?';

      // Build the new URL
      newStates.forEach(function(ns) {
         if (ns.stateId) {
            newUrl += sep + ns.router.name + '=' + ns.stateId;
            sep = '&'; // From now on, use this separator
         }
         if (ns.router._extra) {
            extraState[ns.router.name] = ns.router._extra;
         }
      });

      try {
         newUrl = addStateParam(newUrl, extraState);
      }
      catch (err) {
         oj.Logger.error('Error while building URL: ' + err);
      }

      return newUrl;
   };

   /**
    * Return the significant part of an URL.
    * @ignore
    * @param {!string} url
    * @return {!string} the short URL
    */
   this.cleanUrl = function(url) {
      var index = url.indexOf(_ROUTER_PARAM);
      if (index != -1) {
         return url.substr(0, index - 1);
      }
      return url;
   };

   /**
    * Return extra query param
    * @ignore
    * @param {string} url
    * @return {!Object.<string, string>}
    */
   this.getQueryParam = function(url) {
      var queryIndex = url.indexOf('?');
      var queryString = null;
      var params = {};

      if (queryIndex != -1) {
         queryString = url.substr(queryIndex + 1);
         params = parseQueryParam(queryString);
      }

      //TODO: Need to remove state parameter from the list.
      return params;
   };


};

return rootRouter;

})();