% Conection Manage named connection to MPS persistence service.
%
% mps.cache.Connection is an internal implementation detail. Manipulating
% the list of known connections or modifying a Connection object is likely
% to have unintended and unfortunate side-effects.
%
% A Connection links MATLAB code to a persistence service. A MATLAB
% runtime environment has one connection for every persistence service it
% uses. All the DataCache and ServiceMutex objects that belong to a given
% service share the unique connection to that service. Clients do not
% create Connections explicitly; the functions mps.cache.connect,
% mps.cache.control and mps.sync.mutex create Connections from
% client-supplied data. All connections require a unique name; other inputs
% are persistence service-specific.
%
% mps.cache.Connection manages the list of known connections using the
% getInstance factory function pattern. Each Connection object maintains 
% an opaque handle to the persistence service it communicates with. Do not 
% assume this service handle will remain constant throughout the lifetime 
% of the Connection object. In the case of network interruption the 
% service handle may change.
%
% See Also connect, control, DataCache, mps.sync.mutex

% Copyright 2018 The MathWorks, Inc.
classdef Connection < handle
    
    % Fully accessible, but you have to know the name, as the property name
    % doesn't appear in any public lists.
    properties (Hidden = true)
        ServiceHandle     % Opaque handle; connection to service.
    end
    
    properties (SetAccess = protected)
    % Everyone can read these properties. Only Connection and subclasses can
    % set them.
    
        %CONNECTIONNAME Name of the Connection, e.g., LocalRedis
        ConnectionName 
        %PROVIDERNAME Name of the service provider, like Redis or Mongo
        ProviderName
        %OPERATIONS Data access operations permitted on this connection.
        %Currently not used. All operations permitted.
        Operations 
    end
    
    methods (Static, Access = protected)
        
        function connections = ConnectionList(list)
        % Set/Get connection list variable. ConnectionList will be easier to
        % manage if list is a handle variable.
            persistent ConnectionList;
            if nargin == 1
                ConnectionList = list;
            end
            connections = ConnectionList;
        end
        
        function removeDetachedConnection(name)
            % Remove the cache connection from the list of known
            % connections if the connection does not have a corresponding
            % endpoint file. Only applies when we're using endpoint
            % files -- when running against an MPS connection. In MATLAB
            % (or Dev & Test), we don't use endpoint files, and there's
            % no need to sync the connection cache with the endpoint
            % file list.
            %
            % Make sure we only look in the endpoint directory when running
            % against MPS by checking isdeployed.
            endpointRoot = getenv('MPS_ENDPOINT_ROOT');
            if ~isempty(endpointRoot) && isdeployed
                iList = mps.cache.Connection.ConnectionList();
                if isKey(iList, name)
                    endpointFile = fullfile(endpointRoot, ...
                        strcat(name, '_cache'));
                    if exist(endpointFile,'file') == 0
                        ci = iList(name);
                        % Can't call destroy on empty ([], type double)
                        if ~isempty(ci.ServiceHandle)
                            destroy(ci.ServiceHandle);
                        end
                        remove(iList,name);
                        mps.cache.Connection.ConnectionList(iList);
                    end
                end
            end
       end                
    end
    
    methods (Static)
        
        function release(connection)
        %RELEASE Release the named connection -- disconnect and prevent
        %reconnection.
        %
        %   mps.cache.Connection.release(C) disconnects the named
        %   connection from its persistence service. Any resources
        %   associated with the connection are freed. The connection C
        %   is invalidated, and my not be reused after release.
        %
        %   See Also mps.cache.connect, disconnect, reset, ResourceHandle
        
            iList = mps.cache.Connection.ConnectionList();
            if ~isempty(iList)
                connection = char(connection);
                if isKey(iList,connection)
                    ci = iList(connection);
                    % Empty service handle is [] (double), which doesn't
                    % have a destroy method. So test for empty first.
                    if ~isempty(ci.ServiceHandle)
                        destroy(ci.ServiceHandle);
                    end
                end
            end         
        end
        
        function disconnect(connection)
        %DISCONNECT Disconnect the named connection.
        %
        %   mps.cache.Connection.disconnect(C) disconnects the named
        %   connection from its persistence service. Any resources
        %   associated with the connection are freed. The connection C 
        %   remains on the list of known connections and may be reused
        %   after reconnection.
        %
        %   See Also mps.cache.connect, reset, ResourceHandle            
            iList = mps.cache.Connection.ConnectionList();
            if ~isempty(iList)
                connection = char(connection);
                if isKey(iList,connection)
                    ci = iList(connection);
                    % Empty service handle is [] (double), which doesn't
                    % have a destroy method. So test for empty first.
                    if ~isempty(ci.ServiceHandle)
                        disable(ci.ServiceHandle);
                    end
                end
            end
        end
        
        function reset(connection)
        %RESET Reset the named connection.
        %
        %   mps.cache.Connection.reset(C) disconnects and attempts to 
        %   reconnect the service handle of the connection named C. The 
        %   service handle represents the connection to the persistence
        %   service. Call this method after restarting an active 
        %   persistence service.
        %
        %   See Also mps.cache.connect, disconnect, ResourceHandle
            iList = mps.cache.Connection.ConnectionList();
            if ~isempty(iList)
                connection = char(connection);
                if isKey(iList,connection)
                    ci = iList(connection);
                    % Can't call reset if empty (because empty => []).
                    if ~isempty(ci.ServiceHandle)
                        reset(ci.ServiceHandle);
                    end
                end
            end
        end
        
        function clear(connection)
        %CLEAR Clear the named connection from the connection list.
        %
        %   mps.cache.Connection.clear(CONNECTION) removes the connection 
        %   named CONNECTION from the list of known connections and 
        %   destroys the connection's service handle. The connection may 
        %   no longer be used.
        %
        %   mps.cache.Connection.clear() clears all known connections.
        %
        %   See Also disconnect, reset, mps.cache.connect
            if nargin == 0
                mps.cache.Connection.ConnectionList([]);
            else
                iList = mps.cache.Connection.ConnectionList();
                if ~isempty(iList)
                    connection = char(connection);
                    if isKey(iList,connection)
                        ci = iList(connection);
                        % Can't call destroy on empty ([], type double)
                        if ~isempty(ci.ServiceHandle)
                            destroy(ci.ServiceHandle);
                        end
                        remove(iList,connection);
                    end
                end
            end
        end
        
        function tf = exist(name)
        %EXIST Does the named connection exist?
        %
        %   TF = mps.cache.Connection.exist(NAME) returns logical TRUE if a
        %   connection named NAME exists. 
        %
        %   See Also connect, mps.sync.mutex, getInstance
            tf = false;
            iList = mps.cache.Connection.ConnectionList();
            if ~isempty(iList)
                tf = isKey(iList,name);
            end    
        end
        
        function connections = list()
        %LIST Return a list of the names of the active connections.
        %
        %   LST = mps.cache.Connection.list() returns a cell array of the
        %   names of the active connections.
        %
        %   See Also connect, getInstance
        
            connections = {};
            iList = mps.cache.Connection.ConnectionList();
            if ~isempty(iList)
                connections = keys(iList);
            end
        end
        
        function ci = getInstance(name,provider,ctor,varargin)
        %GETINSTANCE Get existing or create new Connection.
        %
        %   C = mps.cache.Connection.getInstance(NAME) looks for a
        %   connection named NAME. If that connection exists, getInstance
        %   returns C. If name is omitted and only one connection exists, 
        %   getInstance returns that connection. If no suitable connection
        %   exists, getInstance raises an error.
        %   
        %   C = mps.cacheConnection.getInstance(NAME,PROVIDER,CTOR,VARARGIN)
        %   uses function handle CTOR, service provider PROVIDER and
        %   persistence service-specific inputs VARARGIN to create a new
        %   connection named NAME. Connection names must be unique.
        %
        %   See Also connect, mps.sync.mutex
        
            iList = mps.cache.Connection.ConnectionList();
            
            % If we don't know about any connections, look for some in the
            % endpoint directory, if we know about an endpoint directory.
            if isempty(iList)
                iList = mps.cache.Connection.ConnectionList(...
                    containers.Map('KeyType','char','ValueType','any'));
                
                % If there's a valid endpoint directory, with *_cache
                % endpoint files, read those endpoint files onto the
                % ConnectionList.
                
                endpointRoot = getenv('MPS_ENDPOINT_ROOT');
                if ~isempty(endpointRoot)
                    endpointFiles = dir(fullfile(endpointRoot,'*_cache'));
                    if ~isempty(endpointFiles)
                        for k=1:numel(endpointFiles)
                            connectionName = strrep(endpointFiles(k).name, ...
                                '_cache', '');
                            % Insert placeholder to prevent infinite
                            % recursion
                            iList(connectionName) = [];
                            
                            % Results in a recursive call to getConnection()
                            % with non-zero number of arguments, which
                            % creates Connection and inserts it onto
                            % ConnectionList. 
                            createEndpointConnection(...
                                connectionName, ...
                                fullfile(endpointFiles(k).folder, ...
                                         endpointFiles(k).name));
                        end
                    end
                end
            end
            
            % We're looking up an connection if the number of inputs is zero
            % or one. Make sure the connection is still valid.
            if nargin < 2
  
                % If we weren't passed a name, this call can only succeed
                % if there's only one known connection (that's "the
                % default"). Remove any detached connections from the list
                % before determining how many connections are known.
                if nargin == 0
                    connections = keys(iList);
                    for n = 1:numel(connections)
                        mps.cache.Connection.removeDetachedConnection(...
                            connections{n});
                    end
                    if iList.Count == 1 
                        k = keys(iList);
                        name = k{1};
                    end
                end
               
                % Administrator may have stopped or detached some
                % services. Make sure the Connection we're looking for 
                % is still active. Only need to make this check if we were
                % passed a name, because we've already performed it if the
                % caller wants "the default" connection.
                if nargin == 1 && exist('name','var') && ~isempty(name) && ...
                        isKey(iList, name)
                    mps.cache.Connection.removeDetachedConnection(name);
                end
                
                % If we can't find the name on the list, and we know about
                % an endpoint directory, see if there's a corresponding
                % endpoint file that defines the connection. If so, create
                % a new connection.
                if nargin > 0 && ~isKey(iList, name)
                    endpointRoot = getenv('MPS_ENDPOINT_ROOT');
                    if ~isempty(endpointRoot)
                        endpointFile = fullfile(endpointRoot, ...
                                                strcat(name, '_cache'));
                        if exist(endpointFile, 'file') == 2
                            createEndpointConnection(...
                                name, endpointFile);
                        end
                    end
                end 
            end
                                
            % With zero inputs, error if there is more than one Connection on
            % the list. But, if there's only one, return that one.
            if nargin == 0                 
                if iList.Count == 1
                    k = keys(iList);
                    ci = iList(k{1});
                    return;
                end
                
                error(message('MPS:MATLAB:Cache:AmbiguousDefaultConnection', ...
                        iList.Count));
            end
            
            % containers.Map can't handle string names, so convert to char.
            charName = char(name);
            
            % With one input, name, look up Connection of that name.
            % With multiple inputs, create a new Connection of the given
            % name; error if there's an existing one of the same name.
            if nargin == 1
                if isKey(iList,charName)
                    ci = iList(charName);
                else
                    error(message('MPS:MATLAB:Cache:UnknownServiceConnection',name));
                end
            else
                % Must test for empty value, to allow "placeholder"
                % recursion base case to succeed. It's only an error to
                % attempt overwrite of a non-empty Connection.
                if isKey(iList,charName) && ~isempty(iList(charName))
                    error(message('MPS:MATLAB:Cache:DuplicateServiceConnection', name));
                end
                
                ci = ctor(name,provider,varargin{:});
                iList(charName) = ci;
            end
        end
        
    end
    
    methods (Access = protected)   
            
        function ci = Connection(name,provider,varargin)
        % Connection Create Connection object. All inputs required.
        %
        % Name:         Name of the Connection. Must be unique.
        % Provider:     Name of the provider. Control script must exist.
        
            import mps.cache.internal.InputValidator;
            ci.ConnectionName = [];
            p = inputParser;
            p.KeepUnmatched = true;
            addRequired(p,'name',InputValidator.NonEmptyString);
            addRequired(p,'provider',InputValidator.NonEmptyString);

            % Optional list of allowed data access operations
            defaultOperations = "read | write | create | update";
            addParameter(p,'Operations',defaultOperations,...
                InputValidator.NonEmptyString);            
            
            parse(p,name,provider,varargin{:});

            ci.ConnectionName = name;
            ci.ProviderName = provider;
            ci.Operations = p.Results.Operations;
        end
    end
end

function createEndpointConnection(name, endpointFile)
% Read the JSON endpoint file and call mps.cache.configure to create an
% connection based on the data therein.

    % Read all JSON data into memory
    jsonData = fileread(endpointFile);
    
    % Parse the JSON data into a structure
    endpointData = jsondecode(jsonData);
    
    % The provider is special -- extract it, then remove that field from
    % the structure. And "Manage" is not recognized by the Connection
    % classes, so it too must be removed.
    provider = endpointData.Provider;
    endpointData = rmfield(endpointData,["Provider", "Manage"]);
    
    % For the rest of the fields, create name value pairs in a cell array,
    % as that's the input that mps.cache.configure expects.
    fields = fieldnames(endpointData);
    pairs = cellfun(@(f){f, endpointData.(f)}, fields, 'UniformOutput', false);
    pairs = [ pairs{:} ];   % Flatten pairs into a single cell array
    
    % Create the connection based on the data in the endpoint file. This will
    % recursively call Connection.getInstance -- but because the ConnectionList
    % won't be empty, getConnection should not call createEndpointConnection.
    mps.cache.configure(name, provider, pairs{:});
end
