Function SRP_EDITOR_TEMP_HTTP_RESOURCE_SERVICES_FRAMEWORKS(@Service, @Params)
/***********************************************************************************************************************

    This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
    permission from SRP Computer Solutions, Inc.

    Name        :   HTTP_Resource_Services

    Description :   Handler program for all module related services.

    Notes       :   The generic parameters should contain all the necessary information to process the services. Often
                    this will be information like the data Record and Key ID.

    Parameters  :
        Service         [in] -- Name of the service being requested
        Param1-10   [in/out] -- Additional request parameter holders
        Response       [out] -- Response to be sent back to the Controller (MCP) or requesting procedure

    Metadata    :

    History     :   (Date, Initials, Notes)
        04/19/15    dmb     [SRPFW-95] Original programmer.
        04/21/15    dmb     [SRPFW-95] Fix bug in Prepare_Column_Values gosub that ignored calculated columns. Thanks to
                            Barry Stevens for finding this.
        04/21/15    dmb     [SRPFW-95] Add PostDatabaseItem service.
        04/21/15    dmb     [SRPFW-95] Fix bug in Prepare_Column_Names gosub that ignored calculated columns.
        04/21/15    dmb     [SRPFW-95]Modify the GetDatabaseItems service to support multiple types of options in the
                            Filter (formerly Query) argument.
        05/04/15    dmb     [SRPFW-95] Convert Prepare_Column_Names gosub into the GetColumnNames service. Retrofit all
                            calls to the gosub to use the new service.
        05/04/15    dmb     [SRPFW-95] Convert Prepare_Column_Values gosub into the GetColumnValues service. Retrofit 
                            all calls to the gosub to use the new service.
        05/04/15    dmb     [SRPFW-95] Move the logic to exclude non-master columns and full Key ID columns into the
                            GetColumnNames service. The GetColumnValues service will no longer be doing any column name
                            filter. It will just work with the list provided. It will, however, still make sure the
                            column name exists before attempting to get a value for it.
        05/04/15    dmb     [SRPFW-95] Modify the GetColumnValues service to return full Key IDs if the column name
                            requires it. Add logic to decrement delimiters if @RM or @FM characters are found in the
                            value.
        05/04/15    dmb     [SRPFW-95] Modify the PostDatabaseItem service to handle SRP_JSON PARSE errors better and
                            return a 400 response status code.
        05/15/15    dmb     [SRPFW-95] Modify the GetDatabaseItems service to return an empty HAL+JSON embedded response
                            if there are no matches. Remove the 404 status since the URL itself is valid although the
                            database returns no matches.
        06/09/15    dmb     [SRPFW-95] Add ItemArrayLabel option for GetDatabaseItem and GetDatabaseItems services. This
                            allows the calling service to customize the name of the primary HAL item array.
        02/27/16    dmb     [SRPFW-112] Update various services to use Memory_Services for caching.
        03/23/16    dmb     [SRPFW-114] Comment out the logic that attempted to remove full Key ID values from the
                            response in the GetColumnValues services. Mark Boorman pointed out a bug in this logic, but
                            the reason for doing this was unnecessary.
        03/24/16    dmb     [SRPFW-115] Add GetMVGroupNames service.
        03/25/16    dmb     [SRPFW-119] Update all SRP_JSON GETVALUE services used in the PostDatabaseItem service to
                            use the Default argument to return an empty string if the objects value is null.
        04/11/16    dmb     [SRPFW-121] Update GetDatabaseItems service to use %RECORDS% if the Filter argument is
                            empty. This will provide a sorted list of Key IDs automatically.
        04/11/16    dmb     [SRPFW-121] Update PostDatabaseItems service to use %RECORDS% if %SK% does not exist. This
                            will make it easier to calculate the next available Key ID.
        07/26/16    dmb     [SRPFW-126] Refactor the GetColumnNames service and resolve a bug that Barry Steven's
                            discovered where non-master data type columns would be returned.
        10/13/16    dmb     [SRPFW-129] Update the PostDatabaseItem service to set the response status to 201 when the
                            ItemID is being passed in but it does not yet exist in the database.
        07/01/17    dmb     [SRPFW-184] Refactor using Enhanced BASIC+ syntax.
        07/05/17    dmb     Add PutDatabaseItem and PatchDatabaseItem services. Refactor so all DatabaseItem create and
                            update services share relevant common code.
        07/05/17    dmb     Update several variables and comments to better match database terminology rather than
                            HTTP terminology.
        07/19/18    dmb     Update the PostDatabaseItem, PutDatabaseItem, and PatchDatabaseItem services to support a
                            new argument, AllowedColumnNames, which will be used to filter out column names which are
                            submitted in the payload but should not be updated on the server.
        11/29/18    dmb     [SRPFW-259] Fix GetColumnNames to properly remove XREF columns.
        11/29/18    dmb     [SRPFW-259] Add GetObjects service.
        11/29/18    dmb     [SRPFW-259] Add GetSerializedResource service.
        11/29/18    dmb     [SRPFW-259] Add ParseResource service.
        11/29/18    dmb     [SRPFW-259] Add AddLinkRelationships service.
        11/29/18    dmb     [SRPFW-259] Add AddProperties service.
        11/30/18    dmb     [SRPFW-259] Add AddEmbeddedResources service.
        11/30/18    dmb     [SRPFW-259] Add AddNestedProperties service.
        12/01/18    dmb     [SRPFW-259] Update GetDatabaseItem to use the new resource services.
        12/01/18    dmb     [SRPFW-259] Update GetDatabaseItems to use the new resource services.
        11/30/18    dmb     [SRPFW-259] Add AddFormAction service.
        12/02/18    dmb     [SRPFW-259] Update GetColumnValues service so when a Key ID is missing it will set
                            Error_Services and set the response status to 404.
        12/04/18    dmb     [SRPFW-259] Add support for multiple nested objects in the AddNestedProperties service.
        12/06/18    dmb     [SRPFW-257] Add LoremIpsum service.
        12/12/18    dmb     [SRPFW-259] Update GetObjects so that specific Key ID parts can be identified to be used in
                            the self relationship URL and also add feature so the default '*' delimiter can be swapped
                            out for another character.
        01/03/19    dmb     [SRPFW-259] Fix bug AddNestedProperties when using Array formatting.
        02/14/19    dmb     [SRPFW-259] Add IsArray argument for the AddProperties service so data can be automatically
                            formatted as an array regardless if @VMs are found. This is meant to maintain formatting
                            integrity of the property regardless of 0, 1, or multiple values.
        03/04/19    dmb     [SRPFW-259] Add GetObject service. Refactored GetObjects service to use GetObject service.
        03/04/19    dmb     [SRPFW-259] Add AddProperty service. Update AddProperties service to call AddProperty for
                            each property.
        03/04/19    dmb     [SRPFW-259] Add AddSubProperty and AddSubProperties service.
        03/04/19    dmb     [SRPFW-259] Add AddSubResource and AddSubResources service.
        03/05/19    dmb     [SRPFW-259] Add AddSubResourceObject and AddSubResourceObjects service.
        03/05/19    dmb     [SRPFW-259] Remove AddNestedProperties service. Refit other services to use AddSubResources.
        03/05/19    dmb     [SRPFW-259] Add AddLinkRelationship service. Update AddLinkRelationships service to call
                            AddLinkRelationship for each relationship.
        03/05/19    dmb     [SRPFW-259] Update all resource related services to use new services and remove direct calls
                            to SRP_JSON except for the lowest level services.
        03/05/19    dmb     [SRPFW-259] Add AllowKeyProperty argument to the GetObject, GetObjects, and GetColumnNames
                            services to override the default behavior of removing full Key ID columns from being
                            represented in the object. This is useful for sub-resources.
        03/05/19    dmb     [SRPFW-259] Fix bug in GetObject service where associated property names were not being
                            removed if the column names were removed by the GetColumnNames service.
        03/06/19    dmb     [SRPFW-259] Update the GetDatabaseItem and GetDatabaseItems service so the SelfURL argument
                            is now optional.
        04/10/19    dmb     [SRPFW-271] Replace the Content-Location response header with Location in UpdateDatabaseRow.
                            This is the correct header for 201 responses.
                            (cf. https://tools.ietf.org/html/rfc2616#section-10.2.2)
        05/28/19    dmb     [SRPFW-274] Update the AddEmbeddedResources service to support a new Singular argument. This
                            provides support for an embedded resource that is unique and should be represented as
                            sub-properties rather than sub-resources.
        05/28/19    dmb     [SRPFW-274] Update the AddSubProperty service to correctly handle the SubPropertyValue
                            argument if it contains an object handle.
        05/28/19    dmb     [SRPFW-274] Rename the GetLinkRelationship service to GetLinkRelation. Rename the
                            GetLinkRelationships service to GetLinkRelations.
        05/31/19    dmb     [SRPFW-276] Update the GetColumnValues service so Error_Services is called after the
                            SetResponseError call. This prevents Error_Services from being cleared prematurely.
        05/31/19    dmb     [SRPFW-276] Update the GetColumnNames service so Error_Services is called after the
                            SetResponseError call. This prevents Error_Services from being cleared prematurely.
        05/31/19    dmb     [SRPFW-276] Update the GetMVGroupNames service so Error_Services is called after the
                            SetResponseError call. This prevents Error_Services from being cleared prematurely.
        12/09/19    dmb     [SRPFW-296] Update all calls to Memory_Services to use a specific cache name.
        12/16/19    dmb     [SRPFW-296] Update the AddEmbeddedResources so it can be called more than once for the same
                            embedded resource name. New resources will be added rather than replace what is already
                            present.
        01/22/20    dmb     [SRPFW-296] Update the AddLinkRelation and AddLinkRelations services to support IsTemplate
                            (IsTemplates) Boolean argument. This is to add support for URI Templates:
                            https://tools.ietf.org/html/rfc6570
                            https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.2
        01/26/20    dmb     [SRPFW-296] Add GetURLTemplate service.
        01/26/20    dmb     [SRPFW-296] Update the AddLinkRelation service to call the GetURLTemplate service to create
                            a URL Template out of an indicated expanded URL.

***********************************************************************************************************************/

#pragma precomp SRP_PreCompiler

$insert LOGICAL
$insert HTTP_INSERTS
$insert SERVICE_SETUP
$insert INET_EQUATES
$insert INET_HEADERS
$insert DICT_EQUATES

Equ CRLF$       to \0D0A\
Equ CacheName$  to 'SRPHTTPFramework'

Declare function    HTTP_Resource_Services, HTTP_Resource_Manager_Services, Database_Services, Memory_Services
Declare function    SRP_Array
Declare subroutine  HTTP_Resource_Services, RList, Make.List, Activate_Save_Select, Memory_Services

GoToService else
    HTTP_Services('SetResponseError', '', '', 404, Service : ' is not a valid service request within the HTTP Resource services module.')
end

Return Response OR ''


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Service Parameter Options
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Options BOOLEAN         = True$, False$
Options LAYOUT          = 'Array', 'List'
Options METHODS         = 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'
Options STYLES          = 'Fast', 'Styled', 'DropNulls'
Options ALLDATATYPES    = 'String', 'Number', 'Boolean', 'StringList', 'NumberList', 'BooleanList', 'ObjectHandle'


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Services
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//----------------------------------------------------------------------------------------------------------------------
// GetObject
//
// TableName            - Name of the database table where the resource (database row) is stored. - [Optional]
// KeyID                - Key ID of a database row that should be used to build the resource. - [Optional]
// ColumnNames          - A delimited list of column names to build into the resource. Default will be all legitimate
//                        columns which are returned by the GetColumnNames service. - [Optional]
// PropertyNames        - A delimited list of property names to use for each column. Default will be the column name.
//                        - [Optional]
// DataTypes            - A delimited list of JSON data types (e.g., string, number, Boolean, etc.). Default will be
//                        string. - [Optional]
// MVGroupNames         - A delimited list of MV group names that are to be matched with the delimited list of nested
//                        group names. - [Optional]
// NestedGroupNames     - A delimited list of nested group names to use for each MV group name. This is only used if
//                        a list of MV group names is also provided. - [Optional]
// BaseSelfRelURL       - The base URL to use for creating a self link relationship. If empty then no self link
//                        relationship will be created. Otherwise, the Key ID will be appended as a segment to this
//                        URL and added using the AddLinkRelations service. - [Optional]
// KeyParts             - A delimited list of Key ID parts that should be used to created the self link relationship.
//                        Default is all Key ID parts will be used. - [Optional]
// KeyDelimiter         - The character to use as the delimiter for multi-part Key IDs when used in the self link
//                        relationship. Default is '*', although this is not a URL friendly character. - [Optional]
// AllowKeyProperty     - A Boolean flag indicating that the resource object be allowed to represent the Key ID of the
//                        database row to exists. This ultimately informs the GetColumnNames service to avoid stripping
//                        out the Key ID column. - [Optional]
//
// Creates one or more new resource objects. The database table, Key ID, and list of column mames are provided, these
// will be used to create properties within the resource object.
//----------------------------------------------------------------------------------------------------------------------
Service GetObject(TableName, KeyID, ColumnNames, PropertyNames, DataTypes, MVGroupNames, NestedGroupNames, BaseSelfRelURL, KeyParts, KeyDelimiter, AllowKeyProperty)

    ObjectHandle    = ''

    If (TableName NE '') AND (KeyID NE '') then
        FieldArray  = Database_Services('ReadDataRow', 'DICT.' : TableName, '%FIELDS%', False$, 60)
        Convert @Lower_Case to @Upper_Case in ColumnNames
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in ColumnNames
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in PropertyNames
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in DataTypes
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in MVGroupNames
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in NestedGroupNames
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in KeyParts
        If KeyDelimiter EQ '' then KeyDelimiter = '*'
        MVColumnGroupNames  = ''
        CompareColumnNames  = ColumnNames   ; // Save the original column names for comparison after they've been vetted.
        ColumnNames         = HTTP_Resource_Services('GetColumnNames', TableName, ColumnNames, AllowKeyProperty)
        // Check each original column name against the vetted list. If it is missing, remove the associated property name
        // as well.
        NumColumnNames      = DCount(CompareColumnNames, @FM)
        For ColumnPos = NumColumnNames to 1 Step -1
            ColumnName  = CompareColumnNames<ColumnPos>
            Locate ColumnName in ColumnNames using @FM setting fPos else
                PropertyNames   = Delete(PropertyNames, ColumnPos, 0, 0)
            end
        Next ColumnPos
        hTable              = Database_Services('GetTableHandle', TableName)
        If Error_Services('NoError') then
            If SRP_JSON(ObjectHandle, 'New', 'Object') then
                ColumnValues    = HTTP_Resource_Services('GetColumnValues', TableName, ColumnNames, KeyID)
                If Error_Services('NoError') then
                    Convert @VM : @SVM : @TM : @STM to @SVM : @TM : @STM : Char(249) in ColumnValues
                    Convert @Lower_Case to @Upper_Case in ColumnNames
                    For Each ColumnName in ColumnNames using @FM setting fPos
                        PropertyName    = PropertyNames<fPos>
                        If PropertyName EQ '' then PropertyNames<fPos> = Oconv(ColumnName, 'MCL')
                        DataType        = DataTypes<fPos>
                        If DataType EQ '' then DataTypes<fPos> = 'String'
                        Locate ColumnName in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                            MVGroupName         = FieldArray<FIELDS_MV_GROUPNAME$, vPos>
                            If MVGroupName NE '' then
                                Locate MVGroupName in MVGroupNames using @FM setting Pos then
                                    NestedGroupName             = NestedGroupNames<Pos>
                                    If NestedGroupName EQ '' then NestedGroupName = Oconv(MVGroupName, 'MCL')
                                    MVColumnGroupNames<fPos>    = NestedGroupName
                                end else
                                    MVColumnGroupNames<fPos>    = Oconv(MVGroupName, 'MCL')
                                end
                            end
                        end
                    Next ColumnName
                    ColumnArray = ColumnNames : @RM : PropertyNames : @RM : DataTypes : @RM : MVColumnGroupNames : @RM : ColumnValues
                    ColumnArray = SRP_Array('Rotate', ColumnArray, @RM, @FM)
                    Convert @FM to @VM in ColumnArray
                    Convert @RM to @FM in ColumnArray
                    ColumnArray = SRP_Array('SortRows', ColumnArray, 'AL4', 'LIST')
                    NestedValueArray        = ''
                    NestedPropertyNames     = ''
                    PrevMVColumnGroupName   = ''
                    For Each ColumnInfo in ColumnArray using @FM setting fPos
                        ColumnName          = ColumnInfo[1, @VM]
                        PropertyName        = ColumnInfo[Col2() + 1, @VM]
                        DataType            = ColumnInfo[Col2() + 1, @VM]
                        MVColumnGroupName   = ColumnInfo[Col2() + 1, @VM]
                        Value               = ColumnInfo[Col2() + 1, @VM]
                        Convert @SVM : @TM : @STM : Char(249) to @VM : @SVM : @TM : @STM in Value
                        If MVColumnGroupName NE '' then
                            If (MVColumnGroupName NE PrevMVColumnGroupName) AND (PrevMVColumnGroupName NE '') then
                                NestedPropertyNames[-1, 1]  = ''
                                NestedValueArray[-1, 1]     = ''
                                NestedValueArray    = SRP_Array('Rotate', NestedValueArray, @FM, @VM)
                                Convert @FM to @RM in NestedValueArray
                                Convert @VM to @FM in NestedValueArray
                                HTTP_Resource_Services('AddSubResources', ObjectHandle, PrevMVColumnGroupName, NestedPropertyNames, NestedValueArray)
                                NestedPropertyNames     = ''
                                NestedValueArray        = ''
                            end
                            NestedPropertyNames    := PropertyName : @FM
                            NestedValueArray       := Value : @FM
                            PrevMVColumnGroupName   = MVColumnGroupName
                        end else
                            HTTP_Resource_Services('AddProperty', ObjectHandle, PropertyName, Value, DataType)
                        end
                    Next ColumnInfo
                    If PrevMVColumnGroupName NE '' then
                        NestedPropertyNames[-1, 1]  = ''
                        NestedValueArray[-1, 1]     = ''
                        NestedValueArray    = SRP_Array('Rotate', NestedValueArray, @FM, @VM)
                        Convert @FM to @RM in NestedValueArray
                        Convert @VM to @FM in NestedValueArray
                        HTTP_Resource_Services('AddSubResources', ObjectHandle, PrevMVColumnGroupName, NestedPropertyNames, NestedValueArray)
                    end
                    If BaseSelfRelURL NE '' then
                        If KeyParts NE '' then
                            NewKeyID    = ''
                            For Each KeyPart in KeyParts using @FM
                                NewKeyID   := Field(KeyID, '*', KeyPart, 1) : KeyDelimiter
                            Next KeyPart
                            NewKeyID[-1, 1] = ''
                            Transfer NewKeyID to KeyID
                        end else
                            Convert '*' to KeyDelimiter in KeyID
                        end
                        SelfRelURL  = BaseSelfRelURL : '/' : KeyID
                        HTTP_Resource_Services('AddLinkRelations', ObjectHandle, 'self', SelfRelURL)
                    end
                end
            end else
                Error_Services('Add', 'Error creating resource object in the ' : Service : ' service.')
            end
        end
    end else
        If SRP_JSON(ObjectHandle, 'New', 'Object') else
            Error_Services('Add', 'Error creating resource object in the ' : Service : ' service.')
        end
    end

    Response        = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// GetObjects
//
// TableName            - Name of the database table where the resource (database row) is stored. - [Optional]
// Filter               - Information that informs the service of how the database should be filtered. There are four
//                        options: 1. Empty string, which is the default, meaning all database rows should be selected,
//                        2. A "SELECT" query that will be used in an RList() statement, 3. The name of a saved list of
//                        Key IDs, or 4. An @FM delimited list of Key IDs. - [Optional]
// ColumnNames          - A delimited list of column names to build into the resource. Default will be all legitimate
//                        columns which are returned by the GetColumnNames service. - [Optional]
// PropertyNames        - A delimited list of property names to use for each column. Default will be the column name.
//                        - [Optional]
// DataTypes            - A delimited list of JSON data types (e.g., string, number, Boolean, etc.). Default will be
//                        string. - [Optional]
// MVGroupNames         - A delimited list of MV group names that are to be matched with the delimited list of nested
//                        group names. - [Optional]
// NestedGroupNames     - A delimited list of nested group names to use for each MV group name. This is only used if
//                        a list of MV group names is also provided. - [Optional]
// BaseSelfRelURL       - The base URL to use for creating a self link relationship. If empty then no self link
//                        relationship will be created. Otherwise, the Key ID will be appended as a segment to this
//                        URL and added using the AddLinkRelations service. - [Optional]
// KeyParts             - A delimited list of Key ID parts that should be used to created the self link relationship.
//                        Default is all Key ID parts will be used. - [Optional]
// KeyDelimiter         - The character to use as the delimiter for multi-park Key IDs when used in the self link
//                        relationship. Default is '*', although this is not a URL friendly character. - [Optional]
// AllowKeyProperty     - A Boolean flag indicating that the resource object be allowed to represent the Key ID of the
//                        database row to exists. This ultimately informs the GetColumnNames service to avoid stripping
//                        out the Key ID column. - [Optional]
//
// Creates one or more new resource objects. The database table, Key ID, and list of column mames are provided, these
// will be used to create properties within the resource object.
//----------------------------------------------------------------------------------------------------------------------
Service GetObjects(TableName, Filter, ColumnNames, PropertyNames, DataTypes, MVGroupNames, NestedGroupNames, BaseSelfRelURL, KeyParts, KeyDelimiter, AllowKeyProperty)

    ObjectHandles   = ''

    If TableName NE '' then
        hTable  = Database_Services('GetTableHandle', TableName)
        If Error_Services('NoError') then
            Begin Case
                Case Filter EQ ''
                    Filter  = Xlate(TableName, '%RECORDS%', '', 'X')
                    If Len(Filter) then
                        rv = Set_Status(0)
                        Make.List(0, Filter, '', '')
                    end else
                        Filter = 'SELECT ' : TableName : ' BY @ID'
                        rv = Set_Status(0)
                        RList(Filter, 5, '', '', '')
                    end

                Case Filter[1, 7] EQ 'SELECT '
                    rv = Set_Status(0)
                    RList(Filter, 5, '', '', '')

                Case Count(Filter, @FM)
                    rv = Set_Status(0)
                    Make.List(0, Filter, '', '')

                Case Otherwise$
                    // First, test to see if this is a valid saved list. If not, assume it is a single Key ID.
                    If Num(Filter) then
                        SaveList = Quote(Filter)
                    end else
                        SaveList = Filter
                    end
                    Activate_Save_Select(SaveList)
                    If Get_Status() then
                        rv = Set_Status(0)
                        Make.List(0, Filter, '', '')
                    end
            End Case
            EOF = False$
            Loop
                Readnext KeyID else EOF = True$
            Until EOF EQ True$
                ObjectHandle    = HTTP_Resource_Services('GetObject', TableName, KeyID, ColumnNames, PropertyNames, DataTypes, MVGroupNames, NestedGroupNames, BaseSelfRelURL, KeyParts, KeyDelimiter, AllowKeyProperty)
                If Error_Services('NoError') then
                    ObjectHandles  := ObjectHandle : @FM
                end else
                    Error_Services('Add', 'Error creating resource object in the ' : Service : ' service.')
                end
            While Error_Services('NoError')
            Repeat
            ObjectHandles[-1, 1]    = ''
        end
    end else
        ObjectHandles   = HTTP_Resource_Services('GetObject')
    end

    Response        = ObjectHandles

end service


//----------------------------------------------------------------------------------------------------------------------
// ParseResource
//
// SerializedResource   - Name of the database table where the resource (database row) is stored. - [Optional]
//
// Creates a new resource object. If a database table, Key ID, and list of column mames are provided, these will be used
// to create properties within the resource object.
//----------------------------------------------------------------------------------------------------------------------
Service ParseResource(SerializedResource)

    ObjectHandle    = ''

    If SerializedResource NE '' then
        If SRP_JSON(ObjectHandle, 'Parse', SerializedResource) EQ '' else
            Error_Services('Add', 'Error parsing the serialized resource in the ' : Service : ' service.')
        end
    end else
        Error_Services('Add', 'SerializedResource argument was missing from the ' : Service : ' service.')
    end

    Response        = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddProperty
//
// ObjectHandle     - Object handle to the resource. If empty, a new one will be created using the GetObject
//                    service. - [Optional]
// PropertyName     - The property name being added. - [Required]
// PropertyValue    - The property value being added. Default will be an empty string. - [Optional]
// DataType         - The data type for the property value. These can be the standard JSON data types (e.g., 'String',
//                    'Number', or 'Boolean'). If the PropertyValue contains @VM delimiters, these will be formatted as
//                    a JSON Array. There are a few special data types as well:
//                    - 'StringList'    - Same as 'String', but will force the values to be stored in a JSON Array even
//                                        if there are no @VM delimiters in the value.
//                    - 'NumberList'    - Same as 'Number', but will force the values to be stored in a JSON Array even
//                                        if there are no @VM delimiters in the value.
//                    - 'BooleanList'   - Same as 'Boolean', but will force the values to be stored in a JSON Array even
//                                        if there are no @VM delimiters in the value.
//                    - 'ObjectHandle'  - A JSON object handle. This is expected to be a sub-property or a sub-resource
//                                        object. This object handle will be released automatically.
//                    If DataType is empty, the default will be 'String'. - [Optional]
//
// Adds a property to the indicated resource object along with any value provided.
//----------------------------------------------------------------------------------------------------------------------
Service AddProperty(ObjectHandle, PropertyName, PropertyValue, DataType=ALLDATATYPES)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (PropertyName NE '') then
        If DataType EQ '' then DataType = 'String'
        If DataType _EQC 'ObjectHandle' then
            If Num(PropertyValue) then
                Transfer PropertyValue to objProperty
                SRP_JSON(ObjectHandle, 'Set', PropertyName, objProperty)
                SRP_JSON(objProperty, 'Release')
            end else
                Error_Services('Add', 'The PropertyValue argument from the ' : Service : ' service does not contain a valid object handle.')
            end
        end else
            IsList  = (Count(PropertyValue, @VM) GE 1) OR (DataType[-4, 4] _EQC 'List')
            If IsList EQ True$ then
                If SRP_JSON(arrayItem, 'New', 'Array') then
                    For Each ItemValue in PropertyValue using @VM
                        SRP_JSON(arrayItem, 'AddValue', ItemValue, DataType)
                    Next ItemValue
                    SRP_JSON(ObjectHandle, 'Set', PropertyName, arrayItem)
                    SRP_JSON(arrayItem, 'Release')
                end
            end else
                SRP_JSON(ObjectHandle, 'SetValue', PropertyName, PropertyValue, DataType)
            end
        end
    end else
        Error_Services('Add', 'ObjectHandle or PropertyName argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddProperties
//
// ObjectHandle     - Object handle to the resource. If empty, a new one will be created using the GetObject
//                    service. - [Optional]
// PropertyNames    - An @FM delimited list of one or more property names. - [Required]
// PropertyValues   - An @FM delimited list of property values being added. Default will be an empty string.
//                    - [Optional]
// DataTypes        - An @FM delimited list of data types for the property values. See the DataType description for the
//                    AddProperty service. - [Optional]
//
// Adds one or more properties in the indicated resource object. This is a wrapper for the AddProperty service to make
// it easier to add multple properties to a resource object in one call.
//----------------------------------------------------------------------------------------------------------------------
Service AddProperties(ObjectHandle, PropertyNames, PropertyValues, DataTypes)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (PropertyNames NE '') then
        For Each PropertyName in PropertyNames using @FM setting fPos
            PropertyValue   = PropertyValues<fPos>
            DataType        = DataTypes<fPos>
            HTTP_Resource_Services('AddProperty', ObjectHandle, PropertyName, PropertyValue, DataType)
        Next PropertyName
    end else
        Error_Services('Add', 'ObjectHandle or PropertyNames argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddSubProperty
//
// ObjectHandle     - Object handle to the resource. If empty, a new one will be created using the GetObject
//                    service. - [Optional]
// PropertyName     - The property name being added or updated. - [Required]
// SubPropertyName  - The sub-property name being added. - [Required]
// SubPropertyValue - The sub-property value being added. Default will be an empty string. - [Optional]
// DataType         - The data type for the sub-property value. See the DataType description for the AddProperty
//                    service. - [Optional]
//
// Adds a sub-property to the indicated property of the indicated resource object along with any value provided. This is
// a one-service alternative to creating a temporary resource object and property (via GetObject with AddProperty) and
// then adding this object to the primary resource as a property (via AddProperty using 'ObjectHandle' data type).
//----------------------------------------------------------------------------------------------------------------------
Service AddSubProperty(ObjectHandle, PropertyName, SubPropertyName, SubPropertyValue, DataType=ALLDATATYPES)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')
 
    If (ObjectHandle NE '') AND (PropertyName NE '') then
        If DataType EQ '' then DataType = 'String'
        PropertyObjectHandle    = SRP_JSON(ObjectHandle, 'Get', PropertyName)
        If PropertyObjectHandle EQ 0 then PropertyObjectHandle = '' ; // A 0 means the property doesn't exist in the primary resource. Make this empty so it will get created.
        PropertyObjectHandle    = HTTP_Resource_Services('AddProperty', PropertyObjectHandle, SubPropertyName, SubPropertyValue, DataType)
        If Error_Services('NoError') then
            HTTP_Resource_Services('AddProperty', ObjectHandle, PropertyName, PropertyObjectHandle, 'ObjectHandle')
        end
    end else
        Error_Services('Add', 'ObjectHandle, PropertyName, or SubPropertyName argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddSubProperties
//
// ObjectHandle     - Object handle to the resource. If empty, a new one will be created using the GetObject
//                    service. - [Optional]
// PropertyName     - The property name being added or updated. - [Required]
// SubPropertyNames    - An @FM delimited list of one or more sub-property names. - [Required]
// SubPropertyValues   - An @FM delimited list of sub-property values being added. Default will be an empty string.
//                    - [Optional]
// DataTypes        - An @FM delimited list of data types for the sub-property values. See the DataType description for
//                    the AddProperty service. - [Optional]
//
// Adds one or more sub-properties to the indicated property of the indicated resource object. This is a wrapper for the
// AddSubProperty service to make it easier to add multple sub-properties to a property in one call.
//----------------------------------------------------------------------------------------------------------------------
Service AddSubProperties(ObjectHandle, PropertyName, SubPropertyNames, SubPropertyValues, DataTypes)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (SubPropertyNames NE '') then
        For Each SubPropertyName in SubPropertyNames using @FM setting fPos
            SubPropertyValue    = SubPropertyValues<fPos>
            DataType            = DataTypes<fPos>
            HTTP_Resource_Services('AddSubProperty', ObjectHandle, PropertyName, SubPropertyName, SubPropertyValue, DataType)
        Next PropertyName
    end else
        Error_Services('Add', 'ObjectHandle, PropertyName, or SubPropertyNames argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddSubResourceObject
//
// ObjectHandle                 - Object handle to the resource. If empty, a new one will be created using the GetObject
//                                service. - [Optional]
// PropertyName                 - The property name being added or updated. - [Required]
// SubResourceObjectHandle      - The object handle to a sub-resource being added. - [Required]
//
// Adds a sub-resource object handle to the indicated property of the indicated resource object. Any pre-existing
// sub-resources will be preserved and new ones will be appended.
//----------------------------------------------------------------------------------------------------------------------
Service AddSubResourceObject(ObjectHandle, PropertyName, SubResourceObjectHandle)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (PropertyName NE '') AND (SubResourceObjectHandle NE '') then
        PropertyArrayHandle = SRP_JSON(ObjectHandle, 'Get', PropertyName)
        If PropertyArrayHandle EQ 0 then SRP_JSON(PropertyArrayHandle, 'New', 'Array')
        If PropertyArrayHandle GT 0 then
            SRP_JSON(PropertyArrayHandle, 'Add', SubResourceObjectHandle)
            SRP_JSON(SubResourceObjectHandle, 'Release')
            SRP_JSON(ObjectHandle, 'Set', PropertyName, PropertyArrayHandle)
            SRP_JSON(PropertyArrayHandle, 'Release')
        end else
            Error_Services('Add', 'Error creating property array handle in the ' : Service : ' service.')
        end
    end else
        Error_Services('Add', 'ObjectHandle, PropertyName, or SubResourceObjectHandle argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddSubResourceObjects
//
// ObjectHandle                 - Object handle to the resource. If empty, a new one will be created using the GetObject
//                                service. - [Optional]
// PropertyName                 - The property name being added or updated. - [Required]
// SubResourceObjectHandles     - An @FM delimited list of sub-resource object handles being added. - [Required]
//
// Adds one or more sub-resource object handles to the indicated property of the indicated resource object. This is a
// wrapper for the AddSubResourceObject service to make it easier to add multple sub-resource object handles to a
// property in one call.
//----------------------------------------------------------------------------------------------------------------------
Service AddSubResourceObjects(ObjectHandle, PropertyName, SubResourceObjectHandles)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (PropertyName NE '') AND (SubResourceObjectHandles NE '') then
        For Each SubResourceObjectHandle in SubResourceObjectHandles using @FM
            HTTP_Resource_Services('AddSubResourceObject', ObjectHandle, PropertyName, SubResourceObjectHandle)
        Next SubResourceObjectHandle
    end else
        Error_Services('Add', 'ObjectHandle, PropertyName, or SubResourceObjectHandles argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddSubResource
//
// ObjectHandle                 - Object handle to the resource. If empty, a new one will be created using the GetObject
//                                service. - [Optional]
// PropertyName                 - The property name being added or updated. - [Required]
// SubResourcePropertyNames     - An @FM delimited list of one or more sub-resource property names being added.
//                                - [Required]
// SubResourcePropertyValues    - An @FM delimited list of one or more sub-resource property values being added. Default
//                                will be an empty string. - [Optional]
// DataTypes                    - An @FM delimited list of data types for the sub-resource property values. See the
//                                DataType description for the AddProperty service. - [Optional]
//
// Adds a sub-resource to the indicated property of the indicated resource object. A sub-resource object handle is
// created using the sub-resource property names and values and then added. Any pre-existing sub-resources will be
// preserved and new ones will be appended.
//----------------------------------------------------------------------------------------------------------------------
Service AddSubResource(ObjectHandle, PropertyName, SubResourcePropertyNames, SubResourcePropertyValues, DataTypes)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (PropertyName NE '') AND (SubResourcePropertyNames NE '') then
        PropertyArrayHandle = SRP_JSON(ObjectHandle, 'Get', PropertyName)
        If PropertyArrayHandle EQ 0 then SRP_JSON(PropertyArrayHandle, 'New', 'Array')
        If PropertyArrayHandle GT 0 then
            SubResourceObjectHandle = HTTP_Resource_Services('GetObject')
            If SubResourceObjectHandle GT 0 then
                For Each SubResourcePropertyName in SubResourcePropertyNames using @FM setting fPos
                    SubResourcePropertyValue    = SubResourcePropertyValues<fPos>
                    DataType                    = DataTypes<fPos>
                    HTTP_Resource_Services('AddProperty', SubResourceObjectHandle, SubResourcePropertyName, SubResourcePropertyValue, DataType)
                Next PropertyName
                HTTP_Resource_Services('AddSubResourceObject', ObjectHandle, PropertyName, SubResourceObjectHandle)
            end else
                Error_Services('Add', 'Error creating sub-resource object handle in the ' : Service : ' service.')
            end
            SRP_JSON(PropertyArrayHandle, 'Release')
        end else
            Error_Services('Add', 'Error creating property array handle in the ' : Service : ' service.')
        end
    end else
        Error_Services('Add', 'ObjectHandle, PropertyName, or SubResourcePropertyNames argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddSubResources
//
// ObjectHandle                     - Object handle to the resource. If empty, a new one will be created using the
//                                    GetObject service. - [Optional]
// PropertyName                     - The property name being added or updated. - [Required]
// SubResourcePropertyNames         - An @FM delimited list of one or more sub-resource property names being added.
//                                    - [Required]
// SubResourcePropertyValuesArray   - An @RM/@FM delimited array of one or more sub-resource property values being
//                                    added. Each @FM list will be treated as a sub-resource. Default will be an empty
//                                    string. - [Optional]
// DataTypes                        - An @FM delimited list of data types for the sub-resource property values. See the
//                                    DataType description for the AddProperty service. - [Optional]
//
// Adds one or more sub-resources to the indicated property of the indicated resource object. This is a wrapper for the
// AddSubResource service to make it easier to add multple sub-resources to a property in one call.
//----------------------------------------------------------------------------------------------------------------------
Service AddSubResources(ObjectHandle, PropertyName, SubResourcePropertyNames, SubResourcePropertyValuesArray, DataTypes)

    If ObjectHandle EQ '' then ObjectHandle = HTTP_Resource_Services('GetObject')

    If (ObjectHandle NE '') AND (PropertyName NE '') AND (SubResourcePropertyNames NE '') then
        For Each SubResourcePropertyValues in SubResourcePropertyValuesArray using @RM
            HTTP_Resource_Services('AddSubResource', ObjectHandle, PropertyName, SubResourcePropertyNames, SubResourcePropertyValues, DataTypes)
        Next SubResourcePropertyValues
    end else
        Error_Services('Add', 'ObjectHandle, PropertyName, or SubResourcePropertyNames argument was missing from the ' : Service : ' service.')
    end

    Response    = ObjectHandle

end service


//----------------------------------------------------------------------------------------------------------------------
// AddLinkRelation
//
// ObjectHandle         - Object handle to the resource. - [Required]
// Relation             - The relation being added. - [Required]
// URL                  - The URL associated to the indicated relation. - [Required]
// Title                - The friendly title for the indicated relation. - [Optional]
// IsTemplate           - Boolean flag to indicated if the link is URI Templated. - [Optional]
// IgnoreResourceID     - Boolean flag to determine if a resource ID segment should be left untemplated. - [Optional]
// IgnoreProperty       - Boolean flag to determine if a property segment should be left untemplated. - [Optional]
// IgnoreQueryParam     - Boolean flag to determine if existing query params should be left untemplated. - [Optional]
//
// Adds a _links relation to the indicated resource object. This is part of the HAL specification.
//----------------------------------------------------------------------------------------------------------------------
Service AddLinkRelation(ObjectHandle, Relation, URL, Title, IsTemplate=BOOLEAN, IgnoreResourceID=BOOLEAN, IgnoreProperty=BOOLEAN, IgnoreQueryParam=BOOLEAN)

    If (ObjectHandle NE '') AND (Relation NE '') AND (URL NE '') then
        objRel  = HTTP_Resource_Services('GetObject')
        If Error_Services('NoError') then
            If IsTemplate EQ True$ then
                URL = HTTP_Resource_Services('GetURLTemplate', URL, IgnoreResourceID, IgnoreProperty, IgnoreQueryParam)
                If Index(URL, '{', 1) then
                    // The GetURLTemplate service was able to convert the URL to a URL Template.
                    HTTP_Resource_Services('AddProperty', objRel, 'templated', True$, 'Boolean')
                end
            end
            HTTP_Resource_Services('AddProperty', objRel, 'href', URL)
            If Title NE '' then
                HTTP_Resource_Services('AddProperty', objRel, 'title', Title)
            end
            HTTP_Resource_Services('AddSubProperty', ObjectHandle, '_links', Relation, objRel, 'ObjectHandle')
        end
    end else
        Error_Services('Add', 'ObjectHandle, Relation, or URL argument was missing from the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// AddLinkRelations
//
// ObjectHandle         - Object handle to the resource. - [Required]
// Relations            - A delimited list of relations being added. - [Required]
// URLs                 - A delimited list of URLs for each indicated relation. - [Required]
// Titles               - A delimited list of display friendly titles for each indicated relation. - [Optional]
// IsTemplates          - A delimited list of Boolean flags to indicated if the link is URI Templated. - [Optional]
// IgnoreResourceIDs    - Boolean flag to determine if a resource ID segment should be left untemplated. - [Optional]
// IgnoreProperties     - Boolean flag to determine if a property segment should be left untemplated. - [Optional]
// IgnoreQueryParams    - Boolean flag to determine if existing query params should be left untemplated. - [Optional]
//
// Adds one or more _links relations in the indicated resource object. This is part of the HAL specification.
//----------------------------------------------------------------------------------------------------------------------
Service AddLinkRelations(ObjectHandle, Relations, URLs, Titles, IsTemplates, IgnoreResourceIDs=BOOLEAN, IgnoreProperties=BOOLEAN, IgnoreQueryParams=BOOLEAN)

    If (ObjectHandle NE '') AND (Relations NE '') AND (URLs NE '') then
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in Relations
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in URLs
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in Titles
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in IsTemplates
        For Each Relation in Relations using @FM setting fPos
            URL                 = URLs<fPos>
            Title               = Titles<fPos>
            IsTemplate          = IsTemplates<fPos>
            IgnoreResourceID    = IgnoreResourceIDs<fPos>
            IgnoreProperty      = IgnoreProperties<fPos>
            IgnoreQueryParam    = IgnoreQueryParams<fPos>
            HTTP_Resource_Services('AddLinkRelation', ObjectHandle, Relation, URL, Title, IsTemplate, IgnoreResourceID, IgnoreProperty, IgnoreQueryParam)
        Next Name
    end else
        Error_Services('Add', 'ObjectHandle, Relations, or URLs argument was missing from the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// AddEmbeddedResources
//
// ObjectHandle                     - Object handle to the resource. - [Required]
// EmbeddedResourceName             - Name of the embedded resource. - [Required]
// EmbeddedResourceObjectHandles    - An @FM delimited list of resource object handles being added. Each embedded
//                                    resource object handle will be released automatically. - [Required]
// Singular                         - A Boolean flag indicating that the embedded resource object should be represented
//                                    as singular (i.e., unique) rather than as multiple. A singular embedded resource
//                                    object will be displayed using sub-properties but multiple embedded resources
//                                    (even if there is just one) will be displayed using sub-resources. The default
//                                    value is False. - [Optional]
//
// Adds one or more embedded resources in the indicated resource object. Embedded resources are stand-alone resources
// on their own (and usually have their own endpoint), but are included, at least partially, in another resource due to
// the close relationship between them. This is often done to eliminate multiple requests to pull in all significant
// resource data. This is part of the HAL specification.
//----------------------------------------------------------------------------------------------------------------------
Service AddEmbeddedResources(ObjectHandle, EmbeddedResourceName, EmbeddedResourceObjectHandles, Singular)

    If (ObjectHandle NE '') AND (EmbeddedResourceName NE '') AND (EmbeddedResourceObjectHandles NE '') then
        If Singular NE True$ then Singular = False$
        If Singular EQ True$ then
            objEmbedded = HTTP_Resource_Services('AddSubProperty', '', EmbeddedResourceName, '', EmbeddedResourceObjectHandles, 'ObjectHandle')
        end else
            objEmbedded = SRP_JSON(ObjectHandle, 'Get', '_embedded')
            If objEmbedded EQ 0 then objEmbedded = ''
            objEmbedded = HTTP_Resource_Services('AddSubResourceObjects', objEmbedded, EmbeddedResourceName, EmbeddedResourceObjectHandles)
        end
        If Error_Services('NoError') then
            HTTP_Resource_Services('AddProperty', ObjectHandle, '_embedded', objEmbedded, 'ObjectHandle')
        end
    end else
        Error_Services('Add', 'ObjectHandle, EmbeddedResourceName, or EmbeddedResourceObjectHandles argument was missing from the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// AddFormAction
//
// ObjectHandle     - Object handle to the resource. - [Required]
// Name             - Name associated with this form action. - [Required]
// Method           - HTTP method used by this form action. - [Required]
// URL              - URL used as the target by this form action. - [Required]
// Title            - Display friendly title for this form action. Default is the Name argument. - [Optional]
// Fields           - A delimited list of fields that need to be submitted with this action. Fields can be interpreted
//                    in any way the API chooses, but typically they map to a property of the current resource.
//                    - [Optional]
// FieldProperties  - A structured @FM/@VM delimited array of property values associated with the fields. - [Optional]
//                    For each field, the value structure follows this pattern:
//                    <x, 1>    = Default value. The value that should be passed in unless otherwise overridden by the
//                                client.
//                    <x, 2>    = Required flag. A Boolean flag indicating if this property requires a value to be
//                                passed in.
//                    <x, 3>    = Visible flag. A Boolean flag indicating if this property should be visible to the
//                                client. A non-visible field simply means that the default value will automatically
//                                be passed in.
//
// Adds one _forms action in the indicated resource object. Due to the complexity and depth of a single action, this
// service will only add one action at a time. Additional actions can be added by calling this service multiple times.
//----------------------------------------------------------------------------------------------------------------------
Service AddFormAction(ObjectHandle, Name, Method=METHODS, URL, Title, Fields, FieldProperties)

    If (ObjectHandle NE '') AND (Name NE '') AND (Method NE '') AND (URL NE '') then
        If Title EQ '' then Title = Name
        Convert @VM : @SVM : @TM : @STM : ',' to @FM : @FM : @FM : @FM : @FM in Fields
        objAction   = HTTP_Resource_Services('GetObject')
        If Error_Services('NoError') then
            HTTP_Resource_Services('AddProperty', objAction, 'method', Method)
            HTTP_Resource_Services('AddProperty', objAction, 'action', URL)
            HTTP_Resource_Services('AddProperty', objAction, 'title', Title)
            objFields   = HTTP_Resource_Services('GetObject')
            If Error_Services('NoError') then
                If Fields NE '' then
                    For Each Field in Fields using @FM setting fPos
                        HTTP_Resource_Services('AddSubProperty', objFields, Field, 'default', FieldProperties<fPos, 1>, 'String')
                        HTTP_Resource_Services('AddSubProperty', objFields, Field, 'required', FieldProperties<fPos, 2>, 'Boolean')
                        HTTP_Resource_Services('AddSubProperty', objFields, Field, 'visible', FieldProperties<fPos, 3>, 'Boolean')
                    Next Field
                    HTTP_Resource_Services('AddProperty', objAction, 'fields', objFields, 'ObjectHandle')
                end
            end
            HTTP_Resource_Services('AddSubProperty', ObjectHandle, '_forms', Name, objAction, 'ObjectHandle')
        end
    end else
        Error_Services('Add', 'ObjectHandle, Name, Method, or URL argument was missing from the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSerializedResource
//
// ObjectHandle - Object handle to the resource needing to be serialized. - [Required]
// Style        - Formatting style for the final output (Fast, DropNulls, or Styled). Default = Fast. - [Optional]
//
// Serializes the indicated resource object and then releases the object.
//----------------------------------------------------------------------------------------------------------------------
Service GetSerializedResource(ObjectHandle, Style=STYLES)

    SerializedResource  = ''

    If ObjectHandle NE '' then
        If Style EQ '' then Style = 'Fast'
        SerializedResource  = SRP_JSON(ObjectHandle, 'Stringify', Style)
        SRP_JSON(ObjectHandle, 'Release')
    end else
        Error_Services('Add', 'ObjectHandle argument was missing from the ' : Service : ' service.')
    end

    Response            = SerializedResource

end service


//----------------------------------------------------------------------------------------------------------------------
// LoremIpsum
//
// Creates a dummy HTTP response using lorem ipsum content. This is a placeholder service used when new endpoint
// handlers are automatically created. It is expected to be replaced by the actual API logic.
//----------------------------------------------------------------------------------------------------------------------
Service LoremIpsum()

    ObjectHandle    = HTTP_Resource_Services('GetObjects')
    LoremIpsum      = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. '
    LoremIpsum     := 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure '
    LoremIpsum     := 'dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non '
    LoremIpsum     := 'proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
    HTTP_Resource_Services('AddProperty', ObjectHandle, 'content', LoremIpsum, 'String')
    FullEndPointURL = HTTP_Services('GetFullEndPointURL')
    APIURL          = HTTP_Services('GetAPIRootURL', True$)
    HTTP_Resource_Services('AddLinkRelations', ObjectHandle, 'self' : @FM : 'apiEntryPoint', FullEndpointURL : @FM : APIURL)
    jsonResource    = HTTP_Resource_Services('GetSerializedResource', ObjectHandle, 'Fast')
    HTTP_Services('SetResponseBody', jsonResource, False$, 'application/hal+json')
    HTTP_Services('SetResponseStatus', 200)

end service


//----------------------------------------------------------------------------------------------------------------------
// GetURLTemplate
//
// URL                  - The URL that needs to be transformed into a URL template. - [Required]
// IgnoreResourceID     - Boolean flag to determine if a resource ID segment should be left untemplated. - [Optional]
// IgnoreProperty       - Boolean flag to determine if a property segment should be left untemplated. - [Optional]
// IgnoreQueryParam     - Boolean flag to determine if existing query params should be left untemplated. - [Optional]
//
// Returns the indicated URL as a URL Template.
//----------------------------------------------------------------------------------------------------------------------
Service GetURLTemplate(URL, IgnoreResourceID=BOOLEAN, IgnoreProperty=BOOLEAN, IgnoreQueryParam=BOOLEAN)

    URLTemplate = ''

    If IgnoreResourceID NE True$ then IgnoreResourceID  = False$
    If IgnoreProperty   NE True$ then IgnoreProperty    = False$
    If IgnoreQueryParam NE True$ then IgnoreQueryParam  = False$

    If URL NE '' then
        If Index(URL, '{', 1) then
            // The URL already appears to be formatted as a template. Return the URL that was passed in.
            URLTemplate = URL
        end else
            // The URL is not formatted as a template. Match it to a resource and create a templated URL.
            If URL[1, 1] EQ '/' then
                // URL is formatted as a relative URL. Start the URL Template as empty.
                URLTemplate = ''
                URL[1, 1]   = ''
            end else
                // Assume the URL is absolute. Start the URL Template with the base URL.
                URLTemplate = Field(URL, '/', 1, 3)
                URL         = Field(URL, '/', 4, 999)
            end
            If Index(URL, '?', 1) then
                // URL contains query params. Remove these for now and add them back only if IgnoreQueryParams is set
                // to True$. This way the original query params will be included in the URL Template.
                OrigQueryParams                         = Field(URL, '?', 2, 1)
                URL[-1, Neg(Len(OrigQueryParams) + 1)]  = ''
            end else
                OrigQueryParams = ''
            end
            For Each Segment in URL using '/'
                URLTemplate    := '/' : Segment
                ResourceClass   = HTTP_Resource_Manager_Services('GetResourceProperty', URLTemplate, 'CLASS')
                Begin Case
                    Case ResourceClass _EQC 'RESOURCE_ID'
                        If IgnoreResourceID NE True$ then
                            // This segment is a Resource ID. Get the NAME property for this URL and use this for the
                            // segment instead.
                            ResourceID                          = HTTP_Resource_Manager_Services('GetResourceProperty', URLTemplate, 'NAME')
                            URLTemplate[-1, Neg(Len(Segment))]  = ResourceID
                        end
                    Case ResourceClass _EQC 'PROPERTY'
                        If IgnoreProperty NE True$ then
                            // This segment is a Property. Use {propertyName} for the segment instead.
                            URLTemplate[-1, Neg(Len(Segment))]  = '{propertyName}'
                        end
                End Case
            Next Segment
            If IgnoreQueryParam NE True$ then
                // Now the the URLTemplate is complete. Check to see if there are query params supported by this endpoint.
                QueryParams = HTTP_Resource_Manager_Services('GetResourceProperty', URLTemplate, 'QUERY_PARAMS')
                If QueryParams NE '' then
                    QueryParams     = '{?' : QueryParams : '}'
                    URLTemplate    := QueryParams
                end
            end else
                If OrigQueryParams NE '' then
                    URLTemplate    := '?' : OrigQueryParams
                end
            end
        end
    end else
        Error_Services('Add', 'URL argument was missing from the ' : Service : ' service.')
    end

    Response    = URLTemplate

end service


//----------------------------------------------------------------------------------------------------------------------
// GetDatabaseItem
//
// TableName        - Name of the database table where the resource (database row) is stored. - [Required]
// SelfURL          - The URL that identifies the item resource. - [Optional]
// KeyID            - Key ID for the database row. - [Required]
// ColumnNames      - An @FM delimited list of column names to build into the resource. If empty then all documented
//                    column names from the %FIELDS% dictionary record will be used. - [Optional]
// ItemArrayLabel   - Label text to use when naming the primary HAL item array. If empty then "item" will be
//                    used. - [Optional]
//
// Gets the resource item from a database table. The format will be HAL+JSON.
//----------------------------------------------------------------------------------------------------------------------
Service GetDatabaseItem(TableName, SelfURL, KeyID, ColumnNames, ItemArrayLabel)

    BaseSelfURL     = Field(SelfURL, '/', 1, Count(SelfURL, '/'))
    ObjectHandle    = HTTP_Resource_Services('GetObjects', TableName, KeyID, ColumnNames, ItemArrayLabel)
    If SelfURL NE '' then
        HTTP_Resource_Services('AddLinkRelations', ObjectHandle, 'self' : @FM : 'collection', SelfURL : @FM : BaseSelfURL)
    end
    HAL             = HTTP_Resource_Services('GetSerializedResource', ObjectHandle)
    HTTP_Services('SetResponseBody', HAL, False$, 'application/hal+json')

    // Use the Response variable for internal monitoring, tracking, and debugging. This does not affect the HTTP
    // response.
    Response    = HAL

end service


//----------------------------------------------------------------------------------------------------------------------
// GetDatabaseItems
//
// Filter           - Information that informs the service of how the database should be filtered. There are four
//                    options: 1. Empty, if all database rows should be selected, 2. A "SELECT" query that will be used
//                    in an RList() statement, 3. The name of a saved list of Key IDs, or 4. An @FM delimited list of
//                    Key IDs. - [Optional]
// TableName        - Name of the database table where the resource (database row) is stored. - [Required]
// SelfURL          - The URL that identifies the collection resource. - [Optional]
// ColumnNames      - An @FM delimited list of column names to build into the resource. If empty then all documented
//                    column names from the %FIELDS% dictionary record will be used. - [Optional]
// ItemArrayLabel   - Label text to use when naming the primary HAL item array. If empty then "item" will be
//                    used. - (Optional)
//
// Gets the resource items from a database table. The format will be HAL+JSON.
//----------------------------------------------------------------------------------------------------------------------
Service GetDatabaseItems(Filter, TableName, SelfURL, ColumnNames, ItemArrayLabel)

    HAL             = ''

    ObjectHandle    = HTTP_Resource_Services('GetObjects')
    If Error_Services('NoError') then
        objItems    = HTTP_Resource_Services('GetObjects', TableName, Filter, ColumnNames, ItemArrayLabel, '', '', '', SelfURL)
        HTTP_Resource_Services('AddEmbeddedResources', ObjectHandle, 'item', objItems)
        If SelfURL NE '' then
            // Add _links sub-properties for HAL implementation.
            HTTP_Resource_Services('AddLinkRelations', ObjectHandle, 'self', SelfURL)
        end
        If Error_Services('NoError') then
            // Serialize the object into a JSON string.
            HAL     = HTTP_Resource_Services('GetSerializedResource', ObjectHandle)
            // Set the response body with the JSON string and set the Content-Type response header.
            HTTP_Services('SetResponseBody', HAL, False$, 'application/hal+json')
        end
    end

    Response    = HAL

end service


//----------------------------------------------------------------------------------------------------------------------
// DeleteDatabaseItem
//
// TableName    - Name of the database table where the resource (database row) is stored. - [Required]
// KeyID        - Key ID for the database row. - [Required]
//
// Deletes the resource item from a database table.
//----------------------------------------------------------------------------------------------------------------------
Service DeleteDatabaseItem(TableName, KeyID)

    If (TableName NE '') AND (KeyID NE '') then
        Open TableName to hTable then
            Lock hTable, KeyID then
                Read DataRow from hTable, KeyID then
                    Delete hTable, KeyID then
                        HTTP_Services('SetResponseStatus', 204)
                    end else
                        HTTP_Services('SetResponseError', '', '', 500, 'Error deleting ' : KeyID : ' from the ' : TableName : ' table.')
                    end
                end else
                    HTTP_Services('SetResponseStatus', 204)
                end
                Unlock hTable, KeyID else Null
            end else
                HTTP_Services('SetResponseError', '', '', 423, KeyID : ' is currently locked.')
            end
        end else
            // Unable to open the database table. Set a 500 status code.
            HTTP_Services('SetResponseError', '', '', 500, 'Unable to open the ' : TableName : ' table.')
        end
    end else
        // Required arguments are missing so just set a 404 status code.
        HTTP_Services('SetResponseError', '', '', 404, 'Required arguments are missing.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// PutDatabaseItem
//
// TableName          - Name of the database table where the resource (database row) is stored. - [Required]
// SelfURL            - The URL that identifies the resource. - [Required]
// KeyID              - Key ID for the database row. - [Required]
// AllowedColumnNames - An @FM delimited list of column names that are permitted in this service. - [Optional]
//
// Creates a new or updates a specific resource item in the database table. To conform to the requirements of the PUT
// method, the entire resource will be replaced with the content being passed in, even if only a few properties (e.g.,
// database columns) are being updated. Thus, it is assumed that both changed and unchanged properties will be passed
// into the request body.
//----------------------------------------------------------------------------------------------------------------------
Service PutDatabaseItem(TableName, SelfURL, KeyID, AllowedColumnNames)

    If (TableName NE '') AND (SelfURL NE '') AND (KeyID NE '') then
        Open TableName to hTable then
            HaveKeyID  = False$             ; // Assume false for now.

            Lock hTable, KeyID then
                ItemURL     = SelfURL
                Read OrigDataRow from hTable, KeyID then
                    StatusCode  = 200       ; // Updating an existing resource.
                end else
                    StatusCode  = 201       ; // Creating a new resource.
                end
                DataRow     = ''            ; // Clearing data row of all content. PUT assumes the entire resource is being updated.
                HaveKeyID   = True$
            end

            If HaveKeyID then
                GoSub UpdateDatabaseRow
            end else
                HTTP_Services('SetResponseError', '', '', 423, KeyID : ' is currently locked.')
            end
        end else
            HTTP_Services('SetResponseError', '', '', 500, 'Error opening the ' : TableName : ' table.')
        end
    end else
        // Required arguments are missing so just set a 404 status code.
        HTTP_Services('SetResponseError', '', '', 404, 'Required arguments are missing.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// PostDatabaseItem
//
// TableName          - Name of the database table where the resource (database row) is stored. - [Required]
// SelfURL            - The URL that identifies the resource. - [Required]
// AllowedColumnNames - An @FM delimited list of column names that are permitted in this service. - [Optional]
//
// Creates a new resource item in the database table. To conform to the requirements of the POST method, the Key ID
// will be created automatically by the server and the entire resource will be created with the content being passed in.
//----------------------------------------------------------------------------------------------------------------------
Service PostDatabaseItem(TableName, SelfURL, AllowedColumnNames)

    If (TableName NE '') AND (SelfURL NE '') then
        Open TableName to hTable then
            HaveKeyID  = False$     ; // Assume false for now.

            // Logic to create a new KeyID is placed here. The following code attempts to retrieve the KeyID from a
            // %SK% counter. If this fails, it will attempt to retrieve it from %RECORD%. Finally, if this fails then
            // KeyID will be set to a value of 1 and the code will increment this value until a new Key ID can be found.
            KeyID      = Xlate('DICT.' : TableName, '%SK%', 1, 'X')
            If KeyID EQ '' then
                KeyIDs = Xlate(TableName, '%RECORDS%', '', 'X')
                KeyID  = KeyIDs[-1, 'B' : @FM]
                KeyID += 1
            end
            If KeyID EQ '' then KeyID = 1
            Loop
                Lock hTable, KeyID then
                    Read DataRow from hTable, KeyID then
                        Unlock hTable, KeyID else Null
                    end else
                        OrigDataRow = ''
                        DataRow     = ''
                        ItemURL     = SelfURL : '/' : KeyID
                        StatusCode  = 201                   ; // Creating a new resource.
                        HaveKeyID   = True$
                    end
                end
            Until HaveKeyID
                KeyID  += 1
            Repeat
            
            If HaveKeyID then
                GoSub UpdateDatabaseRow
            end else
                HTTP_Services('SetResponseError', '', '', 400, 'Error attempting to POST the resource to the ' : TableName : ' table.')
            end
        end else
            HTTP_Services('SetResponseError', '', '', 500, 'Error opening the ' : TableName : ' table.')
        end
    end else
        // Required arguments are missing so just set a 404 status code.
        HTTP_Services('SetResponseError', '', '', 404, 'Required arguments are missing.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// PatchDatabaseItem
//
// TableName          - Name of the database table where the resource (database row) is stored. - [Required]
// SelfURL            - The URL that identifies the resource. - [Required]
// KeyID              - Key ID for the database row. - [Required]
// AllowedColumnNames - An @FM delimited list of column names that are permitted in this service. - [Optional]
//
// Updates a specific resource item in the database table. To conform to the requirements of the PATCH method, only
// specific properties (e.g., database columns) of the resource will be updated. All properties not identified will
// be left alone.
//----------------------------------------------------------------------------------------------------------------------
Service PatchDatabaseItem(TableName, SelfURL, KeyID, AllowedColumnNames)

    If (TableName NE '') AND (SelfURL NE '') AND (KeyID NE '') then
        Open TableName to hTable then
            HaveKeyID  = False$             ; // Assume false for now.

            Lock hTable, KeyID then
                ItemURL     = SelfURL
                Read OrigDataRow from hTable, KeyID then
                    DataRow     = OrigDataRow
                    StatusCode  = 200       ; // Updating an existing resource.
                    HaveKeyID   = True$
                end else
                    StatusCode  = 404       ; // Creating a new resource, which is unsupported for the PATCH method.
                end
            end

            If HaveKeyID then
                GoSub UpdateDatabaseRow
            end else
                HTTP_Services('SetResponseError', '', '', 423, KeyID : ' is currently locked.')
            end
        end else
            HTTP_Services('SetResponseError', '', '', 500, 'Error opening the ' : TableName : ' table.')
        end
    end else
        // Required arguments are missing so just set a 404 status code.
        HTTP_Services('SetResponseError', '', '', 404, 'Required arguments are missing.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// GetColumnNames
//
// TableName            - Name of the database table where the resource (database row) is stored. - [Required]
// ColumnNames          - An @FM delimited list of column names to build into the resource. If empty then all documented
//                        column names from the %FIELDS% dictionary record will be used. - [Optional]
// AllowKeyProperty     - A Boolean flag indicating that the resource object be allowed to represent the Key ID of the
//                        database row to exists. This ultimately informs the GetColumnNames service to avoid stripping
//                        out the Key ID column. - [Optional]
//
// Gets the column names from the dictionary of the table provided that will be used to create the resource. If the
// ColumnNames argument is used then this service will validate the list as valid columns. In either case, the following
// rules will be applied:
//
//  - Regardless of the case used in the ColumnNames argument it will be upper cased to match against the dictionary
//    names correctly.
//  - All column names bound to a Key ID of part 0 (i.e., the entire key) will be removed (unless AllowKeyProperty is
//    set to True).
//  - All non-master column names will be removed to avoid duplicates.
//  - SRP FrameWorks audit column names (MODIFIED_BY, MODIFIED_DATE, and MODIFIED_TIME) will be removed.
//----------------------------------------------------------------------------------------------------------------------
Service GetColumnNames(TableName, ColumnNames, AllowKeyProperty)

    If AllowKeyProperty NE True$ then AllowKeyProperty = False$

    // Make any column names passed in safe to work with.
    Convert @Lower_Case to @Upper_Case in ColumnNames
    Convert ',' to @FM in ColumnNames
    CompareColumnNames  = ColumnNames

    // Create a service Key ID suitable for caching the results.
    ServiceKeyID   := '*' : TableName
    If ColumnNames NE '' then
        ServiceKeyID   := '*' : SRP_Hash(ColumnNames, 'SHA-1', 'BASE32')
    end
    // Check to see if column names have been cached this session.
    ColumnNames     = Memory_Services('GetValue', ServiceKeyID, '', '', CacheName$)

    If ColumnNames EQ '' then
        If TableName NE '' then
            Open TableName to hTable then
                Open 'DICT.' : TableName to @DICT then
                    Read Fields from @DICT, '%FIELDS%' else Fields = ''
                    // Sort the fields array by descending Field Type. This puts Calculated (Type = 'S') columns before
                    // Data (Type = 'F') columns. All Calculated columns qualify, so the list can stop when the
                    // calculated columns are reached.
                    Fields      = SRP_Array('SortRows', Fields, 'DL' : FIELDS_TYPE$)
                    FieldsList  = SRP_Array('Rotate', Fields)

                    // Restore the list of required column names, if any.
                    Transfer CompareColumnNames to ColumnNames

                    // Loop through the fields starting at the bottom and remove all fields that do not qualify. These
                    // conditions disqualify a field:
                    //  - Full Key IDs - This allows Key Part columns to properly represent the data and abstract the
                    //                   resource better.
                    //  - Non-master data type columns - This prevents duplication of data.
                    //  - Empty Column Names - This prevents special dictionary columns, like 'G' type from being
                    //                         included.
                    //  - Cross-reference columns - These are calculated columns meant for indexing only.
                    FieldPtr    = DCount(FieldsList, @FM)
                    Loop
                    Until FieldPtr EQ 0
                        Master      = Fields<FIELDS_MASTER_FLAG$, FieldPtr> ; // Calculated columns have no Master flag.
                        Type        = Fields<FIELDS_TYPE$, FieldPtr>
                        Conv        = Fields<FIELDS_CONV$, FieldPtr>
                        MV          = Fields<FIELDS_MVFLAG$, FieldPtr>
                        Pos         = Fields<FIELDS_FIELD_NO$, FieldPtr>
                        ColumnName  = Fields<FIELDS_NAME$, FieldPtr>
                        KeyPart     = Fields<FIELDS_PART$, FieldPtr>
                        GroupName   = Fields<FIELDS_MV_GROUPNAME$, FieldPtr>
                        Indexed     = Fields<FIELDS_INDEX$, FieldPtr>
                        If (Master AND (Not(Pos EQ 0 AND KeyPart EQ 0) OR (AllowKeyProperty EQ True$))) OR ((Type _EQC 'S') AND (ColumnName[-5, 5] _NEC '_XREF')) then
                            // The column must be a Master column (but not a full Key ID).
                            If ColumnName EQ '' then
                                // If the column does not have a name in the FIELDS_NAME$ value (for whatever reason) then
                                // remove it from the array so it will be be included in the final results.
                                Fields      = Delete(Fields, FIELDS_FIELD_NO$, FieldPtr, 0)
                                Fields      = Delete(Fields, FIELDS_PART$, FieldPtr, 0)
                                FieldsList  = Delete(FieldsList, FieldPtr, 0, 0)
                            end
                        end else
                            // The column does not meet the criteria. Remove the reference from the Fields array. Check
                            // to see if this field was included in the ColumnNames argument and remove from that as
                            // well.
                            Fields      = Delete(Fields, FIELDS_FIELD_NO$, FieldPtr, 0)
                            Fields      = Delete(Fields, FIELDS_PART$, FieldPtr, 0)
                            FieldsList  = Delete(FieldsList, FieldPtr, 0, 0)
                            Locate ColumnName in ColumnNames using @FM setting fPos then ColumnNames = Delete(ColumnNames, fPos, 0, 0)
                        end
                        // Decrement the Field Position counter so the next item above in the array can be reviewed.
                        FieldPtr -= 1
                    Repeat
                    Fields      = SRP_Array('Rotate', FieldsList)
                    FieldNames  = Fields<FIELDS_NAME$>
                    Convert @VM to @FM in FieldNames
                    If Len(ColumnNames) then
                        // Check the supplied list of column names to make sure they are valid.
                        CompareColumnNames  = ColumnNames
                        For Each ColumnName in CompareColumnNames using @FM
                            Locate ColumnName in FieldNames using @FM setting fPos else
                                Locate ColumnName in ColumnNames using @FM setting fPos then
                                    ColumnNames = Delete(ColumnNames, fPos, 0, 0)
                                end
                            end
                        Next ColumnName
                    end else
                        ColumnNames = Fields<FIELDS_NAME$>
                        Convert @VM to @FM in ColumnNames
                    end
                    // Exclude the audit column names in case they exist.
                    Locate 'MODIFIED_BY' in ColumnNames using @FM setting fPos then ColumnNames = Delete(ColumnNames, fPos, 0, 0)
                    Locate 'MODIFIED_DATE' in ColumnNames using @FM setting fPos then ColumnNames = Delete(ColumnNames, fPos, 0, 0)
                    Locate 'MODIFIED_TIME' in ColumnNames using @FM setting fPos then ColumnNames = Delete(ColumnNames, fPos, 0, 0)
                    If Len(ServiceKeyID) then
                        Memory_Services('SetValue', ServiceKeyID, ColumnNames, CacheName$)
                    end
                end else
                    // Unable to open the dictionary table. Set a 500 status code.
                    ErrorMessage = 'Unable to open the ' : TableName : ' dictionary table.'
                    HTTP_Services('SetResponseError', '', '', 500, ErrorMessage)
                    Error_Services('Add', ErrorMessage)
                end
            end else
                // Unable to open the database table. Set a 500 status code.
                ErrorMessage = 'Unable to open the ' : TableName : ' table.'
                HTTP_Services('SetResponseError', '', '', 500, ErrorMessage)
                Error_Services('Add', ErrorMessage)
            end
        end else
            // Required arguments are missing so just set a 404 status code.
            ErrorMessage = 'Required arguments are missing.'
            HTTP_Services('SetResponseError', '', '', 404, ErrorMessage)
            Error_Services('Add', ErrorMessage)
        end
    end

    Response    = ColumnNames

end service


//----------------------------------------------------------------------------------------------------------------------
// GetColumnValues
//
// TableName    - Name of the database table where the resource (database row) is stored. - [Required]
// ColumnNames  - An @FM delimited list of column names to build into the resource. - [Required]
// KeyID        - Key ID for the database row. - [Required]
//
// Gets the column values from the database row from the provided table and ID that will be used to create the resource.
// This is returned as an @FM delimited list of values.
//----------------------------------------------------------------------------------------------------------------------
Service GetColumnValues(TableName, ColumnNames, KeyID)

    ServiceKeyID   := '*' : SRP_Hash(TableName : ColumnNames : KeyID, 'SHA-1', 'BASE32')
    ColumnValues    = Memory_Services('GetValue', ServiceKeyID, True$, 10, CacheName$)

    If ColumnValues EQ '' then
        If (TableName NE '') AND (ColumnNames NE '') AND (KeyID NE '') then
            Open TableName to hTable then
                Open 'DICT.' : TableName to @DICT then
                    Read Fields from @DICT, '%FIELDS%' else Fields = ''
                    @ID = KeyID
                    Read @RECORD from hTable, @ID then
                        Convert @Lower_Case to @Upper_Case in ColumnNames
                        NumColumns = Count(ColumnNames, @FM) + (ColumnNames NE '')

                        If ColumnNames NE '' then
                            For Each ColumnName in ColumnNames using @FM setting ColumnCnt
                                ColumnName  = ColumnNames<ColumnCnt>
                                ColumnValue = ''
                                Locate ColumnName in Fields<FIELDS_NAME$> using @VM setting vPos then
                                    Master      = Fields<FIELDS_MASTER_FLAG$, vPos>
                                    Type        = Fields<FIELDS_TYPE$, vPos>
                                    Conv        = Fields<FIELDS_CONV$, vPos>
                                    MV          = Fields<FIELDS_MVFLAG$, vPos>
                                    Pos         = Fields<FIELDS_FIELD_NO$, vPos>
                                    KeyPart     = Fields<FIELDS_PART$, vPos>
                                    GroupName   = Fields<FIELDS_MV_GROUPNAME$, vPos>

                                    // Now get the value based on factors like data vs. calculated and whole key vs. key part
                                    Begin Case
                                        Case Type EQ 'F'
                                            // Data column.
                                            If Len(KeyPart) then
                                                // This is a Key ID column. If KeyPart is 0 then this represents the entire
                                                // Key ID, which is not normally returned since the Item ID is already known
                                                // and will appear in the URL. If this is a multipart Key ID then KeyPart
                                                // will be an integer. This value should be returned since it likely is a
                                                // significant part of the resource.
                                                If KeyPart EQ 0 then
                                                    ColumnValue = @ID
                                                end else
                                                    ColumnValue = Field(@ID, '*', KeyPart, 1)
                                                end
                                            end else
                                                ColumnValue = @RECORD<Pos>
                                            end

                                        Case Type EQ 'S'
                                            // Calculated (symbolic) column. If @RM or @FM delimiters are discovered then all
                                            // delimiters will be decremented as needed so the response payload will not become
                                            // out of sync.
                                            ColumnValue = Calculate(ColumnName)
                                            If Index(ColumnValue, @RM, 1) then
                                                Convert @STM to Char(248) in ColumnValue
                                                Convert @TM to Char(249) in ColumnValue
                                                Convert @SVM to @STM in ColumnValue
                                                Convert @VM to @TM in ColumnValue
                                                Convert @FM to @SVM in ColumnValue
                                                Convert @RM to @VM in ColumnValue
                                            end else
                                                If Index(ColumnValue, @FM, 1) then
                                                    Convert @STM to Char(249) in ColumnValue
                                                    Convert @TM to @STM in ColumnValue
                                                    Convert @SVM to @TM in ColumnValue
                                                    Convert @VM to @SVM in ColumnValue
                                                    Convert @FM to @VM in ColumnValue
                                                end
                                            end

                                    End Case

                                    // If there is a conversion pattern then apply it.
                                    If Len(Conv) then ColumnValue = Oconv(ColumnValue, Conv)

*                                   If KeyPart EQ 0 then
*                                       ColumnNames     = Delete(ColumnNames, ColumnCnt, 0, 0)
*                                   end else
                                        ColumnValues   := ColumnValue : @FM
*                                   end
                                end
                            Next ColumnName
                        end

                        ColumnValues[-1, 1] = ''
                        Memory_Services('SetValue', ServiceKeyID, ColumnValues, CacheName$)
                    end else
                        ErrorMessage = KeyID : ' is not a valid row in the ' : TableName : ' table.'
                        HTTP_Services('SetResponseError', '', '', 404, ErrorMessage)
                        Error_Services('Add', ErrorMessage)
                    end
                end else
                    // Unable to open the dictionary table. Set a 500 status code.
                    ErrorMessage = 'Unable to open the ' : TableName : ' dictionary table.'
                    HTTP_Services('SetResponseError', '', '', 500, ErrorMessage)
                    Error_Services('Add', ErrorMessage)
                end
            end else
                // Unable to open the database table. Set a 500 status code.
                ErrorMessage = 'Unable to open the ' : TableName : ' table.'
                HTTP_Services('SetResponseError', '', '', 500, ErrorMessage)
                Error_Services('Add', ErrorMessage)
            end
        end else
            // Required arguments are missing so just set a 404 status code.
            ErrorMessage = 'Required arguments are missing.'
            HTTP_Services('SetResponseError', '', '', 404, ErrorMessage)
            Error_Services('Add', ErrorMessage)
        end
    end

    Response    = ColumnValues

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMVGroupNames
//
// TableName    - Name of the database table. - [Required]
// ColumnNames  - An @FM delimited list of column names. - [Required]
//
// Gets the MV group names for the dictionary columns passed in. Note: the ColumnNames argument should be the same list
// that was returned from the GetColumnNames service. This will return an @FM delimited list of MV group
// names. Thus, for non-multivalue fields, the values will be empty. The MV group names will be returned in lower case.
//----------------------------------------------------------------------------------------------------------------------
Service GetMVGroupNames(TableName, ColumnNames)

    ServiceKeyID   := '*' : SRP_Hash(TableName : ColumnNames, 'SHA-1', 'BASE32')
    MVGroupNames    = Memory_Services('GetValue', ServiceKeyID, '', '', CacheName$)

    If MVGroupNames EQ '' then
        If (TableName NE '') AND (ColumnNames NE '') then
            Open TableName to hTable then
                Open 'DICT.' : TableName to @DICT then
                    Read Fields from @DICT, '%FIELDS%' else Fields = ''
                    MVGroupNames    = ''
                    AllColumnNames  = Fields<FIELDS_NAME$>
                    AllGroupNames   = Fields<FIELDS_MV_GROUPNAME$>
                    // Convert to upper case for now so that matches against the dictionary column names can be made.
                    Convert @Lower_Case to @Upper_Case in ColumnNames
                    // Loop through the fields and append the matching MV group names to the list.
                    NumColumns      = DCount(ColumnNames, @FM)
                    For ColumnCnt = 1 to NumColumns
                        ColumnName  = ColumnNames<ColumnCnt>
                        Locate ColumnName in AllColumnNames using @VM setting vPos then MVGroupNames := AllGroupNames<0, vPos> : @FM
                    Next ColumnCnt
                    MVGroupNames[-1, 1] = ''
                    Convert @Upper_Case to @Lower_Case in MVGroupNames
                    Memory_Services('SetValue', ServiceKeyID, MVGroupNames, CacheName$)
                end else
                    // Unable to open the dictionary table. Set a 500 status code.
                    ErrorMessage = 'Unable to open the ' : TableName : ' dictionary table.'
                    HTTP_Services('SetResponseError', '', '', 500, ErrorMessage)
                    Error_Services('Add', ErrorMessage)
                end
            end else
                // Unable to open the database table. Set a 500 status code.
                ErrorMessage = 'Unable to open the ' : TableName : ' table.'
                HTTP_Services('SetResponseError', '', '', 500, ErrorMessage)
                Error_Services('Add', ErrorMessage)
            end
        end else
            // Required arguments are missing so just set a 404 status code.
            ErrorMessage = 'Required arguments are missing.'
            HTTP_Services('SetResponseError', '', '', 404, 'Required arguments are missing.')
            Error_Services('Add', ErrorMessage)
        end
    end

    Response    = MVGroupNames

end service


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//----------------------------------------------------------------------------------------------------------------------
// UpdateDatabaseRow
//
// Updates the database row with the data contained in the Request Body. This will write the updated row data to the
// table and it will update the HTTP response status and headers as needed. This gosub is used by the PostDatabaseItem,
// PutDatabaseItem, and PatchDatabaseItem services.
//----------------------------------------------------------------------------------------------------------------------
UpdateDatabaseRow:

    // The new resource will have been put into the POST string.
    Body = HTTP_Services('GetHTTPPostString')
    // The POST string will have been encoded so use percent (URL) decoding.
    Body = HTTP_Services('DecodePercentString', Body)
    Convert @Lower_Case to @Upper_Case in AllowedColumnNames

    If Body EQ '' then
        ParseResponse   = SRP_JSON(hBody, 'NEW', 'OBJECT')
    end else
        ParseResponse   = SRP_JSON(hBody, 'PARSE', Body)
    end
    If (ParseResponse EQ '') OR (ParseResponse EQ 1) then
        // Go through the column names in the JSON object and map the column values to the respective
        // field positions in the database row.
        Fields          = Xlate('DICT.' : TableName, '%FIELDS%', '', 'X')
        NumColumns      = SRP_JSON(hBody, 'GETCOUNT')
        ColumnNames     = SRP_JSON(hBody, 'GETMEMBERS')
        If ColumnNames NE '' then
            For Each ColumnName in ColumnNames using @FM setting ColumnCnt
                ColumnName  = ColumnNames<ColumnCnt>
                hColumn     = SRP_JSON(hBody, 'GET', ColumnName)
                JSONType    = SRP_JSON(hColumn, 'TYPE')
                If ColumnName[1, 1] NE '_' then
                    // Ignore special properties which are for HATEOAS purpose.
                    Begin Case
                        Case JSONType _EQC 'Array'
                            // Count the number of members in the array then loop through to build the @VM value list.
                            ColumnValues    = ''
                            NumItems        = SRP_JSON(hColumn, 'GETCOUNT')
                            For ItemCnt = 1 to NumItems
                                hItem       = SRP_JSON(hColumn, 'GET', ItemCnt)
                                ItemNames   = SRP_JSON(hItem, 'GETMEMBERS')
                                If ItemNames NE '' then
                                    For Each ColumnName in ItemNames using @FM setting ItemPtr
                                        ColumnValues<ItemPtr>   = ColumnValues<ItemPtr> : SRP_JSON(hItem, 'GETVALUE', ColumnName, '') : @VM
                                    Next ItemName
                                end
                                SRP_JSON(hItem, 'Release')
                            Next NumItems
                            If ItemNames NE '' then
                                For Each ColumnName in ItemNames using @FM setting ItemPtr
                                    ColumnValue         = ColumnValues<ItemPtr>
                                    ColumnValue[-1, 1]  = ''
                                    GoSub UpdateColumnData
                                Next ColumnName
                            end

                        Case JSONType _EQC 'Object'
                            // This is likely an MV group with one or more embedded arrays which need to be processed
                            // individually.
                            NumMVColumns    = SRP_JSON(hColumn, 'GETCOUNT')
                            MVColumnNames   = SRP_JSON(hColumn, 'GETMEMBERS')
                            If MVColumnNames NE '' then
                                For Each ColumnName in MVColumnNames using @FM setting MVColumnCnt
                                    ColumnName      = MVColumnNames<MVColumnCnt>
                                    hMVColumn       = SRP_JSON(hColumn, 'GET', ColumnName)
                                    ColumnValue     = ''
                                    NumValues       = SRP_JSON(hMVColumn, 'GETCOUNT')
                                    For ValueCnt = 1 to NumValues
                                        ColumnValue := SRP_JSON(hMVColumn, 'GETVALUE', ColumnName : '[' : ValueCnt : ']', '') : @VM
                                    Next ValueCnt
                                    ColumnValue[-1, 1] = ''
                                    SRP_JSON(hMVColumn, 'RELEASE')
                                    GoSub UpdateColumnData
                                Next ColumnName
                            end

                        Case Otherwise$
                            // This is just a regular column. Get the value.
                            ColumnValue = SRP_JSON(hBody, 'GETVALUE', ColumnName, '')
                            GoSub UpdateColumnData

                    End Case
                end

                SRP_JSON(hColumn, 'RELEASE')
            Next ColumnName
        end

        Write DataRow to hTable, KeyID then
            // The database row has been successfully updated. Set the response status as originally identified by the
            // calling service. Set the Content-Location response header with the new or updated items URL so the
            // client can retrieve the resource if needed.
            HTTP_Services('SetResponseStatus', StatusCode)
            HTTP_Services('SetResponseHeaderField', 'Location', ItemURL)
        end else
            HTTP_Services('SetResponseError', '', '', 500, 'Error writing ' : KeyID : ' to the ' : TableName : ' table.')
        end
    end else
        HTTP_Services('SetResponseError', '', '', 400, ParseResponse)
    end
    Unlock hTable, KeyID else Null

return


//----------------------------------------------------------------------------------------------------------------------
// UpdateColumnData
//
// Updates the specified database column with the indicated value. This only updates the dynamic array and does not
// write the data to the table.
//----------------------------------------------------------------------------------------------------------------------
UpdateColumnData:

    Convert @Lower_Case to @Upper_Case in ColumnName

	AllowContinue   = True$ ; // Assume this can continue for now.
	If AllowedColumnNames NE '' then
        Locate ColumnName in AllowedColumnNames using @FM setting AllowedFound else
            // This column is not approved for updating.
            AllowContinue = False$
        end
    end

    Locate ColumnName in Fields<FIELDS_NAME$> using @VM setting vPos then
        Pos     = Fields<FIELDS_FIELD_NO$, vPos>
        Type    = Fields<FIELDS_TYPE$, vPos>
        If Type EQ 'F' then
            If AllowContinue EQ True$ then
                Conv    = Fields<FIELDS_CONV$, vPos>
                If Len(Conv) then ColumnValue = IConv(ColumnValue, Conv)
                DataRow<Pos> = ColumnValue
            end else
                // This column is not allowed to be updated so make sure it retains the value stored in the database.
                DataRow<Pos>    = OrigDataRow<Pos>
            end
        end
    end

return
