Migration to new build pattern and new retrieval code

This also includes lots of minor fixes to just about everything.
The migration only includes moving files for now but will include new
build steps in the future.

For the time being please use /catalog-dev, it is unfiltered original
code. The build script will be building into /catalog in the future.

Change-Id: Ib49515233aa5e7ebfdf1385b763f986341d82f4a
diff --git a/client/catalog-dev/js/autocomplete.js b/client/catalog-dev/js/autocomplete.js
new file mode 100644
index 0000000..d6987e2
--- /dev/null
+++ b/client/catalog-dev/js/autocomplete.js
@@ -0,0 +1,181 @@
+/*
+ * The following code is a jquery extention written to add autocomplete functionality to bootstrap input groups.
+ *
+ * Usage:
+ *
+ * Then simply call $('.someClass').autoComplete(getSuggestions) on it to enable auto completion.
+ *
+ * getSuggestions returns by calling its callback parameter with an array of valid strings.
+ *
+ * Autocomplete can be manually triggered by triggering the autoComplete event.
+ *
+ */
+(function(){
+  "use strict";
+  if (!jQuery){
+    throw new Error("jQuery is required and must be loaded before this script.")
+  }
+  jQuery.fn.extend({
+    /**
+     * @param {Array<String>|getSuggestions}
+     */
+    autoComplete: function(suggestions) {
+
+      var element = $('<div></div>');
+      element.addClass('list-group')
+      .addClass('autoComplete')
+      .css({
+        'top': this.parent().height()
+      });
+
+      this.attr('autocomplete', 'off')
+      .after(element);
+
+      var getSuggestions = function(current, callback){
+        callback(suggestions.reduce(function(prev, suggestion){
+          if (current.toLowerCase().indexOf(suggestion.substr(0, current.length).toLowerCase()) === 0){
+            prev.push(suggestion);
+          }
+          return prev;
+        }, []));
+      }
+
+      var lastList = [];
+
+      var setAutoComplete = function(list){
+        lastList = list;
+        element.empty();
+
+        element.html(list.reduce(function(prev, current){
+          return [prev, '<a href="#" class="list-group-item">', current, '</a>'].join("");
+        }, ""));
+
+      }
+
+      if (suggestions instanceof Function){
+        getSuggestions = suggestions;
+      }
+
+      var input = this;
+
+      var matcher = /^\/([-_\w]+\/)*/; //Returns only the absolute path.
+
+      var getValue = function(){
+        var res = matcher.exec(input.val());
+        if (res){
+          return res[0]; //Return the absolute match
+        } else {
+          throw new Error("Empty or incorrectly formatted path.");
+        }
+      }
+
+      element.bind('click', 'a', function(){
+        input.val($(event.target).text());
+        getSuggestions(getValue(), setAutoComplete);
+        input.focus();
+      });
+
+      var updateFromList = function(){
+        var val = input.val(); //Needs to be unfiltered, for filtering existing results.
+        var temp = lastList;
+        setAutoComplete(lastList.reduce(function(prev, current){
+          if (current.indexOf(val) === 0){
+            prev.push(current);
+          }
+          return prev;
+        }, []));
+        lastList = temp;
+      }
+
+      this.keydown(function(e){
+        switch(e.which){
+          case 38: //up
+          var active = element.find('.active');
+          if (active.length === 0){
+            element.find(':first-child').addClass('active');
+          } else {
+            if (!active.is(':first-child')){
+              var top = active.removeClass('active').prev().addClass('active').offset().top;
+              active.parent().stop().animate({scrollTop: top}, 500);
+            }
+          }
+          e.preventDefault();
+          break;
+
+          case 40: //down
+          var active = element.find('.active');
+          if (active.length === 0){
+            element.find(':first-child').addClass('active');
+          } else {
+            if (!active.is(':last-child')){
+              var top = active.removeClass('active').next().addClass('active').offset().top;
+              active.parent().stop().animate({scrollTop: top}, 500);
+            }
+          }
+          e.preventDefault();
+          break;
+
+          case 13: //Enter
+          var active = element.find('.active');
+          if (active.length === 1){
+            $(this).val(active.text());
+            e.preventDefault();
+            getSuggestions(getValue(), setAutoComplete);
+          }
+          break;
+
+          case 9: //Tab
+          getSuggestions(getValue(), setAutoComplete);
+          e.preventDefault(); //Don't print tab or select a different element.
+          break;
+
+          case 8:
+          console.log("Detected backspace");
+          if (input.val().slice(-1) == "/"){
+            e.preventDefault();
+            input.val(input.val().slice(0,-1)); //Manually backspace early. (Have to do it manually)
+            getSuggestions(getValue(), setAutoComplete);
+          } else {
+            updateFromList(setAutoComplete);
+          }
+
+          default:
+          updateFromList();
+
+        }
+
+      })
+      .keyup(function(e){
+        switch (e.which){
+
+          case 191:
+          getSuggestions(getValue(), setAutoComplete);
+
+          break;
+          case 38:
+          case 40:
+          case 13:
+          case 9:
+          // Do nothing
+
+          break;
+          default:
+          updateFromList();
+        }
+      });
+
+      this.on('autoComplete', function(){
+        getSuggestions(getValue(), setAutoComplete);
+      });
+
+      return this;
+
+    }
+  });
+})();
+
+/**
+ * @callback getSuggestions
+ * @param {string} current - The current value of the input field.
+ * @param {function}
+ */
diff --git a/client/catalog-dev/js/catalog.js b/client/catalog-dev/js/catalog.js
new file mode 100644
index 0000000..d013fda
--- /dev/null
+++ b/client/catalog-dev/js/catalog.js
@@ -0,0 +1,714 @@
+//Run when the document loads AND we have the config loaded.
+(function(){
+  "use strict";
+  var config;
+  Promise.all([
+    new Promise(function(resolve, reject){
+      $.ajax('config.json').done(function(data){
+        config = data;
+        resolve();
+      }).fail(function(){
+        console.error("Failed to get config.");
+        ga('send', 'event', 'error', 'config');
+        reject();
+      });
+    }),
+    new Promise(function(resolve, reject){
+      var timeout = setTimeout(function(){
+        console.error("Document never loaded? Something bad has happened!");
+        reject();
+      }, 10000);
+      $(function () {
+        clearTimeout(timeout);
+        resolve();
+      });
+    })
+  ]).then(function(){
+    new Atmos(config);
+  }, function(){
+    console.error("Failed to initialize!");
+    ga('send', 'event', 'error', 'init');
+  });
+})();
+
+var Atmos = (function(){
+  "use strict";
+
+  var closeButton = '<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
+
+  var guid = function(){
+    var d = new Date().getTime();
+    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+      var r = (d + Math.random()*16)%16 | 0;
+      d = Math.floor(d/16);
+      return (c=='x' ? r : (r&0x3|0x8)).toString(16);
+    });
+    return uuid;
+  }
+
+  /**
+   * Atmos
+   * @version 2.0
+   *
+   * Configures an Atmos object. This manages the atmos interface.
+   *
+   * @constructor
+   * @param {string} catalog - NDN path
+   * @param {Object} config - Object of configuration options for a Face.
+   */
+  var Atmos = function(config){
+
+    //Internal variables.
+    this.results = [];
+    this.resultCount = Infinity;
+    this.name = null;
+    this.page = 0;
+    this.resultsPerPage = 25;
+    this.retrievedSegments = 0;
+
+    //Config/init
+    this.config = config;
+
+    this.catalog = config['global']['catalogPrefix'];
+
+    this.face = new Face(config['global']['faceConfig']);
+
+    //Easy access dom variables
+    this.categories = $('#side-menu');
+    this.resultTable = $('#resultTable');
+    this.filters = $('#filters');
+    this.searchInput = $('#search');
+    this.searchBar = $('#searchBar');
+    this.searchButton = $('#searchButton');
+    this.resultMenu = $('.resultMenu');
+    this.alerts = $('#alerts');
+    this.requestForm = $('#requestForm');
+
+    var scope = this;
+
+    $('.requestSelectedButton').click(function(){
+      ga('send', 'event', 'button', 'click', 'request');
+      scope.request(scope.resultTable.find('.resultSelector:checked:not([disabled])').parent().parent());
+    });
+
+    this.filterSetup();
+
+    //Init autocomplete
+    this.searchInput.autoComplete(function(field, callback){
+      ga('send', 'event', 'search', 'autocomplete');
+      scope.autoComplete(field, function(list){
+        callback(list.map(function(element){
+          return field + element + "/";
+        }));
+      });
+    });
+
+    //Handle search
+    this.searchBar.submit(function(e){
+      ga('send', 'event', 'search', 'submit');
+      e.preventDefault();
+      if (scope.searchInput.val().length === 0){
+        if (!scope.searchBar.hasClass('has-error')){
+          scope.searchBar.addClass('has-error').append('<span class="help-block">Search path is required!</span>');
+        }
+        return;
+      } else {
+        scope.searchBar.removeClass('has-error').find('.help-block').fadeOut(function(){$(this).remove()});
+      }
+      scope.pathSearch();
+    });
+
+    this.searchButton.click(function(){
+      console.log("Search Button Pressed");
+      ga('send', 'event', 'button', 'click', 'search');
+      scope.search();
+    });
+
+    //Result navigation handlers
+    this.resultMenu.find('.next').click(function(){
+      ga('send', 'event', 'button', 'click', 'next');
+      if (!$(this).hasClass('disabled')){
+        scope.getResults(scope.page + 1);
+      }
+    });
+    this.resultMenu.find('.previous').click(function(){
+      ga('send', 'event', 'button', 'click', 'previous');
+      if (!$(this).hasClass('disabled')){
+        scope.getResults(scope.page - 1);
+      }
+    });
+
+    //Change the number of results per page handler
+    var rpps = $('.resultsPerPageSelector').click(function(){
+
+      var t = $(this);
+
+      if (t.hasClass('active')){
+        return;
+      }
+
+      rpps.find('.active').removeClass('active');
+      t.addClass('active');
+      scope.resultsPerPage = Number(t.text());
+      scope.getResults(0); //Force return to page 1;
+
+    });
+
+    //Init tree search
+    $('#treeSearch div').treeExplorer(function(path, callback){
+      console.log("Tree Explorer request", path);
+      ga('send', 'event', 'tree', 'request');
+      scope.autoComplete(path, function(list){
+        console.log("Autocomplete response", list);
+        callback(list.map(function(element){
+          return (path == "/"?"/":"") + element + "/";
+        }));
+      })
+    });
+
+    this.setupRequestForm();
+
+  }
+
+  Atmos.prototype.clearResults = function(){
+    this.results = []; //Drop any old results.
+    this.retrievedSegments = 0;
+    this.resultCount = Infinity;
+    this.page = 0;
+    this.resultTable.empty();
+  }
+
+  Atmos.prototype.pathSearch = function(){
+    var value = this.searchInput.val();
+
+    this.clearResults();
+
+    var scope = this;
+
+    this.query(this.catalog, {"??": value},
+    function(interest, data){
+      console.log("Query response:", interest, data);
+
+      scope.name = data.getContent().toString().replace(/[\n\0]/g,"");
+
+      scope.getResults(0);
+
+    },
+    function(interest){
+      console.warn("Request failed! Timeout", interest);
+      scope.createAlert("Request timed out. \"" + interest.getName().toUri() + "\" See console for details.");
+    });
+
+  }
+
+  Atmos.prototype.search = function(){
+
+    var filters = this.getFilters();
+
+    console.log("Search started!", this.searchInput.val(), filters);
+
+    console.log("Initiating query");
+
+    this.clearResults();
+
+    var scope = this;
+
+    this.query(this.catalog, filters,
+    function(interest, data){ //Response function
+      console.log("Query Response:", interest, data);
+
+      scope.name = data.getContent().toString().replace(/[\n\0]/g,"");
+
+      scope.getResults(0);
+
+    }, function(interest){ //Timeout function
+      console.warn("Request failed! Timeout");
+      scope.createAlert("Request timed out. \"" + interest.getName().toUri() + "\" See console for details.");
+    });
+
+  }
+
+  Atmos.prototype.autoComplete = function(field, callback){
+
+    var scope = this;
+
+    this.query(this.catalog, {"?": field},
+    function(interest, data){
+
+      var name = new Name(data.getContent().toString().replace(/[\n\0]/g,""));
+
+      var interest = new Interest(name);
+      interest.setInterestLifetimeMilliseconds(5000);
+      interest.setMustBeFresh(true);
+
+      scope.face.expressInterest(interest,
+      function(interest, data){
+
+        if (data.getContent().length !== 0){
+          callback(JSON.parse(data.getContent().toString().replace(/[\n\0]/g, "")).next);
+        } else {
+          callback([]);
+        }
+
+      }, function(interest){
+        console.warn("Interest timed out!", interest);
+        scope.createAlert("Request timed out. \"" + interest.getName().toUri() + "\" See console for details.");
+      });
+
+    }, function(interest){
+      console.error("Request failed! Timeout", interest);
+      scope.createAlert("Request timed out. \"" + interest.getName().toUri() + "\" See console for details.");
+    });
+
+  }
+
+  Atmos.prototype.showResults = function(resultIndex) {
+
+    var results = this.results.slice(this.resultsPerPage * resultIndex, this.resultsPerPage * (resultIndex + 1));
+
+    var resultDOM = $(
+      results.reduce(function(prev, current){
+        prev.push('<tr><td><input class="resultSelector" type="checkbox"></td><td>');
+        prev.push(current);
+        prev.push('</td></tr>');
+        return prev;
+      }, ['<tr><th><input id="resultSelectAll" type="checkbox" title="Select All"> Select</th><th>Name</th></tr>']).join('')
+    );
+
+    resultDOM.find('#resultSelectAll').click(function(){
+      if ($(this).is(':checked')){
+        resultDOM.find('.resultSelector:not([disabled])').prop('checked', true);
+      } else {
+        resultDOM.find('.resultSelector:not([disabled])').prop('checked', false);
+      }
+    });
+
+    this.resultTable.hide().empty().append(resultDOM).slideDown('slow');
+
+    this.resultMenu.find('.pageNumber').text(resultIndex + 1);
+    this.resultMenu.find('.pageLength').text(this.resultsPerPage * resultIndex + results.length);
+
+    if (this.resultsPerPage * (resultIndex + 1) >= this.resultCount) {
+      this.resultMenu.find('.next').addClass('disabled');
+    } else if (resultIndex === 0){
+      this.resultMenu.find('.next').removeClass('disabled');
+    }
+
+    if (resultIndex === 0){
+      this.resultMenu.find('.previous').addClass('disabled');
+    } else if (resultIndex === 1) {
+      this.resultMenu.find('.previous').removeClass('disabled');
+    }
+
+  }
+
+  Atmos.prototype.getResults = function(index){
+
+    if ($('#results').hasClass('hidden')){
+      $('#results').removeClass('hidden').slideDown();
+    }
+
+    if ((this.results.length === this.resultCount) || (this.resultsPerPage * (index + 1) < this.results.length)){
+      //console.log("We already have index", index);
+      this.page = index;
+      this.showResults(index);
+      return;
+    }
+
+    if (this.name === null) {
+      console.error("This shouldn't be reached! We are getting results before a search has occured!");
+      throw new Error("Illegal State");
+    }
+
+    var first = new Name(this.name).appendSegment(this.retrievedSegments++);
+
+    console.log("Requesting data index: (", this.retrievedSegments - 1, ") at ", first.toUri());
+
+    var scope = this;
+
+    var interest = new Interest(first)
+    interest.setInterestLifetimeMilliseconds(5000);
+    interest.setMustBeFresh(true);
+
+    this.face.expressInterest(interest,
+      function(interest, data){ //Response
+
+        if (data.getContent().length === 0){
+          scope.resultMenu.find('.totalResults').text(0);
+          scope.resultMenu.find('.pageNumber').text(0);
+          scope.resultMenu.find('.pageLength').text(0);
+          console.log("Empty response.");
+          return;
+        }
+
+        var content = JSON.parse(data.getContent().toString().replace(/[\n\0]/g,""));
+
+        if (!content.results){
+          scope.resultMenu.find('.totalResults').text(0);
+          scope.resultMenu.find('.pageNumber').text(0);
+          scope.resultMenu.find('.pageLength').text(0);
+          console.log("No results were found!");
+          return;
+        }
+
+        scope.results = scope.results.concat(content.results);
+
+        scope.resultCount = content.resultCount;
+
+        scope.resultMenu.find('.totalResults').text(scope.resultCount);
+
+        scope.page = index;
+
+        scope.getResults(index); //Keep calling this until we have enough data.
+
+      },
+      function(interest){ //Timeout
+        console.error("Failed to retrieve results: timeout", interest);
+        scope.createAlert("Request timed out. \"" + interest.getName().toUri() + "\" See console for details.");
+      }
+    );
+
+  }
+
+  Atmos.prototype.query = function(prefix, parameters, callback, timeout) {
+
+    var queryPrefix = new Name(prefix);
+    queryPrefix.append("query");
+
+    var jsonString = JSON.stringify(parameters);
+    queryPrefix.append(jsonString);
+
+    var queryInterest = new Interest(queryPrefix);
+    queryInterest.setInterestLifetimeMilliseconds(4000);
+    queryInterest.setMustBeFresh(true);
+
+    this.face.expressInterest(queryInterest, callback, timeout);
+
+  }
+
+  /**
+   * This function returns a map of all the categories active filters.
+   * @return {Object<string, string>}
+   */
+  Atmos.prototype.getFilters = function(){
+    var filters = this.filters.children().toArray().reduce(function(prev, current){
+      var data = $(current).text().split(/:/);
+      prev[data[0]] = data[1];
+      return prev;
+    }, {}); //Collect a map<category, filter>.
+    //TODO Make the return value map<category, Array<filter>>
+    return filters;
+  }
+
+  /**
+   * Creates a closable alert for the user.
+   *
+   * @param {string} message
+   * @param {string} type - Override the alert type.
+   */
+  Atmos.prototype.createAlert = function(message, type) {
+
+    var alert = $('<div class="alert"><div>');
+    alert.addClass(type?type:'alert-info');
+    alert.text(message);
+    alert.append(closeButton);
+
+    this.alerts.append(alert);
+  }
+
+  /**
+   * Requests all of the names represented by the buttons in the elements list.
+   *
+   * @param elements {Array<jQuery>} A list of the table row elements
+   */
+  Atmos.prototype.request = function(){
+
+    //Pseudo globals.
+    var keyChain;
+    var certificateName;
+    var keyAdded = false;
+
+    return function(elements){
+
+      var names = [];
+      var destination = $('#requestDest .active').text();
+      $(elements).find('>*:nth-child(2)').each(function(){
+        var name = $(this).text();
+        names.push(name);
+      });//.append('<span class="badge">Requested!</span>')
+      //Disabling the checkbox doesn't make sense anymore with the ability to request to multiple destinations.
+      //$(elements).find('.resultSelector').prop('disabled', true).prop('checked', false);
+
+      var scope = this;
+      this.requestForm.on('submit', function(e){ //This will be registered for the next submit from the form.
+        e.preventDefault();
+
+        //Form checking
+        var dest = scope.requestForm.find('#requestDest .active');
+        if (dest.length !== 1){
+          $('#requestForm').append($('<div class="alert alert-warning">A destination is required!' + closeButton + '<div>'));
+          return;
+        }
+
+        $('#request').modal('hide')//Initial params are ok. We can close the form.
+        .remove('.alert') //Remove any alerts
+        .find('.active').removeClass('active'); //Disable the active destination
+
+        $(this).off(e); //Don't fire this again, the request must be regenerated
+
+        //Key setup
+        if (!keyAdded){
+          if (!scope.config.retrieval.demoKey || !scope.config.retrieval.demoKey.pub || !scope.config.retrieval.demoKey.priv){
+            scope.createAlert("This host was not configured to handle retrieval! See console for details.", 'alert-danger');
+            console.error("Missing/invalid key! This must be configured in the config on the server.", scope.config.demoKey);
+            return;
+          }
+
+          //FIXME base64 may or may not exist in other browsers. Need a new polyfill.
+          var pub = new Buffer(base64.toByteArray(scope.config.retrieval.demoKey.pub)); //MUST be a Buffer (Buffer != Uint8Array)
+          var priv = new Buffer(base64.toByteArray(scope.config.retrieval.demoKey.priv));
+
+          var identityStorage = new MemoryIdentityStorage();
+          var privateKeyStorage = new MemoryPrivateKeyStorage();
+          keyChain = new KeyChain(new IdentityManager(identityStorage, privateKeyStorage),
+                        new SelfVerifyPolicyManager(identityStorage));
+
+          var keyName = new Name("/retrieve/DSK-123");
+          certificateName = keyName.getSubName(0, keyName.size() - 1)
+            .append("KEY").append(keyName.get(-1))
+            .append("ID-CERT").append("0");
+
+          identityStorage.addKey(keyName, KeyType.RSA, new Blob(pub, false));
+          privateKeyStorage.setKeyPairForKeyName(keyName, KeyType.RSA, pub, priv);
+
+          scope.face.setCommandSigningInfo(keyChain, certificateName);
+
+          keyAdded = true;
+
+        }
+
+        //Retrieval
+        var retrievePrefix = new Name("/catalog/ui/" + guid());
+
+        //Due to a lack of success callback in the register prefix function, we have to pretend we
+        //know it succeeded with an arbitrary timeout.
+        var sendTimer = setTimeout(function(){
+          var prefix = new Name(dest.text());
+          prefix.append(retrievePrefix);
+          var interest = new Interest(prefix);
+          interest.setInterestLifetimeMilliseconds(3000);
+          scope.face.expressInterest(interest,
+            function(interest, data){ //Success
+              console.log("Request for", prefix.toUri(), "succeeded.", interest, data);
+            }, function(interest){ //Failure
+              console.error("Failure to request", prefix.toUri(), interest);
+              scope.createAlert("Failed to request " + prefix.toUri() + ". This means that the retrieve failed! See console for more details.");
+            }
+          );
+        }, 10000); //Wait 10 seconds
+
+        scope.face.registerPrefix(retrievePrefix,
+          function(prefix, interest, face, interestFilterId, filter){ //On Interest
+            //This function will exist until the page exits but will likely only be used once.
+
+            var data = new Data(interest.getName());
+            var content = JSON.stringify(names);
+            data.setContent(content);
+            keyChain.sign(data, certificateName);
+
+            try {
+              face.putData(data);
+              console.log("Responded for", interest.getName().toUri(), data);
+              scope.createAlert("Data retrieval has initiated.", "alert-success");
+            } catch (e) {
+              console.error("Failed to respond to", interest.getName().toUri(), data);
+              scope.createAlert("Data retrieval failed.");
+            }
+
+          }, function(prefix){ //On fail
+            clearTimeout(sendTimer); //Cancel the earlier request timer
+            scope.createAlert("Failed to register the retrieval URI! See console for details.", "alert-danger");
+            console.error("Failed to register URI:", prefix.toUri(), prefix);
+
+          }
+        );
+
+      });
+      $('#request').modal(); //This forces the form to be the only option.
+
+    }
+  }();
+
+  Atmos.prototype.filterSetup = function() {
+    //Filter setup
+
+    var prefix = new Name(this.catalog).append("filters-initialization");
+
+    var scope = this;
+
+    this.getAll(prefix, function(data) { //Success
+      var raw = JSON.parse(data.replace(/[\n\0]/g, '')); //Remove null byte and parse
+
+      console.log("Filter categories:", raw);
+
+      $.each(raw, function(index, object){ //Unpack list of objects
+        $.each(object, function(category, searchOptions) { //Unpack category from object (We don't know what it is called)
+          //Create the category
+          var e = $('<li><a href="#">' + category.replace(/_/g, " ") + '</a><ul class="subnav nav nav-pills nav-stacked"></ul></li>');
+
+          var sub = e.find('ul.subnav');
+          $.each(searchOptions, function(index, name){
+            //Create the filter list inside the category
+            var item = $('<li><a href="#">' + name + '</a></li>');
+            sub.append(item);
+            item.click(function(){ //Click on the side menu filters
+              if (item.hasClass('active')){ //Does the filter already exist?
+                item.removeClass('active');
+                scope.filters.find(':contains(' + category + ':' + name + ')').remove();
+              } else { //Add a filter
+                item.addClass('active');
+                var filter = $('<span class="label label-default"></span>');
+                filter.text(category + ':' + name);
+
+                scope.filters.append(filter);
+
+                filter.click(function(){ //Click on a filter
+                  filter.remove();
+                  item.removeClass('active');
+                });
+              }
+
+            });
+          });
+
+          //Toggle the menus. (Only respond when the immediate tab is clicked.)
+          e.find('> a').click(function(){
+            scope.categories.find('.subnav').slideUp();
+            var t = $(this).siblings('.subnav');
+            if ( !t.is(':visible') ){ //If the sub menu is not visible
+              t.slideDown(function(){
+                t.triggerHandler('focus');
+              }); //Make it visible and look at it.
+            }
+          });
+
+          scope.categories.append(e);
+
+        });
+      });
+
+    }, function(interest){ //Timeout
+      scope.createAlert("Failed to initialize the filters!", "alert-danger");
+      console.error("Failed to initialize filters!", interest);
+      ga('send', 'event', 'error', 'filters');
+    });
+
+  }
+
+  /**
+   * This function retrieves all segments in order until it knows it has reached the last one.
+   * It then returns the final joined result.
+   */
+  Atmos.prototype.getAll = function(prefix, callback, timeout){
+
+    var scope = this;
+    var d = [];
+
+    var request = function(segment){
+
+      var name = new Name(prefix);
+      name.appendSegment(segment);
+
+      var interest = new Interest(name);
+      interest.setInterestLifetimeMilliseconds(1000);
+      interest.setMustBeFresh(true); //Is this needed?
+
+      scope.face.expressInterest(interest, handleData, timeout);
+
+    }
+
+
+    var handleData = function(interest, data){
+
+      d.push(data.getContent().toString());
+
+      if (interest.getName().get(-1).toSegment() == data.getMetaInfo().getFinalBlockId().toSegment()){
+        callback(d.join(""));
+      } else {
+        request(interest.getName().toSegment()++);
+      }
+
+    }
+
+    request(0);
+
+  }
+
+  Atmos.prototype.setupRequestForm = function(){
+    this.requestForm.find('#requestCancel').click(function(){
+      $('#request').unbind('submit') //Removes all event handlers.
+      .modal('hide'); //Hides the form.
+    });
+
+    var dests = $(this.config['retrieval']['destinations'].reduce(function(prev, current){
+      prev.push('<li><a href="#">');
+      prev.push(current);
+      prev.push("</a></li>");
+      return prev;
+    }, []).join(""));
+
+    this.requestForm.find('#requestDest').append(dests)
+    .on('click', 'a', function(e){
+      $('#requestDest .active').removeClass('active');
+      $(this).parent().addClass('active');
+    });
+
+    //This code will remain unused until users must use their own keys instead of the demo key.
+//    var scope = this;
+
+//    var warning = '<div class="alert alert-warning">' + closeButton + '<div>';
+
+//    var handleFile = function(e){
+//      var t = $(this);
+//      if (e.target.files.length > 1){
+//        var el = $(warning);
+//        t.append(el.append("We are looking for a single file, we will try the first only!"));
+//      } else if (e.target.files.length === 0) {
+//        var el = $(warning.replace("alert-warning", "alert-danger"));
+//        t.append(el.append("No file was supplied!"));
+//        return;
+//      }
+
+//      var reader = new FileReader();
+//      reader.onload = function(e){
+//        var key;
+//        try {
+//          key = JSON.parse(e.target.result);
+//        } catch (e) {
+//          console.error("Could not parse the key! (", key, ")");
+//          var el = $(warning.replace("alert-warning", "alert-danger"));
+//          t.append(el.append("Failed to parse the key file, is it a valid json key?"));
+//        }
+
+//        if (!key.DEFAULT_RSA_PUBLIC_KEY_DER || !key.DEFAULT_RSA_PRIVATE_KEY_DER) {
+//          console.warn("Invalid key", key);
+//          var el = $(warning.replace("alert-warning", "alert-danger"));
+//          t.append(el.append("Failed to parse the key file, it is missing required attributes."));
+//        }
+
+
+//      };
+
+//    }
+
+//    this.requestForm.find('#requestDrop').on('dragover', function(e){
+//      e.dataTransfer.dropEffect = 'copy';
+//    }).on('drop', handleFile);
+
+//    this.requestForm.find('input[type=file]').change(handleFile);
+
+  }
+
+  return Atmos;
+
+})();
diff --git a/client/catalog-dev/js/treeExplorer.js b/client/catalog-dev/js/treeExplorer.js
new file mode 100644
index 0000000..5037629
--- /dev/null
+++ b/client/catalog-dev/js/treeExplorer.js
@@ -0,0 +1,73 @@
+
+(function(){
+  "use strict";
+  jQuery.fn.extend({
+
+    treeExplorer: function(getChildren){
+      var cache = {}; //Cache previously requested paths.
+
+      var tree = $('<div class="treeExplorer"></div>');
+      this.append(tree);
+
+      var lookup = function(path, callback){
+        if (cache[path]){
+          callback(path, cache[path]);
+        } else {
+          getChildren(path, function(children){
+            cache[path] = children;
+            callback(path, children);
+          });
+        }
+      }
+
+      var append = function(path, children, node){
+        var c = $('<div class="nodeChildren"></div>');
+        node.append(c);
+        children.forEach(function(current){
+          var el = $('<div class="treeExplorerNode"></div>');
+          if (current.match(/\/$/)){
+            el.attr('id', path + current);
+            el.append(['<a href="#' , path , current , '">' , current , '</a>'].join(""));
+          } else {
+            el.addClass('file');
+            el.text(current);
+          }
+          c.append(el);
+        });
+      }
+
+      tree.on('click', '.treeExplorerNode > a', function(){
+        var node = $(this).parent();
+
+        if (node.hasClass('open')){ //Are we open already?
+          node.removeClass('open');
+          return;
+        } else { //We need to open
+          if (node.find('.treeExplorerNode').length > 0){ //We already have children
+            node.addClass('open');
+          } else { //We need to get the children.
+            var path = node.attr('id');
+            lookup(path, function(path, children){
+              if (children.length === 0){
+                node.addClass('file');
+                var name = node.find('a').text().replace(/\/$/, "");
+                node.empty().text(name);
+              } else {
+                append(path, children, node);
+                node.addClass('open');
+              }
+            });
+          }
+        }
+
+      });
+
+      getChildren("/", function(children){
+        append("", children, tree);
+      });
+
+      return this;
+
+    }
+  })
+})();