%MJS Interact with an MJS cluster
%   The MJS cluster type provides access to the MATLAB Job Scheduler, which
%   controls the job queue, distributes job tasks to workers or labs for
%   execution, and maintains job results. 
%
%   parallel.cluster.MJS methods:
%      MJS                    - Create a MJS cluster instance
%      batch                  - Run MATLAB script or function as batch job
%      changePassword         - Prompt to change password
%      createCommunicatingJob - Create a new communicating job
%      createJob              - Create a new independent job
%      demote                 - Demote job in the queue
%      findJob                - Find job objects belonging to a cluster object
%      isequal                - True if clusters have same property values
%      logout                 - Log user out
%      parpool                - Create a parallel pool of workers on a cluster and return a pool object
%      pause                  - Pause processing of job queue
%      promote                - Promote job in the queue
%      resume                 - Resume processing of job queue
%      saveAsProfile          - Save modified properties to a profile
%      saveProfile            - Save modified properties to the current profile
%
%   parallel.cluster.MJS properties:
%      AllHostAddresses          - IP addresses of the cluster host
%      BusyWorkers               - Workers currently running tasks
%      ClusterMatlabRoot         - Specifies the path to MATLAB that the cluster is using
%      HasSecureCommunication    - True if the cluster is using secure communication
%      Host                      - Host on which this cluster is running
%      IdleWorkers               - Workers that are free to run tasks
%      JobStorageLocation        - Location where job and task data is stored
%      Jobs                      - List of jobs contained in this cluster
%      LicenseNumber             - License number to use when running jobs with this cluster
%      MaxNumWorkers             - Maximum number of workers for this cluster
%      Modified                  - True if any properties in this cluster have been modified
%      Name                      - Name of this cluster
%      NumBusyWorkers            - Number of workers currently running tasks
%      NumIdleWorkers            - Number of workers that are free to run tasks
%      NumThreads                - Number of computational threads for workers
%      NumWorkers                - Number of workers currently running on this cluster
%      OperatingSystem           - Operating system of the nodes used by the cluster
%      PreferredPoolNumWorkers   - Preferred number of workers in a parallel pool
%      Profile                   - Profile used to build this cluster
%      PromptForPassword         - False if the system should error rather than
%      RequiresClientCertificate - True if the cluster requires the client to have a certificate
%      RequiresOnlineLicensing   - True if the cluster is using online licensing
%      SecurityLevel             - Indicates the degree of security applied to the cluster and its jobs
%      State                     - Current state of the cluster
%      SupportedReleases         - The MATLAB releases that this MJS supports
%      Type                      - Type of this cluster
%      UserData                  - Data associated with a cluster object in this client session
%      Username                  - User who is using the cluster
%
%   See also PARCLUSTER, parallel.Cluster.

% Copyright 2011-2023 The MathWorks, Inc.

classdef ( ConstructOnLoad = true, Sealed ) MJS < parallel.Cluster
    
    % An MJS Support providing access to the underlying functionality
    properties ( Transient, GetAccess = private, SetAccess = immutable )
        Support
    end

    % Profile/Construction properties
    properties ( PCTGetSet, Transient, SetAccess = immutable, PCTConstraint = 'string' )
        %Name Name of this cluster 
        %   The descriptive name of an MJS cluster is set when its service
        %   is started, as described in "Customize Services" in the
        %   MATLAB Parallel Server System Administrator's
        %   Guide. You can use the name of the cluster in a profile to
        %   search for the particular service when creating a cluster
        %   object with the parcluster function or the cluster constructor.
        %
        %    See also PARCLUSTER, parallel.cluster.MJS.MJS.
        Name = '';
        
        %Host Host on which this cluster is running
        %   You can use the host of the cluster in a profile to find a
        %   desired cluster when creating the cluster object with
        %   parcluster or the cluster constructor.
        %
        %   See also PARCLUSTER, parallel.cluster.MJS.MJS,
        %            parallel.cluster.MJS.AllHostAddresses,
        %            parallel.cluster.MJS.OperatingSystem.
        Host = '';
        
    end
    % Profile/Construction/Settable properties
    properties ( PCTGetSet, Transient, PCTConstraint = 'string' )
        %Username User who is using the cluster
        %   The Username property value indicates the user who created the
        %   cluster object or who is using the object to access jobs in its
        %   queue.
        %
        %   See also parallel.cluster.MJS.SecurityLevel,
        %            parallel.cluster.MJS.PromptForPassword,
        %            parallel.cluster.MJS.changePassword,
        %            parallel.cluster.MJS.logout.
        Username = '';

        %LicenseNumber License number to use when running jobs with this cluster
        %   License number specifies the license number to use on the
        %   workers when the cluster is using online licensing.
        %   This string must be the same as one of the licenses listed in
        %   your MathWorks account.
        %
        %   LicenseNumber cannot be defined if
        %   RequiresOnlineLicensing is false.
        %    
        %   See also parallel.cluster.MJS.RequiresOnlineLicensing
        LicenseNumber = '';
    end
    
    properties ( PCTGetSet, Transient, SetAccess = immutable, PCTConstraint = 'string', Hidden )
        % Trusted certificate authority which allows us to trust the server
        % certificate during the SSL handshake.
        Certificate = '';

        % Client certificate used by client during SSL handshake. This
        % certificate is expected to be signed such that the server will
        % trust it.
        ClientCertificate = '';

        % Client private key used by client during SSL handshake. This is
        % the sensitive data which authenticates the client to the server.
        ClientPrivateKey = '';
    end
    properties ( Transient, Hidden )
        ClusterLogLevel
        ClusterLogSpec
        MpiLibraryName
    end
    properties ( Transient, Hidden, SetAccess = immutable )
        WorkerProxiesPoolConnections;
    end
    
    % MJS Clusters have a variety of properties that are Dependent and derived by
    % querying the Support object.
    properties ( Dependent, SetAccess = private )
        %BusyWorkers Workers currently running tasks
        %   The BusyWorkers property value indicates which workers are
        %   currently running tasks for the cluster.
        %   (read-only)
        %
        %   As workers complete tasks and assume new ones, the lists of
        %   workers in BusyWorkers and IdleWorkers can change rapidly. If
        %   you examine these two properties at different times, you might
        %   see the same worker on both lists if that worker has changed
        %   its status between those times. If a worker stops unexpectedly,
        %   the cluster's knowledge of that as a busy or idle worker does
        %   not get updated until the cluster runs the next job and tries
        %   to send a task to that worker
        % 
        %   See also parallel.cluster.MJS.NumWorkers,
        %            parallel.cluster.MJS.IdleWorkers,
        %            parallel.cluster.MJS.NumBusyWorkers, 
        %            parallel.cluster.MJS.NumIdleWorkers, 
        %            parallel.job.MJSIndependentJob.NumWorkersRange,
        %            parallel.job.MJSCommunicatingJob.NumWorkersRange.
        BusyWorkers
        
        %IdleWorkers Workers that are free to run tasks
        %   The IdleWorkers property value indicates which workers are
        %   currently not working on any tasks and are available to the
        %   cluster for the performance of job tasks. 
        %   (read-only)
        %
        %   As workers complete tasks and assume new ones, the lists of
        %   workers in BusyWorkers and IdleWorkers can change rapidly. If
        %   you examine these two properties at different times, you might
        %   see the same worker on both lists if that worker has changed
        %   its status between those times. If a worker stops unexpectedly,
        %   the cluster's knowledge of that as a busy or idle worker does
        %   not get updated until the cluster runs the next job and tries
        %   to send a task to that worker.
        % 
        %   See also parallel.cluster.MJS.NumWorkers,
        %            parallel.cluster.MJS.BusyWorkers,
        %            parallel.cluster.MJS.NumBusyWorkers, 
        %            parallel.cluster.MJS.NumIdleWorkers, 
        %            parallel.job.MJSIndependentJob.NumWorkersRange,
        %            parallel.job.MJSCommunicatingJob.NumWorkersRange.
        IdleWorkers
        
        %NumBusyWorkers Number of workers currently running tasks
        %   The NumBusyWorkers property value indicates how many workers
        %   are currently running tasks for the cluster.
        %   (read-only)
        %
        %   The value of NumBusyWorkers can range from 0 up to the total
        %   number of workers registered with the cluster.
        %
        %   See also parallel.cluster.MJS.NumWorkers,
        %            parallel.cluster.MJS.BusyWorkers,
        %            parallel.cluster.MJS.IdleWorkers, 
        %            parallel.cluster.MJS.NumIdleWorkers, 
        %            parallel.job.MJSIndependentJob.NumWorkersRange,
        %            parallel.job.MJSCommunicatingJob.NumWorkersRange.
        NumBusyWorkers
        
        %NumIdleWorkers Number of workers that are free to run tasks
        %   The NumIdleWorkers property value indicates how many workers
        %   are currently not working on any tasks and are available to the
        %   cluster for the performance of job tasks. If the NumIdleWorkers
        %   is equal to or greater than the lower value of NumWorkersRange
        %   of the job at the front of the queue, that job can start
        %   running. (read-only)
        %
        %   The value of NumIdleWorkers can range from 0 up to the total
        %   number of workers registered with the cluster.
        %
        %   See also parallel.cluster.MJS.NumWorkers,
        %            parallel.cluster.MJS.BusyWorkers,
        %            parallel.cluster.MJS.IdleWorkers, 
        %            parallel.cluster.MJS.NumBusyWorkers, 
        %            parallel.job.MJSIndependentJob.NumWorkersRange,
        %            parallel.job.MJSCommunicatingJob.NumWorkersRange.
        NumIdleWorkers
        
        %State Current state of the cluster
        %   A cluster can be in state paused, running, or unavailable.
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.pause,
        %            parallel.cluster.MJS.resume.
        State

        %SupportedReleases The MATLAB releases that this MJS supports
        %   The SupportedReleases property value indicates which releases
        %   of MATLAB are supported by this MJS. To use this MJS your
        %   client MATLAB release must be in this list.
        SupportedReleases
    end
    properties ( Dependent )
        %NumWorkers Number of workers currently running on this cluster
        %   NumWorkers indicates the total number of workers currently
        %   available to the cluster for running your jobs.
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.BusyWorkers,
        %            parallel.cluster.MJS.IdleWorkers, 
        %            parallel.cluster.MJS.MaxNumWorkers,
        %            parallel.cluster.MJS.NumBusyWorkers, 
        %            parallel.cluster.MJS.NumIdleWorkers.
        NumWorkers
        
        %MaxNumWorkers Maximum number of workers for this cluster
        %   MaxNumWorkers indicates the maximum allowed size of this
        %   cluster. New workers will be added to the cluster up to and
        %   including this value, as necessary.
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.NumWorkers.
        MaxNumWorkers

        %ClusterMatlabRoot Specifies the path to MATLAB that the cluster is using
        %   ClusterMatlabRoot specifies the path name to MATLAB for the
        %   cluster to use for starting MATLAB worker processes. 
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.JobStorageLocation
        ClusterMatlabRoot

        %OperatingSystem Operating system of the nodes used by the cluster
        %   OperatingSystem can be pc, unix or mixed.
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.Name, parallel.cluster.MJS.Host.
        OperatingSystem

        %JobStorageLocation Location where job and task data is stored
        %   (read-only)
        %   
        %   See also parallel.cluster.MJS.ClusterMatlabRoot
        JobStorageLocation  
        
        %PromptForPassword False if the system should error rather than
        %prompt the user
        %   The PromptForPassword property is true by default, so that when
        %   you access a cluster object, if you do not already have a
        %   password stored, the system prompts you to enter it.
        %
        %   Setting PromptForPassword to false causes the system to
        %   generate an error when a password is required. This can be
        %   useful when you have a non-interactive script or function that
        %   programmatically accesses the cluster, and you might prefer an
        %   error rather than a password prompt.
        %
        %   See also parallel.cluster.MJS.SecurityLevel,
        %            parallel.cluster.MJS.Username,
        %            parallel.cluster.MJS.changePassword,
        %            parallel.cluster.MJS.logout.
        PromptForPassword         

        %RequiresOnlineLicensing True if the cluster is using online licensing
        %   RequiresOnlineLicensing indicates whether the cluster uses
        %   online licensing.  You must provide your MathWorks account
        %   login information if this value is true.  If you have more than one
        %   MATLAB Parallel Server license associated with your
        %   MathWorks account you will also need to supply the LicenseNumber to
        %   use.  (read-only)
        %
        %   See also parallel.cluster.MJS.LicenseNumber.
        RequiresOnlineLicensing
    end

    properties ( Transient, SetAccess = immutable )
        %AllHostAddresses IP addresses of the cluster host
        %   AllHostAddresses indicates the IP addresses of the computer
        %   running the cluster. You can use one of the IP addresses as the
        %   value for the host of the cluster in a profile to find a
        %   desired cluster when creating the cluster object with
        %   parcluster or the cluster constructor.
        %   (read-only)
        %
        %   See also PARCLUSTER, parallel.cluster.MJS.MJS, 
        %            parallel.cluster.MJS.Host,
        %            parallel.cluster.MJS.OperatingSystem.
        AllHostAddresses           = {};
        
        %HasSecureCommunication True if the cluster is using secure communication
        %   The HasSecureCommunication property indicates whether secure
        %   communication is being used between the job manager and the
        %   workers. The mjs_def file sets the parameter that controls
        %   secure communication when the mjs process starts on the
        %   cluster nodes.  
        %   
        %   Secure communication is required when running
        %   with SecurityLevel set to 3. It is optional at other security
        %   levels.
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.SecurityLevel, 
        %            parallel.cluster.MJS.Username,
        %            parallel.cluster.MJS.PromptForPassword,
        %            parallel.cluster.MJS.changePassword,
        %            parallel.cluster.MJS.logout.
        HasSecureCommunication = false;
        
        %RequiresClientCertificate True if the cluster requires the client to have a certificate
        %   The RequiresClientCertificate property indicates whether the
        %   job manager requires clients to have a certificate for
        %   establishing secure communication. The mjs_def file sets
        %   the parameter that controls requiring client certificates
        %   when the mjs process starts on the cluster nodes.
        %
        %   Client certificates can only be used when also using
        %   secure communication.
        %   (read-only)
        %
        %   See also parallel.cluster.MJS.HasSecureCommunication
        RequiresClientCertificate = false;

        %SecurityLevel Indicates the degree of security applied to the cluster and its jobs
        %   The SecurityLevel property indicates the degree of security
        %   applied to the cluster and its jobs. The mjs_def file sets
        %   the parameter that controls security level when the mjs
        %   process starts on the cluster nodes.
        %   (read-only)
        %
        %   The job manager and the workers should run at the same security
        %   level. A worker running at too low a security level will fail
        %   to register with the job manager, because the job manager does
        %   not trust it. 
        %
        %   SecurityLevel can have the following values:
        %   0 - No security. All users can access all jobs; the
        %       AuthorizedUsers property of the job is ignored.
        %   1 - You are warned when you try to access other users' jobs and
        %       tasks, but can still perform all actions. You can suppress
        %       the warning by adding your user name to the AuthorizedUsers
        %       property of the job.
        %   2 - Authentication required. You must enter a password to
        %       access any jobs and tasks. You cannot access other users'
        %       jobs unless your user name is included in the job's
        %       AuthorizedUsers property.
        %   3 - Same as level 2, but in addition, tasks run on the workers
        %       as the user to whom the job belongs. The user name and
        %       password for authentication in the client session need to
        %       be the same as the system password used to log on to a
        %       worker machine. NOTE: This level requires secure
        %       communication between job manager and workers. Secure
        %       communication is also set in the mjs_def file, and is
        %       indicated by a cluster's IsUsingSecureCommunication
        %       property.
        %
        %   See also parallel.cluster.MJS.HasSecureCommunication,
        %            parallel.cluster.MJS.Username,
        %            parallel.cluster.MJS.PromptForPassword,
        %            parallel.cluster.MJS.changePassword,
        %            parallel.cluster.MJS.logout.
        SecurityLevel              = 0;
    end

    % API2(orig) compatibility
    properties ( Dependent, Hidden )
        IsUsingSecureCommunication
        % Used with resizable MJS clusters
        IsResizable
        MaxCapacity
        % Compatibility for rename of MHLM to online licensing
        RequiresMathWorksHostedLicensing
    end
    
    properties ( Transient, SetAccess = private, Hidden )
        % Key for storing TLSConnectInfo succinctly in memento
        TLSConnectKey = "";
    end

    properties ( Constant, GetAccess = private )
        %MinimalMementoProperties A string array that defines the
        % properties needed for a minimal cluster memento (i.e. one that
        % allows a cluster object to be constructed that can find the
        % original cluster's jobs). A property should only be included if
        % it is required in the memento for this class and every sub-class.
        %
        % A sub-class can define additional properties to be included in
        % the memento in a constant, private property of the same name. A
        % sub-class should not include properties that are included by any
        % of its super-classes.  A super-class should only define a
        % property if it should be included in the memento of each of its
        % sub-classes.
        %
        % The full list of properties required by any class can be gathered
        % using hDetermineMementoProperties.
        MinimalMementoProperties = [ ...
            "Host", ...
            "Name", ...
            "Username", ...
            "TLSConnectKey" ];
        
        % A string array that stores all the properties needed for a
        % minimal cluster memento for this class. It should include all
        % those defined in MinimalMementoProperties in this class and in
        % all superclasses.
        AllPropertiesForMinimalMemento = parallel.Cluster.hDetermineMementoProperties( ...
            ?parallel.cluster.MJS, parallel.internal.types.ClusterMementoType.Minimal );
        
        % A string array that stores all the properties needed for a full
        % cluster memento for this class. It should include all those
        % defined in MinimalMementoProperties and FullMementoProperties in
        % this class and in all superclasses.
        AllPropertiesForFullMemento = parallel.Cluster.hDetermineMementoProperties( ...
            ?parallel.cluster.MJS, parallel.internal.types.ClusterMementoType.Full );
    end
    
    methods
        function v = get.IsUsingSecureCommunication( obj )
            v = obj.HasSecureCommunication;
        end

        % Conversion from new name to old name for online licensing (formerly MHLM)
        function v = get.RequiresMathWorksHostedLicensing( obj )
            v = obj.RequiresOnlineLicensing;
        end

        function set.RequiresMathWorksHostedLicensing( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'RequiresMathWorksHostedLicensing' ) );
        end
    end
    
    % Access properties via the support. iGetClusterProps incorporates
    % handleJavaException.
    methods
        function v = get.State( obj )
            se = iGetClusterProps( obj, 'StateEnum' );
            v  = se.Name;
        end
        function v = get.NumBusyWorkers( obj )
            v = iGetClusterProps( obj, 'NumBusyWorkers' );
        end
        function v = get.NumIdleWorkers( obj )
            v = iGetClusterProps( obj, 'NumIdleWorkers' );
        end
        function cmr = get.ClusterMatlabRoot( obj )
            cmr = iGetClusterProps( obj, 'ClusterMatlabRoot' );
        end
        function set.ClusterMatlabRoot( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'ClusterMatlabRoot' ) );
        end
        function set.NumWorkers( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'NumWorkers' ) );
        end
        function set.JobStorageLocation( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'JobStorageLocation' ) );
        end
        function v = get.JobStorageLocation( obj )
            m = message( 'parallel:cluster:MJSJobStorageLocation', obj.Host );
            v = m.getString();
        end
        function v = get.OperatingSystem( obj )
            v = iGetClusterProps( obj, 'ClusterOperatingSystem' );
        end
        function set.OperatingSystem( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'OperatingSystem' ) );
        end
        function nw = get.NumWorkers( obj )
            nw = iGetClusterProps( obj, 'NumWorkers' );
        end
        function v = get.ClusterLogLevel( obj )
            v = iGetClusterProps( obj, 'ClusterLogLevel' );
        end
        function v = get.ClusterLogSpec( obj )
            v = iGetClusterProps( obj, 'ClusterLogSpec' );
        end
        function v = get.MpiLibraryName( obj )
            v = iGetClusterProps( obj, 'MpiLibraryName' );
        end
        function set.ClusterLogLevel( obj, logLevel )
            try
                validateattributes( logLevel, {'numeric'}, {'scalar', '>=', 0, '<=', 6}, 'setClusterLogLevel' );
                iSetClusterProps( obj, 'ClusterLogLevel', logLevel );
            catch E
                throwAsCaller( distcomp.handleJavaException( obj, E ) );
            end
        end
        function set.ClusterLogSpec( obj, logSpec )
            logSpec = convertStringsToChars( logSpec );
            try
                validateattributes( logSpec, {'char'}, {'scalartext'}, 'setClusterLogSpec' );
                iSetClusterProps( obj, 'ClusterLogSpec', logSpec );
            catch E
                throwAsCaller( distcomp.handleJavaException( obj, E ) );
            end
        end

        % The MJSSupport.getWorkers() method does not handleJavaException, so we
        % must.
        function bw = get.BusyWorkers( obj )
            try
                bw = obj.Support.getWorkers( obj, 'BusyWorkers' );
            catch E
                throwAsCaller( distcomp.handleJavaException( obj, E ) );
            end
        end
        function iw = get.IdleWorkers( obj )
            try
                iw = obj.Support.getWorkers( obj, 'IdleWorkers' );
            catch E
                throwAsCaller( distcomp.handleJavaException( obj, E ) );
            end
        end
        function prompt = get.PromptForPassword( obj )
            prompt = iGetClusterProps( obj, 'PromptForPassword' );
        end
        function set.PromptForPassword( obj, promptForPassword )
            try
                parallel.internal.customattr.checkConstraint( 'logicalscalar', obj, 'PromptForPassword', promptForPassword );
                iSetClusterProps( obj, 'PromptForPassword', promptForPassword );
            catch E
                throwAsCaller( E );
            end
        end
        function tf = get.RequiresOnlineLicensing(obj)
            tf = iGetClusterProps( obj, 'RequiresOnlineLicensing' );
        end
        function set.RequiresOnlineLicensing(obj, ~)
            throwAsCaller( obj.buildCannotModify( 'RequiresOnlineLicensing' ) );
        end
        function supportedReleases = get.SupportedReleases( obj )
            supportedReleases = iGetClusterProps( obj, 'SupportedReleases' );
        end
        function maxCapacity = get.MaxCapacity( obj )
            maxCapacity = iGetClusterProps( obj, 'MaxCapacity' );
        end
        function set.IsResizable( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'IsResizable' ) );
        end
        function isResizable = get.IsResizable( obj )
            isResizable = iGetClusterProps( obj, 'IsResizable' );
        end
        function set.MaxCapacity( obj, maxCapacity )
            try
                iSetClusterProps( obj, 'MaxCapacity', maxCapacity );
            catch E
                throwAsCaller( E );
            end
        end
        function maxNumWorkers = get.MaxNumWorkers( obj )
            maxNumWorkers = iGetClusterProps( obj, 'MaxNumWorkers' );
        end
        function set.MaxNumWorkers( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'MaxNumWorkers' ) );
        end
    end

    methods
        function obj = MJS( varargin )
            %MJS Create a MJS cluster instance
            %   parallel.cluster.MJS(p1, v1, p2, v2, ...) builds an MJS
            %   cluster with the specified property values. The properties can
            %   include any of the properties of the MJS cluster class, or a
            %   profile.
            %
            %   See also PARCLUSTER, parallel.Cluster.

            import parallel.internal.settings.ValueType
            import parallel.internal.mjs.TLSConnectInfo;

            if nargin == 2 ...
                    && isa( varargin{1}, ...
                    'com.mathworks.toolbox.distcomp.mjs.jobmanager.JobManagerLocal' ) ...
                    && isa( varargin{2}, ...
                    'com.mathworks.toolbox.distcomp.mjs.auth.credentials.Credentials' )
                % Constructor used by distcomp_evaluate_task
                % PCT_JT_CLEANUP g2364333 - Remove
                proxy       = varargin{1};
                credentials = varargin{2};
                % The following properties are faked out since we don't have
                % normal arguments.
                propsFromArgs   = containers.Map();
                cProf           = '';
                hostType        = ValueType.Factory;
                nameType        = ValueType.Factory;
                usernameType    = ValueType.Factory;
                certificateType = ValueType.Factory;
                clientCertificateType = ValueType.Factory;
                clientPrivateKeyType = ValueType.Factory;
                tlsConnectInfo = TLSConnectInfo();
                tlsConnectKey = "";
                isWorker = true;

                cache = parallel.internal.cluster.TwoLevelCache();
                accessFactory = parallel.internal.cluster.WorkerJmAccessFactory( cache, proxy, credentials );
                
                % Support builder function for java worker
                supportBuildFcn = @() parallel.internal.cluster.MJSSupport( cache, accessFactory );
            else
                % Extract all supported non-property args
                [credentials, varargin] = iExtractNameValuePair( 'Credentials', [], varargin{:} );
                [cachedData, varargin] = iExtractNameValuePair( 'CachedData', [], varargin{:} );
                [endpoint, varargin] = iExtractNameValuePair( 'Endpoint', [], varargin{:} );
                % Workers have the connect Endpoint passed directly in
                isWorker = ~isempty(endpoint);
                [~, undoc] = pctconfig;
                % We don't even want to construct the MJSEndpointDiscoverer on workers, because
                % this triggers unnecessary loading of the SPF Endpoint Manager infrastructure.
                % See g2690147.
                defaultDiscoverer = [];
                if ~isWorker
                    defaultDiscoverer = parallel.internal.mjs.defaultMJSEndpointDiscoverer();
                end
                [discoverer, varargin] = iExtractNameValuePair( 'Discoverer', ...
                    defaultDiscoverer, varargin{:} );
                [certificateFile, varargin] = iExtractNameValuePair( 'CertificateFile', "", varargin{:} );

                % Extract profile and cluster properties
                try
                    [propsFromArgs, cProf] = parallel.internal.cluster.ConstructorArgsHelper.interpretClusterCtorArgs( ...
                        ?parallel.cluster.MJS, varargin{:} );
                catch E
                    % User error in args
                    throw( E );
                end
                [name, nameType] = iMaybeGetFromMapAndRemove( propsFromArgs, 'Name', [] );
                [username, usernameType]       = iMaybeGetFromMapAndRemove( propsFromArgs, 'Username', distcomp.pGetDefaultUsername() );
                [host, hostType] = iMaybeGetFromMapAndRemove( propsFromArgs, 'Host', [] );
                [certificate, certificateType] = iMaybeGetFromMapAndRemove( propsFromArgs, 'Certificate', '' );
                [clientCertificate, clientCertificateType] = iMaybeGetFromMapAndRemove( propsFromArgs, 'ClientCertificate', '' );
                [clientPrivateKey, clientPrivateKeyType] = iMaybeGetFromMapAndRemove( propsFromArgs, 'ClientPrivateKey', '' );
                promptForPassword = iMaybeGetFromMapAndRemove( propsFromArgs, 'PromptForPassword', parallel.internal.cluster.defaultMJSCredentialPromptsEnabled() );
                tlsConnectKey = string(iMaybeGetFromMapAndRemove( propsFromArgs, 'TLSConnectKey', "" ));

                % Load TLSConnectInfo
                if ~strcmp(certificateFile, "")
                    % 'CertificateFile' argument takes priority
                    tlsConnectInfo = TLSConnectInfo.loadFromFile(certificateFile);
                    certificateType = ValueType.User;
                    clientCertificateType = ValueType.User;
                    clientPrivateKeyType = ValueType.User;
                else
                    if isWorker
                        % Workers get their tls info from the Endpoint
                        tlsConnectInfo = TLSConnectInfo.loadFromEndpoint(endpoint);
                    else
                        if ~strcmp(tlsConnectKey, "")
                            % Try and load tlsConnectInfo from cache if created
                            % from cluster memento
                            tlsConnectInfo = TLSConnectInfo.loadFromCache(tlsConnectKey);
                        else
                            % Constructed from profile or directly
                            tlsConnectInfo = TLSConnectInfo(certificate, clientPrivateKey, clientCertificate);
                        end
                    end
                end

                if isWorker
                    % Support builder function for spf worker
                    supportBuildFcn = @() parallel.internal.cluster.MJSSupportWrapperProvider.createWorkerSupport( endpoint, name, username, credentials, cachedData );
                else
                    % PCT_JT_CLEANUP g2364333 - Remove
                    % ExperimentalVersion switching code and java paths
                    useSPF = ~feature("pctUseJavaTransport");
                    if useSPF
                        if isempty(host) || strcmp(host, "")
                            endpointFactoryFcn = @() discoverer.discover();
                        else
                            % Connect to a known lookup service
                            endpointFactoryFcn = @() parallel.internal.mjs.createLookupConnectEndpoint(host);
                        end
                        specifier = parallel.internal.mjs.MJSEndpointSpecifier(endpointFactoryFcn, host, name);
                        
                        selector = parallel.internal.mjs.MJSSelector(specifier, undoc.mjslookupconnecttimeout);
                        % Support builder function for spf client
                        supportBuildFcn = @() parallel.internal.cluster.MJSSupportWrapperProvider.createClientSupport( selector, promptForPassword, username, tlsConnectInfo );
                    else
                        % The certificate needs to be installed before the lookup is done.
                        certRemovalCleanup = parallel.internal.cluster.installJmCertificate(tlsConnectInfo.Certificate);
    
                        % Defer building the support object until we have a MJS
                        % instance to pass to handleJavaException
                        cache = parallel.internal.cluster.TwoLevelCache();
                        accessFactory = parallel.internal.cluster.ClientJmAccessFactory( cache, host, name, username, promptForPassword );
                        % Support builder function for java client
                        supportBuildFcn = @() parallel.internal.cluster.MJSSupport( cache, accessFactory, certRemovalCleanup );
                    end
                end
            end

            obj@parallel.Cluster( 'MJS', cProf );
            
            try 
                dctSchedulerMessage(2, 'About to connect to MATLAB Job Scheduler');
                obj.Support = supportBuildFcn();
                dctSchedulerMessage(2, 'Successfully connected to MATLAB Job Scheduler');
            catch E
                % Ideally wouldn't pass obj out until it is fully
                % constructed, but we know that handleJavaException is only
                % calling "isa"
                if strcmp( E.identifier, 'parallel:cluster:MJSMultipleFound' )
                    if isempty( cProf )
                        ex = MException(message('parallel:cluster:MJSMultipleFoundSpecifyHostInConstructor'));
                    else
                        ex = MException(message('parallel:cluster:MJSMultipleFoundSpecifyHostInProfile', cProf));
                    end
                    ex = ex.addCause( E );
                    throw( ex );
                else
                    throw( distcomp.handleJavaException( obj, E ) );
                end
            end

            factoryLicenseNumber    = '';
            actualHost              = iGetClusterProps( obj, 'Host');
            actualName              = iGetClusterProps( obj, 'Name');
            actualUsername          = iGetClusterProps( obj, 'Username');
            
            % Set up the PropStorage with the actual name and host, but indicate
            % where we got those values from.
            obj.PropStorage.addFactoryValue( 'Name', actualName );
            obj.PropStorage.setValueType(    'Name', nameType );
            obj.PropStorage.addFactoryValue( 'Host', actualHost );
            obj.PropStorage.setValueType(    'Host', hostType );
            obj.PropStorage.addFactoryValue( 'Username', actualUsername );
            obj.PropStorage.setValueType(    'Username', usernameType );
            obj.PropStorage.addFactoryValue( 'Certificate', char(tlsConnectInfo.Certificate) );
            obj.PropStorage.setValueType(    'Certificate', certificateType );
            obj.PropStorage.addFactoryValue( 'ClientCertificate', char(tlsConnectInfo.ClientCertificate) );
            obj.PropStorage.setValueType(    'ClientCertificate', clientCertificateType );
            obj.PropStorage.addFactoryValue( 'ClientPrivateKey', char(tlsConnectInfo.ClientPrivateKey) );
            obj.PropStorage.setValueType(    'ClientPrivateKey', clientPrivateKeyType );
            obj.PropStorage.addFactoryValue( 'LicenseNumber', factoryLicenseNumber );

            % Set TLS key for memento
            if ~isWorker && strcmp(tlsConnectKey, "")
                % Store tlsConnectInfo in cache so we can reference it
                % with the much smaller alias in memento
                tlsConnectKey = tlsConnectInfo.addToCache();
            end
            obj.TLSConnectKey = tlsConnectKey;
            
            % Set up immutable properties
            obj.AllHostAddresses = ...
                iGetClusterProps( obj, 'AllHostAddresses' );
            obj.HasSecureCommunication = ...
                iGetClusterProps( obj, 'HasSecureCommunication' );
            obj.RequiresClientCertificate = ...
                iGetClusterProps( obj, 'RequiresClientCertificate' );
            obj.SecurityLevel = ...
                iGetClusterProps( obj, 'SecurityLevel' );
            obj.WorkerProxiesPoolConnections = ...
                iGetClusterProps( obj, 'WorkerProxiesPoolConnections' );
            % Ensure remaining constructor args go through the correct set methods.
            obj.applyConstructorArgs(propsFromArgs);

            % Check two-way communications. We do not force a new check here
            % as the check will already have been performed by the ServiceAccessor.
            forceCheck = false;
            [commsOk, msg] = obj.hCheckTwoWayCommunications( forceCheck );
            if ~commsOk
                parallel.internal.warningNoBackTrace(message('parallel:cluster:MJSCommunicationProblem', msg));
            end
            
            % Listen for MATLAB logout events if the cluster is using
            % online licensing. RequiresOnlineLicensing never changes
            % for an MJS cluster, so we only need to worry about doing
            % this in the constructor.
            if obj.RequiresOnlineLicensing
                obj.listenForMatlabLogout();
            end
        end

        function pause( mjs )
            % PAUSE Pause processing of job queue
            %   PAUSE(myCluster) pauses the cluster's queue so that jobs
            %   waiting in the queued state will not run. Jobs that are
            %   already running also pause, after completion of tasks that
            %   are already running. No further jobs or tasks will run
            %   until the resume function is called for the cluster.
            %
            %   The pause function does nothing if the cluster is already
            %   paused.
            %
            %   See also parallel.cluster.MJS.resume,
            %            parallel.cluster.MJS.State.
            validateattributes( mjs, {'parallel.Cluster'}, {'nonempty', 'scalar'}, 'pause', 'cluster', 1 );            
            try
                mjs.Support.pauseQueue();
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end

        function resume( mjs )
            %RESUME Resume processing of job queue
            %   RESUME(myCluster) resumes processing of the cluster's queue
            %   so that jobs waiting in the queued state will be run. This
            %   call will do nothing if the cluster is not paused.
            %
            %   See also parallel.cluster.MJS.pause,
            %            parallel.cluster.MJS.State.
            validateattributes( mjs, {'parallel.Cluster'}, {'nonempty', 'scalar'}, 'resume', 'cluster', 1 );
            try
                mjs.Support.resumeQueue();
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        
        function logout( mjs )
            %LOGOUT Log user out
            %    LOGOUT(myCluster) logs you out of the cluster. Any
            %    subsequent call to a privileged action will require you to
            %    re-authenticate with a valid password. Logging out might
            %    be useful after you have finished working on a shared
            %    machine.
            %
            %    See also parallel.cluster.MJS.changePassword,
            %             parallel.cluster.MJS.Username
            
            try
                mjs.Support.logout();
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        
        function changePassword( mjs, username )
            % changePassword Prompt to change password
            %   changePassword(myCluster) prompts you to change your
            %   password. Your current password must be entered as well as
            %   the new password.
            %
            %   changePassword(myCluster, userName) allows you to set 
            %   another user's password when you are currently logged in as
            %   the MJS cluster 'admin' user. You must provide the admin
            %   password followed by the specified user's new password.
            %   This allows an admin to reset a password if a user has
            %   forgotten it.
            %   
            %   See also parallel.cluster.MJS.logout,
            %            parallel.cluster.MJS.Username.
             
            if nargin > 1
                % Convert any string inputs to character vectors
                username = convertStringsToChars(username);
                validateattributes( username, {'char'}, {}, 'changePassword', 'username' );
                args = {username};
            else
                args = {};
            end
            
            try
                mjs.Support.changePassword(args{:});
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        
        function promote( mjs, job )
            %PROMOTE Promote job in the queue
            %   PROMOTE(myCluster, job) if job is not the first job in the
            %   queue, the position of job and the job that precedes it are
            %   exchanged.
            %
            %   After a call to promote or demote, there is no change in
            %   the order of job objects contained in the Jobs property of
            %   the cluster. To see the scheduled order of execution for
            %   jobs in the queue, use the findJob function in the form
            %   [pending queued running finished] = findJob(myCluster).
            %
            %   See also parallel.cluster.MJS.demote,
            %            parallel.cluster.MJS.findJob.
            validateattributes( job, {'parallel.Job'}, {'nonempty', 'scalar'}, 'promote', 'job' );
            
            if mjs ~= job.Parent
                error(message('parallel:cluster:InvalidJobToPromote'));                
            end
            
            try
                % Call the job's promote method
                job.hPromote();
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        
        function demote( mjs, job )       
            %DEMOTE Demote job in the queue
            %   DEMOTE(myCluster, job) if job is not the last job in the
            %   queue, the position of job and the job that follows it are
            %   exchanged.
            %
            %   After a call to promote or demote, there is no change in
            %   the order of job objects contained in the Jobs property of
            %   the cluster. To see the scheduled order of execution for
            %   jobs in the queue, use the findJob function in the form
            %   [pending queued running finished] = findJob(myCluster).
            %
            %   See also parallel.cluster.MJS.promote,
            %            parallel.cluster.MJS.findJob.
            
            validateattributes( job, {'parallel.Job'}, {'nonempty', 'scalar'}, 'demote', 'job' );
            
            if mjs ~= job.Parent
                error(message('parallel:cluster:InvalidJobToDemote'));                
            end
            
            try
                % Call the job's demote method
                job.hDemote();
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
                
        end        
    end

    methods ( Sealed, Hidden )        
        function support = hGetSupport( obj )
        % Return the underlying MJS Support. For use by tests.
            support = obj.Support;
        end
        
        function [taskIDs, taskSIds] = hBuildTasksVec( obj, varargin )
            [taskIDs, taskSIds] = obj.Support.buildTasksVec( varargin{:} );
        end
    end

    methods ( Hidden )
        
        function tf = hIsClusterAvailable( obj )
            import parallel.internal.types.States
            tf = States.fromName( obj.State ) ~= States.Unavailable;
        end
        function hSetProperty( obj, names, values )
            if ~iscell( names )
                names  = { names };
                values = { values };
            end
            clusterProperties = {'PromptForPassword', 'Username'};
            licenseNumberProperty = {'LicenseNumber'};
            for ii = 1:length( names )
                if ismember( names{ii}, clusterProperties )
                    iSetClusterProps( obj, names{ii}, values{ii} );
                elseif ismember( names{ii}, licenseNumberProperty ) && ...
                        obj.isNotCurrentValue('LicenseNumber', values{ii})
                    % If the license number is different to the currently
                    % stored number then check its validity before setting
                    % and make sure we stash the corresponding entitlement.
                    try
                        obj.LicenseEntitlement = obj.checkLicenseNumber( values{ii} );
                    catch E
                        throwAsCaller( E )
                    end
                end                
                obj.hSetPropertyNoCheck( names{ii}, values{ii} );
            end
        end

        function [job, task] = hGetJobAndTaskFromStructs( obj, jobStruct, taskStruct )
        % Used by distcomp_evaluate_task to get the job and task to execute.
            try
                job  = obj.Support.getJobFromStruct( obj, jobStruct );
                task = obj.Support.getTaskFromStruct( job, jobStruct, taskStruct );
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end

        function newjob = hBuildChild( mjs, variant, jobID, jobSId )
        % Build a new job object of the requested variant. Called by the support
        % which caches the resulting new job.

            import parallel.job.MJSIndependentJob
            import parallel.job.MJSCommunicatingJob
            import parallel.internal.types.Variant

            try
                switch variant
                  case Variant.IndependentJob
                    newjob = MJSIndependentJob( ...
                        mjs, jobID, jobSId, mjs.Support );
                  case { Variant.CommunicatingSPMDJob, ...
                         Variant.CommunicatingPoolJob }
                    newjob = MJSCommunicatingJob( ...
                        mjs, jobID, variant, jobSId, mjs.Support );
                  otherwise
                    error(message('parallel:cluster:MJSUnexpectedType'));
                end
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        
        % Add Cluster Properties to Display
        function [clusterPropertyMap, propNames] = hGetDisplayItems(obj, diFactory)
            % Create an empty map to hold displayable cluster properties
            clusterPropertyMap = containers.Map('KeyType', 'char', 'ValueType', 'any');
            
            % Add default displayable items to map.
            names = {...
                'Profile', ...
                'Modified', ...
                'Host', ...
                'NumThreads', ...
                'PreferredPoolNumWorkers', ...
                'LicenseNumber'};
            values = diFactory.makeMultipleItems(@createDefaultItem, obj.hGetDisplayPropertiesNoError(names));
            clusterPropertyMap = [clusterPropertyMap; containers.Map(names, values)];

            % Add non-default display items to map
            clusterPropertyMap('JobStorageLocation') = diFactory.createJobStorageStructItem(obj.hGetDisplayPropertiesNoError('JobStorageLocation'));
            clusterPropertyMap('Separator') = diFactory.createSeparator();

            % Use iGetClusterProps to get values for cluster props when 
            % possible, as requesting multiple properties simultaneously 
            % is much more efficient.
            names = {...
                'AllHostAddresses', ...
                'ClusterMatlabRoot', ...
                'ClusterOperatingSystem', ...
                'IsResizable', ...
                'Name', ...
                'RequiresOnlineLicensing', ...
                'SupportedReleases', ...
                'Username', ...
                'WorkerPoolSummary', ...
                };
            values = diFactory.makeMultipleItems(@createDefaultItem, obj.hGetDisplayPropertiesNoError(names, @iGetClusterProps));
            clusterPropertyMap = [clusterPropertyMap; containers.Map(names, values)];
            clusterPropertyMap('OperatingSystem') = clusterPropertyMap('ClusterOperatingSystem');

            workerPoolSummary = clusterPropertyMap('WorkerPoolSummary').getValue();
            clusterPropertyMap('MaxNumWorkers') = diFactory.createDefaultItem(workerPoolSummary.MaxNumWorkers);
            clusterPropertyMap('NumBusyWorkers') = diFactory.createDefaultItem(workerPoolSummary.NumBusyWorkers);
            clusterPropertyMap('NumIdleWorkers') = diFactory.createDefaultItem(workerPoolSummary.NumIdleWorkers);
            clusterPropertyMap('NumWorkers') = diFactory.createDefaultItem(workerPoolSummary.NumWorkers);

            % Add properties that are stored locally and/or not gotten with iGetClusterProps
            clusterPropertyMap('SecurityLevel') = diFactory.createWrappedTextItem(iSecurityLevelInfo(obj.hGetDisplayPropertiesNoError('SecurityLevel')));
            clusterPropertyMap('HasSecureCommunication') = diFactory.createDefaultItem(obj.hGetDisplayPropertiesNoError('HasSecureCommunication'));
            clusterPropertyMap('RequiresClientCertificate') = diFactory.createDefaultItem(obj.hGetDisplayPropertiesNoError('RequiresClientCertificate'));

            % Only include certain properties in the display for resizable
            % clusters
            if clusterPropertyMap('IsResizable').getValue()
                maxNumWorkers = {'MaxNumWorkers'};
            else
                maxNumWorkers = {};
            end

            % The order the properties appear in the propNames array is the
            % order in which they will be displayed. 
            propNames = [{...
                'Name', ...
                'Profile', ...
                'Modified', ...
                'Host', ...
                'Username', ...
                'Separator', ...
                'NumWorkers'}, ...
                maxNumWorkers, {...
                'NumThreads', ...
                'NumBusyWorkers', ...
                'NumIdleWorkers', ...
                'Separator', ...
                'JobStorageLocation', ...
                'ClusterMatlabRoot', ...
                'SupportedReleases', ...
                'OperatingSystem', ...
                'AllHostAddresses', ...
                'SecurityLevel', ...
                'HasSecureCommunication', ...
                'RequiresClientCertificate', ...
                'RequiresOnlineLicensing', ...
                'PreferredPoolNumWorkers'}];
        end
        
        function [ok, msg] = hCheckTwoWayCommunications( obj, forceCheck )
        % Simply defer to the support.
            try
                [ok, msg] = obj.Support.checkTwoWayCommunications( forceCheck );
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end

        function getClusterLogs( mjs, saveLocation )
        % Get the MJS logs from the job manager and save as a zip file
        % saveLocation.
            if nargin < 2
                saveLocation = ".";
            end
            filename = mjs.Name + "-logs.zip";
            filename = fullfile( saveLocation, filename );
            try
                mjs.Support.getClusterLogs( filename );
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        
        function [p, q, r, f] = hEnumerateJobsInStates(obj)
        % Get the number of pending, queued, running and finished jobs
            try
                [p, q, r, f] = obj.Support.enumerateJobsInStates();
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end

        function hErrorIfNumPoolWorkersInvalid( obj, numWorkers )
            clusterNumWorkers = obj.MaxNumWorkers;
            obj.Support.hErrorIfNumPoolWorkersInvalid(numWorkers, clusterNumWorkers);
        end
        
        function maxNumWorkers = hGetMaxNumWorkers( obj )
            % MJS clusters may be resizable and should include workers that
            % are not yet a part of the cluster, hence we must override
            % this method
            maxNumWorkers = obj.MaxNumWorkers;
        end
        
        function hAddRemoteInvocationListener( obj, listener )
            % Add listener for job manager remote invocation events
            obj.Support.hAddRemoteInvocationListener( listener );
        end
        
        function hRemoveRemoteInvocationListener( obj, listener )
            % Remove listener for job manager remote invocation events
            obj.Support.hRemoveRemoteInvocationListener( listener );
        end
        
        function provider = hCreateJobStatusProvider( obj )
        % hCreateJobStatusProvider - return an update provider for Job Monitor
        % to pull updates.
            provider = obj.Support.hCreateJobStatusProvider( obj );
        end
        
        function factory = hCreatePoolEndpointFactory( obj )
            factory = obj.Support.createEndpointFactory(class(obj));
        end

        function [isNetworkCluster, latency, dataSend, dataReceive] = hTestClusterConnection( obj )
            [isNetworkCluster, latency, dataSend, dataReceive] = parallel.cluster.MJS.hDoTestClusterConnection( obj );
        end

        function option = hGetMpiLibraryForConnectAccept(obj)
        %Ensure spmd-auto pools use the configured library.
            option = obj.MpiLibraryName;
        end
    end

    methods ( Static, Hidden )
        function [isNetworkCluster, latency, dataSend, dataReceive] = hDoTestClusterConnection( mjsCluster )
        % Cluster connection test for MJS-backed clusters
            isNetworkCluster = true;

            % Test the network latency by making a 'ping'-like call
            latency = timeit(@() mjsCluster.State);

            % Test the data send/receive by transferring 300KB of data with
            % an independent job
            job = mjsCluster.createJob();
            cleanupObj = onCleanup(@() delete(job));
            numBytes = 3E5;
            data = rand(numBytes / 8, 1);
            dataSend = numBytes / timeit(@() hSetPropertyNoCheck(job, 'JobData', data));
            dataReceive = numBytes / timeit(@() hGetProperty(job, 'JobData'));
        end
    end

    methods ( Access = protected )
        function job = buildIndependentJob( mjs )
            import parallel.internal.types.Variant

            % Check that we are logged in as required.
            mjs.ensureLicenseNumberIsSetOrError();
            try
                job = mjs.Support.buildJob( mjs, Variant.IndependentJob, mjs.LicenseEntitlement );
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        function job = buildCommunicatingJob( mjs, variant, ~ )
            % Check that we are logged in as required.
            mjs.ensureLicenseNumberIsSetOrError();
            try
                job = mjs.Support.buildJob( mjs, variant, mjs.LicenseEntitlement );
            catch E
                throw( distcomp.handleJavaException( mjs, E ) );
            end
        end
        function jl = getJobs( obj )
            try
                if parallel.internal.cluster.hasSameSupport( obj )
                    jl = obj(1).Support.getJobs( obj );
                else
                    jl = arrayfun( @(x) x.Support.getJobs( x ), ...
                            obj, 'UniformOutput', false );
                end
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end
        function varargout = getJobsByState( obj )
            try
                if parallel.internal.cluster.hasSameSupport( obj )
                    [varargout{1:nargout}] = obj(1).Support.getJobsByState( obj );
                else
                    [varargout{1:nargout}] = arrayfun( @(x) x.Support.getJobsByState( x ), ...
                                                obj, 'UniformOutput', false );
                end
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end
        function [connMgr, strategy] = buildConnectionManager( obj )
            try
                [connMgr, strategy] = obj.Support.buildConnectionManager();
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end
        function tf = isEquivalentWorkers( obj1, obj2 )
        % isEquivalentWorkers TRUE if Host and Name match.
            tf = isequal( obj1.Host, obj2.Host ) && ...
                 isequal( obj1.Name, obj2.Name );
        end

        function tf = isEquivalentStorage( obj1, obj2 )
        % isEquivalentStorage TRUE if Host and Name match.
            tf = isequal( obj1.Host, obj2.Host ) && ...
                 isequal( obj1.Name, obj2.Name );
        end

        function num = getNumConcurrentTasks( obj, job )
            try
                num = job.hGetProperty( 'NumLabs' );
                while num == 0 % jobmanager hasn't filled in the details
                    pause(0.5);
                    num = job.hGetProperty( 'NumLabs' );
                end
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end
        
        function onInvalidatingClusters( obj )
            % If the cluster is using online licensing at the
            % time of logout, invalidate the cluster object and all of its
            % children (jobs/tasks/workers)
            if obj.RequiresOnlineLicensing
                obj.Support.invalidateAllChildren();
                obj.delete();
            end
        end
        
        % Get all the properties needed for the ClusterMemento
        function props = getAllMementoProperties( obj, clusterMementoType )
            import parallel.internal.types.ClusterMementoType

            switch clusterMementoType
                case ClusterMementoType.Minimal
                    props = obj.AllPropertiesForMinimalMemento;
                case ClusterMementoType.Full
                    props = obj.AllPropertiesForFullMemento;
                otherwise
                    % Default to the full memento
                    props = obj.AllPropertiesForFullMemento;
            end
        end

        function props = getPublicPropertiesForComparison(obj)
            props = getPublicPropertiesForComparison@parallel.Cluster(obj);
            % Filter TLSConnectKey
            props = setdiff(props, 'TLSConnectKey');
        end
    end

    methods ( Access = private )
        function delete( obj )
            parallel.internal.mjs.TLSConnectInfo.removeFromCache(obj.TLSConnectKey);
            % Private for g787820
            delete@handle( obj );
        end
    end
end
%#ok<*ATUNK> custom attributes

% --------------------------------------------------------------------------
function v = iGetClusterProps( obj, names )
% Simple wrapper to include handleJavaException
    try
        v = obj.Support.getClusterProperties( names );
    catch E
        throwAsCaller( distcomp.handleJavaException( obj, E ) );
    end
end

% --------------------------------------------------------------------------
function iSetClusterProps( obj, names, values )
% Simple wrapper to include handleJavaException
    try
        obj.Support.setClusterProperties( names, values );
    catch E
        throwAsCaller( distcomp.handleJavaException( obj, E ) );
    end
end

% --------------------------------------------------------------------------
% iMaybeGetFromMapAndRemove - simply return the value from the propsFromArgs map; return
% the source - 'Factory' if not in the map.  Remove the property from
% the map if it is in there.
function [val, type] = iMaybeGetFromMapAndRemove( map, prop, val )
    if map.isKey( prop )
        valtmp = map(prop);
        val    = valtmp{1};
        type   = valtmp{2};
        map.remove(prop);
    else
        type = parallel.internal.settings.ValueType.Factory;
    end
end

% --------------------------------------------------------------------------
% iSecurityLevelInfo - return a string to be displayed for each security
% level.
function info = iSecurityLevelInfo( securityLevel )
    switch securityLevel
        case 0
            msg = message( 'parallel:cluster:mjs:SecurityLevelInfo0' );
        case 1
            msg = message( 'parallel:cluster:mjs:SecurityLevelInfo1' );
        case 2
            msg = message( 'parallel:cluster:mjs:SecurityLevelInfo2' );
        case 3
            msg = message( 'parallel:cluster:mjs:SecurityLevelInfo3' );
        otherwise
            assert( false, 'The MJS security level must be one of: 0, 1, 2, or 3' );
    end
    info = msg.getString();
end

% --------------------------------------------------------------------------
function [value, varargin] = iExtractNameValuePair( name, defaultValue, varargin )
    % Find the indices of all names (in the name-value pairs of varargin)
    % that match the given name
    matchingIndices = 2 * ( find( strcmp( name, varargin(1:2:end) ) ) - 1 ) + 1;
    
    % Extract all matching name-value pairs and return the right-most value
    % if one is present, otherwise return the default value
    if isempty( matchingIndices )
        value = defaultValue;
    else
        value = varargin{matchingIndices(end) + 1};
        varargin([matchingIndices, matchingIndices + 1]) = [];
    end
end
