@gmod/jbrowse
Version:
JBrowse - client-side genome browser
324 lines (280 loc) • 9.5 kB
JavaScript
define(
[
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/Deferred',
'dojo/promise/all',
'JBrowse/Util',
'JBrowse/ConfigAdaptor/AdaptorUtil'
],
function(
declare,
lang,
array,
Deferred,
all,
Util,
AdaptorUtil
) {
return declare(null,
/**
* @lends JBrowse.ConfigManager.prototype
*/
{
/**
* @constructs
*/
constructor: function( args ) {
this.bootConfig = lang.clone( args.bootConfig || {} );
this.defaults = lang.clone( args.defaults || {} );
this.browser = args.browser;
this.skipValidation = args.skipValidation;
this.bootConfig = (this._regularizeIncludes ([ this.bootConfig ])) [0];
if(this.bootConfig.cacheBuster === false) {
this.bootConfig.cacheBuster = false
} else {
this.bootConfig.cacheBuster = true
}
var thisB = this;
this._getConfigAdaptor( this.bootConfig )
.then( function( adaptor ) {
thisB.bootConfig = adaptor.regularizeTrackConfigs ( thisB.bootConfig );
});
// this.topLevelIncludes = this._fillTemplates(
// lang.clone( this.config.include || this.defaults.include ),
// this._applyDefaults( lang.clone( this.config ), this.defaults )
// );
// delete this.defaults.include;
// delete this.config.include;
},
/**
* @param callback {Function} callback, receives a single arguments,
* which is the final processed configuration object
*/
getFinalConfig: function() {
return this.finalConfig || ( this.finalConfig = function() {
var thisB = this;
var bootstrapConf = this._applyDefaults( lang.clone( this.bootConfig ), this.defaults );
return this._loadIncludes( bootstrapConf )
.then( function( includedConfig ) {
// merge the boot config *into* the included config last, so
// that values in the boot config override the others
var finalConf = thisB._mergeConfigs( includedConfig, thisB.bootConfig );
thisB._fillTemplates( finalConf, finalConf );
finalConf = AdaptorUtil.evalHooks( finalConf );
if( ! thisB.skipValidation )
thisB._validateConfig( finalConf );
return finalConf;
});
}.call(this) );
},
/**
* Instantiate the right config adaptor for a given configuration source.
* @param {Object} config the configuraiton
* @param {Function} callback called with the new config object
* @returns {Object} the right configuration adaptor to use, or
* undefined if one could not be found
* @private
*/
_getConfigAdaptor: function( config_def, callback ) {
var adaptor_name = "JBrowse/ConfigAdaptor/" + config_def.format;
if( 'version' in config_def )
adaptor_name += '_v'+config_def.version;
adaptor_name.replace( /\W/g,'' );
return Util.loadJS( [adaptor_name] )
.then( function( modules ) {
return new (modules[0])( config_def );
});
},
_fillTemplates: function( subconfig, config ) {
// skip "menuTemplate" keys to prevent messing
// up their feature-based {} interpolation
//var skip = { menuTemplate: true };
var skip = {};
var type = typeof subconfig;
if( lang.isArray( subconfig ) ) {
for( var i = 0; i<subconfig.length; i++ )
subconfig[i] = this._fillTemplates( subconfig[i], config );
}
else if( type == 'object' ) {
for( var name in subconfig ) {
if( subconfig.hasOwnProperty( name ) && !skip[name] )
subconfig[name] = this._fillTemplates( subconfig[name], config );
}
}
else if( type == 'string' ) {
return Util.fillTemplate( subconfig, config );
}
return subconfig;
},
/**
* Recursively fetch, parse, and merge all the includes in the given
* config object. Calls the callback with the resulting configuration
* when finished.
* @private
*/
_loadIncludes: function( inputConfig ) {
var thisB = this;
inputConfig = lang.clone( inputConfig );
function _loadRecur( config, upstreamConf ) {
var sourceUrl = config.sourceUrl || config.baseUrl;
var newUpstreamConf = thisB._mergeConfigs( lang.clone( upstreamConf ), config );
var includes = thisB._fillTemplates(
thisB._regularizeIncludes( config.include || [] ),
newUpstreamConf
);
delete config.include;
var loads = includes.map(include => {
include.cacheBuster = inputConfig.cacheBuster
return thisB._loadInclude( include, sourceUrl )
.then(includedData => _loadRecur(includedData, newUpstreamConf))
});
return all( loads )
.then( function( includedDataObjects ) {
array.forEach( includedDataObjects, function( includedData ) {
config = thisB._mergeConfigs( config, includedData );
});
return config;
});
}
return _loadRecur( inputConfig, {} );
},
_loadInclude: function( include, baseUrl ) {
var thisB = this;
// instantiate the adaptor and load the config
return this._getConfigAdaptor( include )
.then( function( adaptor ) {
if( !adaptor )
throw new Error(
"Could not load config "+include.url+", "
+ "no configuration adaptor found for config format "
+include.format+' version '+include.version
);
return adaptor.load(
{ config: include,
baseUrl: baseUrl
});
}
)
.then( null,
function(error) {
try {
if( error.response.status == 404 )
return {};
} catch(e) {}
throw error;
});
},
_regularizeIncludes: function( includes ) {
if( ! includes )
return [];
// coerce include to an array
if( typeof includes != 'object' )
includes = [ includes ];
// include array might have undefined elements in it if
// somebody left a trailing comma in and we are running under
// IE
includes = array.filter( includes, function(r) { return r; } );
return array.map( includes, function( include ) {
// coerce bare strings in the includes to URLs
if( typeof include == 'string' )
include = { url: include };
// set defaults for format and version
if( ! ('format' in include) ) {
include.format = /\.conf$/.test( include.url ) ? 'conf' : 'JB_json';
}
if( include.format == 'JB_json' && ! ('version' in include) ) {
include.version = 1;
}
return include;
});
},
/**
* @private
*/
_applyDefaults: function( config, defaults ) {
return Util.deepUpdate( dojo.clone(defaults), config );
},
/**
* Examine the loaded and merged configuration for errors. Throws
* exceptions if it finds anything amiss.
* @private
* @returns nothing meaningful
*/
_validateConfig: function( c ) {
if( ! c.tracks )
c.tracks = [];
if( ! c.baseUrl ) {
this._fatalError( 'Must provide a `baseUrl` in configuration' );
}
if( this.hasFatalErrors )
throw "Errors in configuration, cannot start.";
},
/**
* @private
*/
_fatalError: function( error ) {
this.hasFatalErrors = true;
// if( error.url )
// error = error + ' when loading '+error.url;
this.browser.fatalError( error );
},
// list of config properties that should not be recursively merged
_noRecursiveMerge: function( propName ) {
return propName == 'datasets';
},
/**
* Merges config object b into a. a <- b
* @private
*/
_mergeConfigs: function( a, b ) {
if( b === null )
return null;
if( a === null )
a = {};
for (var prop in b) {
if( prop == 'tracks' && (prop in a) ) {
a[prop] = this._mergeTrackConfigs( a[prop] || [], b[prop] || [] );
}
else if ( ! this._noRecursiveMerge( prop )
&&(prop in a)
&& ("object" == typeof b[prop])
&& ("object" == typeof a[prop]) ) {
a[prop] = Util.deepUpdate( a[prop], b[prop] );
} else if(prop == 'dataRoot') {
if(a[prop] === undefined || a[prop] == 'data' && b[prop] !== undefined ){
a[prop] = b[prop];
}
} else if( a[prop] === undefined || b[prop] !== undefined ){
a[prop] = b[prop];
}
}
return a;
},
/**
* Special-case merging of two <code>tracks</code> configuration
* arrays.
* @private
*/
_mergeTrackConfigs: function( a, b ) {
if( ! b.length )
return a;
// index the tracks in `a` by track label
var aTracks = {};
array.forEach( a, function(t,i) {
t.index = i;
aTracks[t.label] = t;
});
array.forEach( b, function(bT) {
var aT = aTracks[bT.label];
if( aT ) {
this._mergeConfigs( aT, bT );
} else {
a.push( bT );
}
},this);
return a;
}
});
});