cosmo config

function [config,fn]=cosmo_config(fn, config)
% return a struc with configuration settings, or store such settings
%
% Usages:
% - get the configuation (either default, or what is in '.cosmomvpa.cfg'):
%   >> config=cosmo_config();
%
% - read configuration from a specified file
%   >> config=cosmo_config(fn)
%
% - store configuration in a file
%   >> cosmo_config(fn, to_store)
%
% - related function (in CoSMoMVPA's 'examples/' directory):
%   >> cosmo_wizard_set_config
%
% Inputs:
%   fn          optional filename of a configuration file.
%               This can either be a path of a file, or (on Unix platforms)
%               a filename in the user's home directory.
%               If fn is omitted, then it defaults to '.cosmomvpa.cfg'
%               and will read the configuration from that file if it
%               exists in one of the aforementioned locations; see the
%               notes below for details.
%   to_store    optional struct with configuration to store.
%               If omitted then the configuration is not stored.
%
% Returns:
%   config      Struct with configurations
%
% Notes:
%  - the rationale for this function is to keep the example code fixed
%    (that is, without any paths hard-coded) and still allow each
%    user to store the example data in a directory of their choice.
%
%  - the format for a configuration file is of the form <key>=<value>,
%    where <key> cannot contain the '=' character and <value> cannot start
%    or end with a white-space character.

%  - if the .cosmomvpa.cfg file is in the same directory as this file
%   (presently, 'mvpa/'), it will be found there as well.
%
%  - an example configuration file (of three lines):
%
% # path for runnable examples
% tutorial_data_path=/Users/nick/organized/_datasets/CoSMoMVPA/tutorial_data
% output_data_path=/Users/nick/organized/_datasets/CoSMoMVPA/tutorial_data
%
%  - If a configuration file with the name '.cosmomvpa.cfg' is stored
%    in a directory that is in the matlab path, the user path, or (on Unix
%    platforms) the user's home directory, then calling this function with
%    no arguments will read that file and return the configuration stored
%    in it.
%
%  - The configuration can also be changed as follows:
%    >> % get default configuration
%    >> % If the following command gives an error, do: config=struct()
%    >> config=cosmo_config();
%    >>
%    >> % update settings
%    >> config.tutorial_data_path='/path/to/some/data';
%    >> config.output_data_path='/where/i/want/output';
%    >>
%    >> % get first directory in user path
%    >> matlab_path=cosmo_strsplit (userpath,':',1);
%    >>
%    >> % set configuration filename
%    >> config_fn=fullfile(matlab_path,'.cosmomvpa.cfg');
%    >> cosmo_config(config_fn, config);
%    >> fprintf('Configuration stored in %s\n', config_fn);
%    >>
%    >> % now check they are the same
%    >> loaded_config=cosmo_config();
%    >> assert(isequal(loaded_config,config));
%
%  - The configuration can be set using cosmo_wizard_set_config in
%    CoSMoMVPA's 'examples/' directory
%
% #   For CoSMoMVPA's copyright information and license terms,   #
% #   see the COPYING file distributed with CoSMoMVPA.           #

    default_config_fn='.cosmomvpa.cfg';

    if nargin==1 && ischar(fn)
        fn=find_config_file(fn);
        config=read_config(fn);
    elseif nargin==0
        % see if the configuration file can be found
        fn=find_config_file(default_config_fn);
        if isempty(fn)
            config=struct();
        else
            config=read_config(fn);
        end
    elseif nargin==2 && ischar(fn) && isstruct(config)
        write_config(fn,config);
    else
        error('Illegal input');
    end

    expected_fields={'tutorial_data_path','output_data_path'};
    missing_fields=setdiff(expected_fields,fieldnames(config));

    if numel(missing_fields)>0
        missing_fields_str=cosmo_strjoin(missing_fields,''', ''');
        cosmo_warning(['Using %s, fields ''%s'' are missing. This makes '...
                    'it more complicated to run CoSMoMVPA''s '...
                    'exercises and examples.\n'...
                    'To set the configuration, consider running\n\n'...
                    '    cosmo_wizard_set_config\n\n'...
                    'in the CoSMoMVPA ''examples/'' directory'],...
                    mfilename(),missing_fields_str);
    end

    validate_config(config);


function validate_config(config)
    % simple validation of config

    % poor-man version of OO
    path_exists=struct();
    path_exists.match=@(x)isempty(cosmo_strsplit(x,'_path',-1));
    path_exists.test=@(p) exist(p,'file');
    path_exists.msg=@(key, p) sprintf('%s: path "%s" not found. ',key,p);

    checks={path_exists};
    add_msg=sprintf('To set the configuration, run: help %s', mfilename());

    % perform checks on fieldnames present in 'checks'.

    fns=fieldnames(config);
    for k=1:numel(fns)
        fn=fns{k};
        for j=1:numel(checks)
            check=checks{j};
            if check.match(fn)
                test_func=check.test;
                value=config.(fn);

                if ~test_func(value)
                    msg_func=check.msg;
                    cosmo_warning('%s\n%s',msg_func(fn, value),add_msg);
                end
            end
        end
    end


function path_fn=find_config_file(fn, raise_)
% tries to find a configuration file by looking:
% - for the path of the file
% - in the matlab path
% - in the user's home directory (on Unix)
    if nargin<2, raise_=false; end

    exist_=@(fn_) exist(fn_,'file');

    path_fn=[];

    % simulate 'go-to' statement using a while loop with break at the end
    while true
        % does the file exist 'as is'?
        if exist_(fn)
            path_fn=fn;
            break
        end

        % is it in the mvpa directory?
        parent_dir=fileparts(mfilename('fullpath'));
        m_fn=fullfile(parent_dir,fn);
        if exist_(m_fn)
            path_fn=m_fn;
            break;
        end

        % is it in the user path?
        % (not supported on octave)
        if cosmo_wtf('is_matlab')
            upaths=cosmo_strsplit(userpath(),pathsep());

            for k=1:numel(upaths)
                u_fn=fullfile(upaths{k},fn);
                if exist_(u_fn)
                    path_fn=u_fn;
                    break;
                end
            end
            if ~isempty(path_fn)
                break;
            end
        end

        if isunix()
            % is it in the home directory?
            u_fn=fullfile(getenv('HOME'),fn);
            if exist_(u_fn)
                path_fn=u_fn;
                break;
            end
        end

        if raise_
            error('Cannot find config file "%s"', fn);
        end

        break;
    end



function config=read_config(fn)
% reads configuration from a file fn

    config=struct(); % space for output
    if isempty(fn)
        error('Input file not specified - cannot open')
    end

    fid=fopen(fn);
    if fid==-1
        error('Unable to open file %s',fn);
    end

    line_number=0;
    while true
        % read each line
        line=fgetl(fid);
        if ~ischar(line)
            % end of file
            break;
        end

        line_number=line_number+1;

        % ignore empty lines or lines starting with '#'
        if isempty(line) || line(1)=='#'
            continue
        end

        % look for lines of form '<key>=<value>'.
        % white spaces around key or value are ignored.
        m=regexp(line,'(?<key>[^=]+)\s*=\s*(?<value>.*)\s*','names');

        if isempty(m)
            cosmo_warning('Skipping non-recognized line "%s"', line);
            continue;
        end


        % get key and value
        key=m.key;
        value=m.value;

        verify_no_illegal_char(fn,line_number,[key value])

        % see if it can be converted to numeric
        value_num=str2double(value);
        if ~isnan(value_num)
            value=value_num;
        end

        config.(key)=value;
    end

    fclose(fid);

function verify_no_illegal_char(fn,line_number,s)
    illegal_chars=sprintf('"''');
    matching=bsxfun(@eq,s,illegal_chars(:));
    if any(matching(:))
        error(['File %s, line %d: found illegal character. '...
            'note that quote characters are not allowed '...
            'for keys or values'],fn,line_number)
    end



function write_config(fn, config)
% writes the config to a file fn
% no support for comments or empty lines

    fid=fopen(fn,'w');

    fns=fieldnames(config);
    for k=1:numel(fns)
        fn=fns{k};

        v=config.(fn);
        if isnumeric(v)
            % convert numeric to string
            v=sprintf('%d ',v);
        elseif ischar(v)
            % no converstion
        else
            cosmo_warning('Skipping unsupported data type for key "%s"',...
                                                fn);
        end
        fprintf(fid,'%s=%s\n',fn,v);
    end

    fclose(fid);