text: /*
  Copyright 2008-2010 University of Cambridge
  Copyright 2008-2009 University of Toronto
  Copyright 2010-2011 Lucendo Development Ltd.
  Copyright 2010 OCAD University
  
  Licensed under the Educational Community License (ECL), Version 2.0 or the New
  BSD license. You may not use this file except in compliance with one these
  Licenses.
  
  You may obtain a copy of the ECL 2.0 License and BSD License at
  https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
  */
  
  // Declare dependencies
  /*global fluid_1_5:true, jQuery*/
  
  // JSLint options 
  /*jslint white: true, funcinvoke: true, continue: true, elsecatch: true, operator: true, jslintok:true, undef: true, newcap: true, regexp: true, bitwise: true, browser: true, forin: true, maxerr: 100, indent: 4 */
  
  var fluid_1_5 = fluid_1_5 || {};
  
  (function ($, fluid) {
      
      fluid.BINDING_ROOT_KEY = "fluid-binding-root";
      
      /** Recursively find any data stored under a given name from a node upwards
       * in its DOM hierarchy **/
       
      fluid.findData = function (elem, name) {
          while (elem) {
              var data = $.data(elem, name);
              if (data) {
                  return data;
              }
              elem = elem.parentNode;
          }
      };
    
      fluid.bindFossils = function (node, data, fossils) {
          $.data(node, fluid.BINDING_ROOT_KEY, {data: data, fossils: fossils});
      };
          
      fluid.boundPathForNode = function (node, fossils) {
          node = fluid.unwrap(node);
          var key = node.name || node.id;
          var record = fossils[key];
          return record ? record.EL : null;
      };
        
     /** "Automatically" apply to whatever part of the data model is
       * relevant, the changed value received at the given DOM node*/
      fluid.applyBoundChange = function (node, newValue, applier) {
          node = fluid.unwrap(node);
          if (newValue === undefined) {
              newValue = fluid.value(node);
          }
          if (node.nodeType === undefined && node.length > 0) {
              node = node[0];
          } // assume here that they share name and parent
          var root = fluid.findData(node, fluid.BINDING_ROOT_KEY);
          if (!root) {
              fluid.fail("Bound data could not be discovered in any node above " + fluid.dumpEl(node));
          }
          var name = node.name;
          var fossil = root.fossils[name];
          if (!fossil) {
              fluid.fail("No fossil discovered for name " + name + " in fossil record above " + fluid.dumpEl(node));
          }
          if (typeof(fossil.oldvalue) === "boolean") { // deal with the case of an "isolated checkbox"
              newValue = newValue[0] ? true : false;
          }
          var EL = root.fossils[name].EL;
          if (applier) {
              applier.fireChangeRequest({path: EL, value: newValue, source: node.id});
          } else {
              fluid.set(root.data, EL, newValue);
          }    
      };
      
      /** MODEL ACCESSOR ENGINE (trundler) **/
      
      /** Standard strategies for resolving path segments **/
      fluid.model.makeEnvironmentStrategy = function (environment) {
          return function (root, segment, index) {
              return index === 0 && environment[segment] ?
                  environment[segment] : undefined; 
          };
      };
  
      fluid.model.defaultCreatorStrategy = function (root, segment) {
          if (root[segment] === undefined) {
              root[segment] = {};
              return root[segment];
          }
      };
      
      fluid.model.defaultFetchStrategy = function (root, segment) {
          return segment === "" ? root : root[segment];
      };
          
      fluid.model.funcResolverStrategy = function (root, segment) {
          if (root.resolvePathSegment) {
              return root.resolvePathSegment(segment);
          }
      };
      
          
      fluid.model.defaultGetConfig = {
          strategies: [fluid.model.funcResolverStrategy, fluid.model.defaultFetchStrategy]
      };
  
      fluid.model.defaultSetConfig = {
          strategies: [fluid.model.funcResolverStrategy, fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
      };
      
      // unsupported, NON-API function
      fluid.model.traverseWithStrategy = function (root, segs, initPos, config, uncess) {
          var strategies = config.strategies;
          var limit = segs.length - uncess;
          for (var i = initPos; i < limit; ++i) {
              if (!root) {
                  return root;
              }
              var accepted = undefined;
              for (var j = 0; j < strategies.length; ++ j) {
                  accepted = strategies[j](root, segs[i], i + 1, segs);
                  if (accepted !== undefined) {
                      break; // May now short-circuit with stateless strategies
                  }
              }
              if (accepted === fluid.NO_VALUE) {
                  accepted = undefined;
              }
              root = accepted;
          }
          return root;
      };
      
      /** Returns both the value and the path of the value held at the supplied EL path **/
      fluid.model.getValueAndSegments = function (root, EL, config, initSegs) {
          return fluid.model.accessWithStrategy(root, EL, fluid.NO_VALUE, config, initSegs, true);
      };
      
      // Very lightweight remnant of trundler, only used in resolvers
      // unsupported, NON-API function
      fluid.model.makeTrundler = function (config) {
          return function (valueSeg, EL) {
              return fluid.model.getValueAndSegments(valueSeg.root, EL, config, valueSeg.segs); 
          };  
      };
      
      fluid.model.getWithStrategy = function (root, EL, config, initSegs) {
          return fluid.model.accessWithStrategy(root, EL, fluid.NO_VALUE, config, initSegs);
      };
      
      fluid.model.setWithStrategy = function (root, EL, newValue, config, initSegs) {
          fluid.model.accessWithStrategy(root, EL, newValue, config, initSegs);
      };
      
      // unsupported, NON-API function
      fluid.model.accessWithStrategy = function (root, EL, newValue, config, initSegs, returnSegs) {
          // This function is written in this unfortunate style largely for efficiency reasons. In many cases
          // it should be capable of running with 0 allocations (EL is preparsed, initSegs is empty)
          if (!fluid.isPrimitive(EL) && !fluid.isArrayable(EL)) {
              var key = EL.type || "default";
              var resolver = config.resolvers[key];
              if (!resolver) {
                  fluid.fail("Unable to find resolver of type " + key);
              }
              var trundler = fluid.model.makeTrundler(config); // very lightweight trundler for resolvers
              var valueSeg = {root: root, segs: initSegs};
              valueSeg = resolver(valueSeg, EL, trundler);
              if (EL.path && valueSeg) { // every resolver supports this piece of output resolution
                  valueSeg = trundler(valueSeg, EL.path);
              }
              return returnSegs ? valueSeg : (valueSeg ? valueSeg.root : undefined);
          }
          else {
              return fluid.model.accessImpl(root, EL, newValue, config, initSegs, returnSegs, fluid.model.traverseWithStrategy);
          }
      };
    
      // Implementation notes: The EL path manipulation utilities here are somewhat more thorough
      // and expensive versions of those provided in Fluid.js - there is some duplication of 
      // functionality. This is a tradeoff between stability and performance - the versions in
      // Fluid.js are the most frequently used and do not implement escaping of characters .
      // as \. and \ as \\ as the versions here. The implementations here are not 
      // performant and are left here partially as an implementation note. Problems will
      // arise if clients manipulate JSON structures containing "." characters in keys as if they
      // are models. The basic  utilities fluid.path(), fluid.parseEL and fluid.composePath are 
      // the ones recommended for general users and the following implementations will
      // be upgraded to use regexes in future to make them better alternatives
     
      fluid.pathUtil = {};
     
      var getPathSegmentImpl = function (accept, path, i) {
          var segment = null; // TODO: rewrite this with regexes and replaces
          if (accept) {
              segment = "";
          }
          var escaped = false;
          var limit = path.length;
          for (; i < limit; ++i) {
              var c = path.charAt(i);
              if (!escaped) {
                  if (c === '.') {
                      break;
                  }
                  else if (c === '\\') {
                      escaped = true;
                  }
                  else if (segment !== null) {
                      segment += c;
                  }
              }
              else {
                  escaped = false;
                  if (segment !== null) {
                      segment += c;
                  }
              }
          }
          if (segment !== null) {
              accept[0] = segment;
          }
          return i;
      };
      
      var globalAccept = []; // TODO: serious reentrancy risk here, why is this impl like this?
      
      /** Parses a path segment, following escaping rules, starting from character index i in the supplied path */
      fluid.pathUtil.getPathSegment = function (path, i) {
          getPathSegmentImpl(globalAccept, path, i);
          return globalAccept[0];
      }; 
    
      /** Returns just the head segment of an EL path */
      fluid.pathUtil.getHeadPath = function (path) {
          return fluid.pathUtil.getPathSegment(path, 0);
      };
    
      /** Returns all of an EL path minus its first segment - if the path consists of just one segment, returns "" */  
      fluid.pathUtil.getFromHeadPath = function (path) {
          var firstdot = getPathSegmentImpl(null, path, 0);
          return firstdot === path.length ? "" : path.substring(firstdot + 1);
      };
      
      function lastDotIndex(path) {
          // TODO: proper escaping rules
          return path.lastIndexOf(".");
      }
  
      /** Returns all of an EL path minus its final segment - if the path consists of just one segment, returns "" - 
       * WARNING - this method does not follow escaping rules */    
      fluid.pathUtil.getToTailPath = function (path) {
          var lastdot = lastDotIndex(path);
          return lastdot === -1 ? "" : path.substring(0, lastdot);
      };
  
      /** Returns the very last path component of an EL path 
       * WARNING - this method does not follow escaping rules */
      fluid.pathUtil.getTailPath = function (path) {
          var lastdot = lastDotIndex(path);
          return fluid.pathUtil.getPathSegment(path, lastdot + 1);
      };
  
      /** A version of fluid.model.parseEL that apples escaping rules - this allows path segments
       * to contain period characters . - characters "\" and "}" will also be escaped. WARNING - 
       * this current implementation is EXTREMELY slow compared to fluid.model.parseEL and should
       * not be used in performance-sensitive applications */
       
      fluid.pathUtil.parseEL = function (path) {
          var togo = [];
          var index = 0;
          var limit = path.length;
          while (index < limit) {
              var firstdot = getPathSegmentImpl(globalAccept, path, index);
              togo.push(globalAccept[0]);
              index = firstdot + 1;
          }
          return togo;
      };
      
      var composeSegment = function (prefix, toappend) {
          for (var i = 0; i < toappend.length; ++i) {
              var c = toappend.charAt(i);
              if (c === '.' || c === '\\' || c === '}') {
                  prefix += '\\';
              }
              prefix += c;
          }
          return prefix;
      };
      
      /** Escapes a single path segment by replacing any character ".", "\" or "}" with
       * itself prepended by \
       */
      fluid.pathUtil.escapeSegment = function (segment) {
          return composeSegment("", segment);  
      };
      
      /**
       * Compose a prefix and suffix EL path, where the prefix is already escaped.
       * Prefix may be empty, but not null. The suffix will become escaped.
       */
      fluid.pathUtil.composePath = function (prefix, suffix) {
          if (prefix.length !== 0) {
              prefix += '.';
          }
          return composeSegment(prefix, suffix);
      };
      
      /** Helpful utility for use in resolvers - matches a path which has already been
        * parsed into segments **/
      
      fluid.pathUtil.matchSegments = function (toMatch, segs, start, end) {
          if (end - start !== toMatch.length) {
              return false;
          }
          for (var i = start; i < end; ++ i) {
              if (segs[i] !== toMatch[i - start]) {
                  return false;
              }
          }
          return true;
      };
      
      /** Determine the path by which a given path is nested within another **/
      
      fluid.pathUtil.getExcessPath = function (base, longer) {
          var index = longer.indexOf(base);
          if (index !== 0) {
              fluid.fail("Path " + base + " is not a prefix of path " + longer);
          }
          if (base.length === longer.length) {
              return "";
          }
          if (longer[base.length] !== ".") {
              fluid.fail("Path " + base + " is not properly nested in path " + longer);
          }
          return longer.substring(base.length + 1);
      };
      
      /** Determines whether a particular EL path matches a given path specification.
       * The specification consists of a path with optional wildcard segments represented by "*".
       * @param spec (string) The specification to be matched
       * @param path (string) The path to be tested
       * @param exact (boolean) Whether the path must exactly match the length of the specification in
       * terms of path segments in order to count as match. If exact is falsy, short specifications will
       * match all longer paths as if they were padded out with "*" segments 
       * @return (string) The path which matched the specification, or <code>null</code> if there was no match
       */
     
      fluid.pathUtil.matchPath = function (spec, path, exact) {
          var togo = "";
          while (true) {
              if (((path === "") ^ (spec === "")) && exact) {
                  return null;
              }
              // FLUID-4625 - symmetry on spec and path is actually undesirable, but this
              // quickly avoids at least missed notifications - improved (but slower) 
              // implementation should explode composite changes
              if (!spec || !path) {
                  break;
              }
              var spechead = fluid.pathUtil.getHeadPath(spec);
              var pathhead = fluid.pathUtil.getHeadPath(path);
              // if we fail to match on a specific component, fail.
              if (spechead !== "*" && spechead !== pathhead) {
                  return null;
              }
              togo = fluid.pathUtil.composePath(togo, pathhead);
              spec = fluid.pathUtil.getFromHeadPath(spec);
              path = fluid.pathUtil.getFromHeadPath(path);
          }
          return togo;
      };
          
      /** CHANGE APPLIER **/    
        
      fluid.model.isNullChange = function (model, request, resolverGetConfig) {
          if (request.type === "ADD") {
              var existing = fluid.get(model, request.path, resolverGetConfig);
              if (existing === request.value) {
                  return true;
              }
          }
      };
      /** Applies the supplied ChangeRequest object directly to the supplied model.
       */
      fluid.model.applyChangeRequest = function (model, request, resolverSetConfig) {
          var pen = fluid.model.accessWithStrategy(model, request.path, fluid.VALUE, resolverSetConfig || fluid.model.defaultSetConfig, null, true);
          var last = pen.segs[pen.segs.length - 1];
          
          if (request.type === "ADD" || request.type === "MERGE") {
              if (pen.segs.length === 0 || (request.type === "MERGE" && pen.root[last])) {
                  if (request.type === "ADD") {
                      fluid.clear(pen.root);
                  }
                  $.extend(true, pen.segs.length === 0 ? pen.root : pen.root[last], request.value);
              }
              else {
                  pen.root[last] = request.value;
              }
          }
          else if (request.type === "DELETE") {
              if (pen.segs.length === 0) {
                  fluid.clear(pen.root);
              }
              else {
                  delete pen.root[last];
              }
          }
      };
            
      /** Add a listener to a ChangeApplier event that only acts in the case the event
       * has not come from the specified source (typically ourself)
       * @param modelEvent An model event held by a changeApplier (typically applier.modelChanged)
       * @param path The path specification to listen to
       * @param source The source value to exclude (direct equality used)
       * @param func The listener to be notified of a change
       * @param [eventName] - optional - the event name to be listened to - defaults to "modelChanged" 
       */
      fluid.addSourceGuardedListener = function(applier, path, source, func, eventName) {
          eventName = eventName || "modelChanged";
          applier[eventName].addListener(path, 
              function() {
                  if (!applier.hasChangeSource(source)) {
                      func.apply(null, arguments);
                  }
              });
      };
  
      /** Convenience method to fire a change event to a specified applier, including
       * a supplied "source" identified (perhaps for use with addSourceGuardedListener)
       */ 
      fluid.fireSourcedChange = function (applier, path, value, source) {
          applier.fireChangeRequest({
              path: path,
              value: value,
              source: source
          });         
      };
      
      /** Dispatches a list of changes to the supplied applier */
      fluid.requestChanges = function (applier, changes) {
          for (var i = 0; i < changes.length; ++i) {
              applier.fireChangeRequest(changes[i]);
          }  
      };
      
    
      // Automatically adapts requestChange onto fireChangeRequest
      // unsupported, NON-API function    
      fluid.bindRequestChange = function (that) {
          that.requestChange = function (path, value, type) {
              var changeRequest = {
                  path: path,
                  value: value,
                  type: type
              };
              that.fireChangeRequest(changeRequest);
          };
      };
      
      // Utility used for source tracking in changeApplier
      
      function sourceWrapModelChanged(modelChanged, threadLocal) {
          return function (changeRequest) {
              var sources = threadLocal().sources;
              var args = arguments;
              var source = changeRequest.source || "";
              fluid.tryCatch(function () {
                  if (sources[source] === undefined) {
                      sources[source] = 0;
                  }
                  ++sources[source];
                  modelChanged.apply(null, args);
              }, null, function() {
                  --sources[source];
              });
          };
      }
    
      /** The core creator function constructing ChangeAppliers. See API documentation
       * at http://wiki.fluidproject.org/display/fluid/ChangeApplier+API for the various
       * options supported in the options structure */
       
      fluid.makeChangeApplier = function (model, options) {
          options = options || {};
          var baseEvents = {
              guards: fluid.event.getEventFirer(false, true, "guard event"),
              postGuards: fluid.event.getEventFirer(false, true, "postGuard event"),
              modelChanged: fluid.event.getEventFirer(false, false, "modelChanged event")
          };
          var threadLocal = fluid.threadLocal(function() { return {sources: {}};});
          var that = {
          // For now, we don't use "id" to avoid confusing component detection which uses
          // a simple algorithm looking for that field
              changeid: fluid.allocateGuid(),
              model: model
          };
          
          function makeGuardWrapper(cullUnchanged) {
              if (!cullUnchanged) {
                  return null;
              }
              var togo = function (guard) {
                  return function (model, changeRequest, internalApplier) {
                      var oldRet = guard(model, changeRequest, internalApplier);
                      if (oldRet === false) {
                          return false;
                      }
                      else {
                          if (fluid.model.isNullChange(model, changeRequest)) {
                              togo.culled = true;
                              return false;
                          }
                      }
                  };
              };
              return togo;
          }
  
          function wrapListener(listener, spec) {
              var pathSpec = spec;
              var transactional = false;
              var priority = Number.MAX_VALUE;
              if (typeof (spec) !== "string") {
                  pathSpec = spec.path;
                  transactional = spec.transactional;
                  if (spec.priority !== undefined) {
                      priority = spec.priority;
                  }
              }
              else {
                  if (pathSpec.charAt(0) === "!") {
                      transactional = true;
                      pathSpec = pathSpec.substring(1);
                  }
              }
              var wrapped = function (changePath, fireSpec, accum) {
                  var guid = fluid.event.identifyListener(listener);
                  var exist = fireSpec.guids[guid];
                  if (!exist) {
                      var match = fluid.pathUtil.matchPath(pathSpec, changePath);
                      if (match !== null) {
                          var record = {
                              changePath: changePath,
                              pathSpec: pathSpec,
                              listener: listener,
                              priority: priority,
                              transactional: transactional
                          };
                          if (accum) {
                              record.accumulate = [accum];
                          }
                          fireSpec.guids[guid] = record;
                          var collection = transactional ? "transListeners" : "listeners";
                          fireSpec[collection].push(record);
                          fireSpec.all.push(record);
                      }
                  }
                  else if (accum) {
                      if (!exist.accumulate) {
                          exist.accumulate = [];
                      }
                      exist.accumulate.push(accum);
                  }
              };
              fluid.event.impersonateListener(listener, wrapped);
              return wrapped;
          }
          
          function fireFromSpec(name, fireSpec, args, category, wrapper) {
              return baseEvents[name].fireToListeners(fireSpec[category], args, wrapper);
          }
          
          function fireComparator(recA, recB) {
              return recA.priority - recB.priority;
          }
  
          function prepareFireEvent(name, changePath, fireSpec, accum) {
              baseEvents[name].fire(changePath, fireSpec, accum);
              fireSpec.all.sort(fireComparator);
              fireSpec.listeners.sort(fireComparator);
              fireSpec.transListeners.sort(fireComparator);
          }
          
          function makeFireSpec() {
              return {guids: {}, all: [], listeners: [], transListeners: []};
          }
          
          function getFireSpec(name, changePath) {
              var fireSpec = makeFireSpec();
              prepareFireEvent(name, changePath, fireSpec);
              return fireSpec;
          }
          
          function fireEvent(name, changePath, args, wrapper) {
              var fireSpec = getFireSpec(name, changePath);
              return fireFromSpec(name, fireSpec, args, "all", wrapper);
          }
          
          function adaptListener(that, name) {
              that[name] = {
                  addListener: function (spec, listener, namespace) {
                      baseEvents[name].addListener(wrapListener(listener, spec), namespace);
                  },
                  removeListener: function (listener) {
                      baseEvents[name].removeListener(listener);
                  }
              };
          }
          adaptListener(that, "guards");
          adaptListener(that, "postGuards");
          adaptListener(that, "modelChanged");
          
          function preFireChangeRequest(changeRequest) {
              if (!changeRequest.type) {
                  changeRequest.type = "ADD";
              }
          }
  
          var bareApplier = {
              fireChangeRequest: function (changeRequest) {
                  that.fireChangeRequest(changeRequest, true);
              }
          };
          fluid.bindRequestChange(bareApplier);
  
          that.fireChangeRequest = function (changeRequest, defeatGuards) {
              preFireChangeRequest(changeRequest);
              var guardFireSpec = defeatGuards ? null : getFireSpec("guards", changeRequest.path);
              if (guardFireSpec && guardFireSpec.transListeners.length > 0) {
                  var ation = that.initiate();
                  ation.fireChangeRequest(changeRequest, guardFireSpec);
                  ation.commit();
              }
              else {
                  if (!defeatGuards) {
                      // TODO: this use of "listeners" seems pointless since we have just verified that there are no transactional listeners
                      var prevent = fireFromSpec("guards", guardFireSpec, [model, changeRequest, bareApplier], "listeners");
                      if (prevent === false) {
                          return false;
                      }
                  }
                  var oldModel = model;
                  if (!options.thin) {
                      oldModel = {};
                      fluid.model.copyModel(oldModel, model);                    
                  }
                  fluid.model.applyChangeRequest(model, changeRequest, options.resolverSetConfig);
                  fireEvent("modelChanged", changeRequest.path, [model, oldModel, [changeRequest]]);
              }
          };
          
          that.fireChangeRequest = sourceWrapModelChanged(that.fireChangeRequest, threadLocal);
          fluid.bindRequestChange(that);
  
          function fireAgglomerated(eventName, formName, changes, args, accpos) {
              var fireSpec = makeFireSpec();
              for (var i = 0; i < changes.length; ++i) {
                  prepareFireEvent(eventName, changes[i].path, fireSpec, changes[i]);
              }
              for (var j = 0; j < fireSpec[formName].length; ++j) {
                  var spec = fireSpec[formName][j];
                  if (accpos) {
                      args[accpos] = spec.accumulate;
                  }
                  var ret = spec.listener.apply(null, args);
                  if (ret === false) {
                      return false;
                  }
              }
          }
  
          that.initiate = function (newModel) {
              var cancelled = false;
              var changes = [];
              if (options.thin) {
                  newModel = model;
              }
              else {
                  newModel = newModel || {};
                  fluid.model.copyModel(newModel, model);
              }
              // the guard in the inner world is given a private applier to "fast track"
              // and glob collateral changes it requires
              var internalApplier = {
                  fireChangeRequest: function (changeRequest) {
                      preFireChangeRequest(changeRequest);
                      fluid.model.applyChangeRequest(newModel, changeRequest, options.resolverSetConfig);
                      changes.push(changeRequest);
                  }
              };
              fluid.bindRequestChange(internalApplier);
              var ation = {
                  commit: function () {
                      var oldModel;
                      if (cancelled) {
                          return false;
                      }
                      var ret = fireAgglomerated("postGuards", "transListeners", changes, [newModel, null, internalApplier], 1);
                      if (ret === false) {
                          return false;
                      }
                      if (options.thin) {
                          oldModel = model;
                      }
                      else {
                          oldModel = {};
                          fluid.model.copyModel(oldModel, model);
                          fluid.clear(model);
                          fluid.model.copyModel(model, newModel);
                      }
                      fireAgglomerated("modelChanged", "all", changes, [model, oldModel, null], 2);
                  },
                  fireChangeRequest: function (changeRequest) {
                      preFireChangeRequest(changeRequest);
                      if (options.cullUnchanged && fluid.model.isNullChange(model, changeRequest, options.resolverGetConfig)) {
                          return;
                      } 
                      var wrapper = makeGuardWrapper(options.cullUnchanged);
                      var prevent = fireEvent("guards", changeRequest.path, [newModel, changeRequest, internalApplier], wrapper);
                      if (prevent === false && !(wrapper && wrapper.culled)) {
                          cancelled = true;
                      }
                      if (!cancelled) {
                          if (!(wrapper && wrapper.culled)) {
                              fluid.model.applyChangeRequest(newModel, changeRequest, options.resolverSetConfig);
                              changes.push(changeRequest);
                          }
                      }
                  }
              };
              
              ation.fireChangeRequest = sourceWrapModelChanged(ation.fireChangeRequest, threadLocal);
              fluid.bindRequestChange(ation);
  
              return ation;
          };
          
          that.hasChangeSource = function (source) {
              return threadLocal().sources[source] > 0;
          };
          
          return that;
      };
      
      fluid.makeSuperApplier = function () {
          var subAppliers = [];
          var that = {};
          that.addSubApplier = function (path, subApplier) {
              subAppliers.push({path: path, subApplier: subApplier});
          };
          that.fireChangeRequest = function (request) {
              for (var i = 0; i < subAppliers.length; ++i) {
                  var path = subAppliers[i].path;
                  if (request.path.indexOf(path) === 0) {
                      var subpath = request.path.substring(path.length + 1);
                      var subRequest = fluid.copy(request);
                      subRequest.path = subpath;
                      // TODO: Deal with the as yet unsupported case of an EL rvalue DAR
                      subAppliers[i].subApplier.fireChangeRequest(subRequest);
                  }
              }
          };
          fluid.bindRequestChange(that);
          return that;
      };
      
      fluid.attachModel = function (baseModel, path, model) {
          var segs = fluid.model.parseEL(path);
          for (var i = 0; i < segs.length - 1; ++i) {
              var seg = segs[i];
              var subModel = baseModel[seg];
              if (!subModel) {
                  baseModel[seg] = subModel = {};
              }
              baseModel = subModel;
          }
          baseModel[segs[segs.length - 1]] = model;
      };
      
      fluid.assembleModel = function (modelSpec) {
          var model = {};
          var superApplier = fluid.makeSuperApplier();
          var togo = {model: model, applier: superApplier};
          for (var path in modelSpec) {
              var rec = modelSpec[path];
              fluid.attachModel(model, path, rec.model);
              if (rec.applier) {
                  superApplier.addSubApplier(path, rec.applier);
              }
          }
          return togo;
      };
  
  })(jQuery, fluid_1_5);
  