% SUBMIT Submits the given objects to an LTPDA repository
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% DESCRIPTION: Submits the given objects to an LTPDA repository. If multiple
% objects are submitted together a corresponding collection entry will be made.
%
% If not explicitly disabled the user will be prompt for entering submission
% metadata and for chosing the database where to submit the objects.
%
% CALL:        [IDS, CIDS] = submit(O1, PL)
%              [IDS, CIDS] = submit(O1, O2, PL)
%
% INPUTS:      O1, O2, ... - objects to be submitted
%              PL          - plist whih submission and repository informations
%
% OUTPUTS:     IDS         - IDs assigned to the submitted objects
%              CID         - ID of the collection entry
%
% <a href="matlab:utils.helper.displayMethodInfo('ltpda_uo', 'submit')">Parameters Description</a>
%
% METADATA:
%
%   'experiment title'       - title for the submission (required >4 characters)
%   'experiment description' - description of this submission (required >10 characters)
%   'analysis description'   - description of the analysis performed (required >10 characters)
%   'quantity'               - the physical quantity represented by the data
%   'keywords'               - comma-delimited list of keywords
%   'reference ids'          - comma-delimited list object IDs
%   'additional comments'    - additional comments
%   'additional authors'     - additional author names
%
% VERSION:     $Id: submit.m,v 1.91 2011/11/18 08:08:26 mauro Exp $
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% Notes on submission
%
% We can check ask the database for a list of allowed modules. This needs a
% new table in the database. Then this list is passed to validate so that
% if the 'validate' plist option (which needs to be added) is set to true,
% then we call validate on the object before submitting. If validate is
% true, then we set the validated flag in the database after submission if
% it passes.
% 
% 


function varargout = submit(varargin)
  
  % check if this is a call for parameters
  if utils.helper.isinfocall(varargin{:})
    varargout{1} = getInfo(varargin{3});
    return
  end
  
  import utils.const.*
  utils.helper.msg(msg.PROC3, 'running %s/%s', mfilename('class'), mfilename);
  
  % collect all AOs
  [pls,   invars, rest] = utils.helper.collect_objects(varargin(:), 'plist');
  [sinfo, invars, objs] = utils.helper.collect_objects(rest(:),    'struct');
  
  % identify plists which are only used for the submission process
  mask = false(numel(pls), 1);
  for ii = 1:numel(pls)
    if ~utils.helper.isSubmissionPlist(pls(ii))
      mask(ii) = true;
    end
  end
  % add all plist that do not look to contain parameters for the
  % submission to the list of objects submitted to the repository
  if sum(mask)
    objs = [objs, {pls(mask)}];
  end
  % keep all the other as parameters plist
  if sum(~mask)
    pls = combine(pls(~mask));
  end
  
  % rearrange nested objects lists into a single cell array
  objs = flatten(objs);
  
  if isempty(objs)
    error('### input at least one object to submit to the repository');
  end
  
  % combine user plist with default
  pls = fixPlist(pls);
  dpl = getDefaultPlist();
  pls = combine(pls, dpl.pset('HOSTNAME', ''));
  
  % for backwards compatibility convert any user supplied sinfo-structure into a plist
  pls = ltpda_uo.convertSinfo2Plist(pls, sinfo);
  
  % read XML submission informations file
  filename = pls.find('sinfo filename');
  if ~isempty(filename)
    try
      pl = fixPlist(utils.xml.read_sinfo_xml(filename));
      pls = combine(pl, pls);
    catch err
      error('### unable to read specified file: %s', filename);
    end
  end
  
  % collect additional informations
  sinfo = ltpda_uo.submitDialog(pls);
  if isempty(sinfo)
    [varargout{1}, varargout{2}] = userCanceled();
    return
  end
  
  % check completeness of user supplied informations
  sinfo = checkSinfo(sinfo);
  
  % user supplied connection
  conn = find(pls, 'conn');
  
  % obtain a connection from the connection manager
  rm = LTPDARepositoryManager();
  
  if isempty(conn)
    connections = rm.findConnections(pls);
    if isempty(connections)
      % no connection found. create a new one
      conn = rm.newConnection(pls);
      if isempty(conn)
        [varargout{1}, varargout{2}] = userCanceled();
        return
      end
    elseif numel(connections) == 1 && ~utils.prog.yes2true(pls.find('use selector'))
      % found only one connection and we are not forcing the use of the selector
      conn = connections(1);
    else
      % make the user select the desired database connection
      conn = rm.manager.selectConnection([]);
      if isempty(conn)
        [varargout{1}, varargout{2}] = userCanceled();
        return
      end
    end
  end
  
  % avoid to ask for a password if one was specified in the plist
  if isjava(conn) && ~conn.isConnected()
    passwd = pls.find('password');
    if ~isempty(passwd)
      conn.setPassword(passwd);
    end
  end
  
  % connect to the database
  if isempty(conn.openConnection())
    error('### unable to connect to the database')
  end
  
  utils.helper.msg(msg.PROC1, 'submitting %d objects to repository', numel(objs));
  
  try
    % lock connection to avoid expire during processing
    conn.setLocked(true);
    
    % reload connection table if we have a GUI
    if ~isempty(rm.gui)
      rm.gui.reloadConnectionTable();
    end
    
    % look-up user id
    userid   = utils.jmysql.getUserID(conn);
    username = conn.getUsername();
    
    utils.helper.msg(msg.PROC1, 'got user id %d for user: %s', userid, char(username));
    if userid < 1 || isnan(userid)  || strcmp(userid, 'No Data') || ischar(userid)
      error('### Unknown username.');
    end

    % author of the data: let's take the username
    author = username;
    
    % date for the transaction.transdata and objmeta.submitted columns as UTC time string
    t     = time();
    tdate = format(t, 'yyyy-mm-dd HH:MM:SS', 'UTC');
    
    % machine details
    prov = provenance();
    
    utils.helper.msg(msg.IMPORTANT, 'submitting to %s@%s:%s', ...
      char(conn.getUsername), char(conn.getHostname), char(conn.getDatabase));
    
    % obtain the underlying connection
    c = conn.getConn();
    
    % start a transaction. either we submitt all objects or we roll back all changes
    c.setAutoCommit(false);
    
    % process each object and collect id numbers
    ids = zeros(numel(objs), 1); cid = [];
    for kk = 1:numel(objs)
      
      % this object
      obj = objs{kk};
      
      utils.helper.msg(msg.PROC1, 'submitting object: %s / %s', class(obj), obj.name);
      
      % format object creation time as UTC time string
      if isa(obj, 'plist')
        % plist-objects stores creatins time as milli secs since the epoch
        created = time().format('yyyy-mm-dd HH:MM:SS', 'UTC');
      else
        created = obj.created.format('yyyy-mm-dd HH:MM:SS', 'UTC');
      end
      
      % Set the UUID if it is empty. This should only happen for PLIST
      % objects.
      if isempty(obj.UUID)
        obj.UUID = char(java.util.UUID.randomUUID);
      end
      
      % create an XML representaion of the object
      if utils.prog.yes2true(pls.find('binary'));
        utils.helper.msg(msg.PROC2, 'binary submit');
        otxt = ['binary submit ' datestr(now)];
      else
        utils.helper.msg(msg.PROC2, 'xml submit');
        otxt = utils.prog.obj2xml(obj);
      end
      
      % create an MD5 hash of the xml representation
      md5hash = utils.prog.hash(otxt, 'MD5');
      
      % create a binary representaion of the object
      bobj = utils.prog.obj2binary(obj);
      if isempty(bobj)
        error('### failed to obtain a binary representation');
      end
      
      % submit object to objs table
      stmt = c.prepareStatement(...
        'INSERT INTO objs (xml, hash, uuid) VALUES (?, ?, ?)');
      stmt.setObject(1, otxt);
      stmt.setObject(2, char(md5hash));
      stmt.setObject(3, obj.UUID);
      stmt.executeUpdate();
      
      % obtain object id
      rs = stmt.getGeneratedKeys();
      if rs.next()
        objid = rs.getInt(1);
      else
        objid = [];
      end
      rs.close();
      stmt.close();
      
      % insert binary representation
      stmt = c.prepareStatement(...
        'INSERT INTO bobjs (obj_id, mat) VALUES (?,?)');
      stmt.setObject(1, objid);
      stmt.setObject(2, bobj);
      stmt.execute();
      stmt.close();
      
      % reference IDs are stored in a CSV string
      if ischar(sinfo.reference_ids)
        refids = sinfo.reference_ids;
      else
        refids = utils.prog.csv(sinfo.reference_ids);
      end
      
      % insert object meta data
      stmt = c.prepareStatement(...
        [ 'INSERT INTO objmeta (obj_id, obj_type, name, created, version, ' ...
        'ip, hostname, os, submitted, experiment_title, experiment_desc, ' ...
        'reference_ids, additional_comments, additional_authors, keywords, ' ...
        'quantity, analysis_desc, author) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)' ]);
      stmt.setObject( 1, objid);
      stmt.setObject( 2, java.lang.String(class(obj)));
      stmt.setObject( 3, java.lang.String(obj.name));
      stmt.setObject( 4, java.lang.String(created));
      stmt.setObject( 5, java.lang.String(getappdata(0, 'ltpda_version')));
      stmt.setObject( 6, java.lang.String(prov.ip));
      stmt.setObject( 7, java.lang.String(prov.hostname));
      stmt.setObject( 8, java.lang.String(prov.os));
      stmt.setObject( 9, java.lang.String(tdate));
      stmt.setObject(10, java.lang.String(sinfo.experiment_title));
      stmt.setObject(11, java.lang.String(sinfo.experiment_description));
      stmt.setObject(12, java.lang.String(refids));
      stmt.setObject(13, java.lang.String(sinfo.additional_comments));
      stmt.setObject(14, java.lang.String(sinfo.additional_authors));
      stmt.setObject(15, java.lang.String(sinfo.keywords));
      stmt.setObject(16, java.lang.String(sinfo.quantity));
      stmt.setObject(17, java.lang.String(sinfo.analysis_description));
      stmt.setObject(18, java.lang.String(author));
      stmt.execute();
      stmt.close();
      
      % update other meta-data tables
      cols = utils.jmysql.execute(c, 'SHOW COLUMNS FROM tsdata');
      if utils.helper.ismember('obj_id',  cols(:,1))
        % the tsdata table contains an obj id column. use the new database schema
        utils.jmysql.insertObjMetadata(c, obj, objid);
      else
        % otherwise use the old one
        utils.helper.msg(msg.PROC2, 'using back-compatibility code');
        utils.jmysql.insertObjMetadataV1(c, obj, objid);
      end
      
      % update transactions table
      stmt = c.prepareStatement(...
        'INSERT INTO transactions (obj_id, user_id, transdate, direction) VALUES (?, ?, ?, ?)');
      stmt.setObject(1, objid);
      stmt.setObject(2, userid);
      stmt.setObject(3, java.lang.String(tdate));
      stmt.setObject(4, java.lang.String('in'));
      stmt.execute();
      stmt.close();
      
      % collect the id of the submitted object
      ids(kk) = objid;
    end
    
    % make collection entry
    if numel(objs) > 1
      
      % insert record into collections table
      stmt = c.prepareStatement(...
        'INSERT INTO collections (nobjs, obj_ids) VALUES (?, ?)');
      stmt.setObject(1, length(ids));
      stmt.setObject(2, java.lang.String(utils.prog.csv(ids)));
      stmt.executeUpdate();
      
      % obtain collection id
      rs = stmt.getGeneratedKeys();
      if rs.next()
        cid = rs.getInt(1);
      else
        cid = [];
      end
      rs.close();
      stmt.close();
      
    end
    
  catch ex
    utils.helper.msg(msg.IMPORTANT, 'submission error. no object submitted')
    c.close()
    conn.setLocked(false);
    rethrow(ex)
  end
  
  % commit the transaction
  c.commit();
  
  % report IDs of the inserted objects
  for kk = 1:numel(objs)
    utils.helper.msg(msg.IMPORTANT, 'submitted %s object with ID: %d, UUID: %s, name: %s', ...
      class(objs{kk}), ids(kk), objs{kk}.UUID, objs{kk}.name);
  end
  if ~isempty(cid)
    utils.helper.msg(msg.IMPORTANT, 'made collection entry with ID: %d', cid);
  end
  
  % unlock connection expire
  conn.setLocked(false);
  
  % reset timer
  LTPDARepositoryManager.resetTimer(rm.timerClearPass, conn);
  LTPDARepositoryManager.resetTimer(rm.timerDisconnect, conn);
  
  % pass back outputs
  if nargout > 0
    varargout{1} = ids;
  end
  if nargout > 1
    varargout{2} = cid;
  end
end


function varargout = userCanceled()
  % signal that the user cancelled the submission
  import utils.const.*
  utils.helper.msg(msg.PROC1, 'user cancelled');
  varargout{1} = [];
  varargout{2} = [];
end


function sinfo = checkSinfo(sinfo)
  % check sinfo structure
  
  import utils.const.*
  
  % fieldnames
  mainfields = {'experiment_title', 'experiment_description', 'analysis_description'};
  extrafields = {'quantity', 'keywords', 'reference_ids', 'additional_comments', 'author', 'additional_authors'};
  
  % fieldnames of the input structure
  fnames = fieldnames(sinfo);
  
  % check mandatory fields
  for jj = 1:length(mainfields)
    if ~ismember(fnames, mainfields{jj})
      error('### the sinfo structure should contain a ''%s'' field', mainfields{jj});
    end
  end
  
  % check extra fields
  for jj = 1:length(extrafields)
    if ~ismember(fnames, extrafields{jj})
      utils.helper.msg(msg.PROC2, 'setting default for field %s', extrafields{jj});
      sinfo.(extrafields{jj}) = '';
    end
  end
  
  % additional checks
  if length(sinfo.experiment_title) < 5
    error('### ''experiment title'' should be at least 5 characters long');
  end
  if length(sinfo.experiment_description) < 10
    error('### ''experiment description'' should be at least 10 characters long');
  end
  if length(sinfo.analysis_description) < 10
    error('### ''analysis description'' should be at least 10 characters long');
  end
  
end


function ii = getInfo(varargin)
  if nargin == 1 && strcmpi(varargin{1}, 'None')
    sets = {};
    pl   = [];
  else
    sets = {'Default'};
    pl   = getDefaultPlist();
  end
  % Build info object
  ii = minfo(mfilename, 'ltpda_uo', 'ltpda', utils.const.categories.internal, '$Id: submit.m,v 1.91 2011/11/18 08:08:26 mauro Exp $', sets, pl);
  ii.setModifier(false);
end


function plout = getDefaultPlist()
  persistent pl;
  if ~exist('pl', 'var') || isempty(pl)
    pl = buildplist();
  end
  plout = pl;
end

function plo = buildplist()
  
  plo = plist.TO_REPOSITORY_PLIST;
  
  p = param({'sinfo filename', 'Path to an XML file containing submission metadata'}, paramValue.EMPTY_STRING);
  plo.append(p);
  
  p = param({'binary', 'Submit only binary version of the objects'}, paramValue.FALSE_TRUE);
  plo.append(p);
end


function pl = fixPlist(pl)
  % accept parameters where words are separated either by spaces or underscore
  if ~isempty(pl)
    for ii = 1:pl.nparams
      pl.params(ii).setKey(strrep(pl.params(ii).key, '_', ' '));
    end
  end
end


function flat = flatten(objs)
  % flatten nested lists into a single cell array
  
  flat = {};
  
  while iscell(objs) && numel(objs) == 1
    objs = objs{1};
  end
  
  if numel(objs) == 1
    flat = {objs};
    return;
  end
  
  for jj = 1:numel(objs)
    obj = flatten(objs(jj));
    for kk = 1:numel(obj)
      flat = [ flat obj(kk) ];
    end
  end
  
end
