Function HTTP_Resource_Manager_Services(@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_Manager_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) 07/12/19 dmb [SRPFW-277] Original programmer. 07/13/19 dmb [SRPFW-277] Added various services so that the NDW_HTTP_FRAMEWORKS_SERVICES commuter as well as additional tools can get or set various resource meta data. 07/15/19 dmb [SRPFW-277] Added the GetEndpointResourceKeyID service. 07/16/19 dmb [SRPFW-277] Added the GetResourceSignature service. 07/16/19 dmb [SRPFW-277] Update GetResourceProperty service to support the RESOURCE property. 07/16/19 dmb [SRPFW-277] Replaced harcoded cache duration with the CacheTTL$ equate. 07/16/19 dmb [SRPFW-277] Rename the IsResource service to IsValidEndpoint. 07/17/19 dmb [SRPFW-277] Update the SetResource service to refresh the GetResource cache better. 07/24/19 dmb [SRPFW-278] Fix bug in the GetEndpointResourceKeyID service so that matching the URLEndpoint works better. 09/25/19 dmb [SRPFW-278] Fix bug in the GetResource service that prevented endpoints with a resource ID from being matched correctly to the currently stored resources endpoints. 12/09/19 dmb [SRPFW-296] Update all calls to Memory_Services to use a specific cache name. 01/23/20 dmb [SRPFW-296] Update the SetResourceProperty and GetResourceSignature services to support the HEAD method. ***********************************************************************************************************************/ #pragma precomp SRP_PreCompiler $insert LOGICAL $insert HTTP_INSERTS $insert SERVICE_SETUP $insert INET_EQUATES $insert INET_HEADERS $insert HTTP_FRAMEWORK_SETUP_EQUATES Equ CRLF$ to \0D0A\ Equ CacheTTL$ to 300 ; // Allow cached data to only be fresh for 5 seconds. Equ CacheName$ to 'SRPHTTPFramework' Equ ValidMethods$ to 'GET,POST,PUT,PATCH,DELETE,HEAD' Declare function HTTP_Resource_Manager_Services, HTTP_Resource_Services, Database_Services, Memory_Services, SRP_Array Declare subroutine HTTP_Resource_Manager_Services, HTTP_Resource_Services, Database_Services, Memory_Services GoToService else HTTP_Services('SetResponseError', '', '', 404, Service : ' is not a valid service request within the HTTP Resource Manager services module.') end Return Response OR '' //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Service Parameter Options //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Options BOOLEAN = True$, False$ Options CLASSES = 'APIROOT', 'RESOURCE', 'RESOURCE_ID', 'PROPERTY' Options PROPERTIES = 'CLASS', 'DESCRIPTION', 'EXCLUDE_LOGGING', 'METHODS', 'NAME', 'RESOURCE', 'TITLE', 'QUERY_PARAMS' Options CHILDREN = 'ALL', 'RESOURCE', 'RESOURCE_ID', 'PROPERTY' Options METHODS = 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD' //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Services //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //---------------------------------------------------------------------------------------------------------------------- // GetResourceProperty // // URLEndpoint - URL endpoint for the resource. If empty, the APIROOT resource will be returned. - [Optional] // PropertyName - Name of the property whose value is being queried. - [Required] // // Returns the value of the indicated resource property for the indicated URL endpoint. //---------------------------------------------------------------------------------------------------------------------- Service GetResourceProperty(URLEndpoint, PropertyName=PROPERTIES) PropertyValue = '' If PropertyName NE '' then Resource = HTTP_Resource_Manager_Services('GetResource', URLEndpoint) If Error_Services('NoError') then PropertyNames = Resource<1> Locate PropertyName in PropertyNames using @VM setting PropertyPos then PropertyValue = Resource<2, PropertyPos> end Begin Case Case PropertyName EQ 'EXCLUDE_LOGGING' // This property defaults to false unless true. If PropertyValue NE True$ then PropertyValue = False$ Case PropertyName EQ 'RESOURCE' AND PropertyValue = '' // Unless the system has forced this property to store a value, it should be derived from the // resource signature. Signature = HTTP_Resource_Manager_Services('GetResourceSignature', URLEndpoint) PropertyValue = Signature[1, '.'] End Case end end else Error_Services('Add', 'PropertyName argument was missing in the ' : Service : ' service.') end Response = PropertyValue end service //---------------------------------------------------------------------------------------------------------------------- // SetResourceProperty // // URLEndpoint - URL endpoint for the resource. If empty, the APIROOT resource will be returned. - [Optional] // PropertyName - Name of the property whose value is being set. - [Required] // PropertyValue - Value of the property being updated. Depending upon the property, this might clear the value or // set a default value. - [Optional] // // Sets (or updates) the value of the indicated resource property for the indicated URL endpoint. //---------------------------------------------------------------------------------------------------------------------- Service SetResourceProperty(URLEndpoint, PropertyName=PROPERTIES, PropertyValue) If PropertyName NE '' then Resource = HTTP_Resource_Manager_Services('GetResource', URLEndpoint) If Error_Services('NoError') then PropertyNames = Resource<1> Begin Case Case PropertyName EQ 'METHODS' // Make sure only valid HTTP methods are set. Transfer PropertyValue to Methods For Each Method in Methods using ',' setting cPos Locate Method in ValidMethods$ using ',' setting MethodPos then PropertyValue := Method : ',' end Next Method PropertyValue[-1, 1] = '' Case PropertyName EQ 'EXCLUDE_LOGGING' // This property defaults to false unless true. If PropertyValue NE True$ then PropertyValue = False$ Case PropertyName EQ 'CLASS' // Make sure only a valid resource class is set. Locate PropertyValue in 'RESOURCE,RESOURCE_ID,PROPERTY' using ',' setting ClassPos else PropertyValue = 'RESOURCE' end End Case Locate PropertyName in PropertyNames using @VM setting PropertyPos else Resource<1, PropertyPos> = PropertyName end Resource<2, PropertyPos> = PropertyValue HTTP_Resource_Manager_Services('SetResource', URLEndpoint, Resource) end end else Error_Services('Add', 'PropertyName argument was missing in the ' : Service : ' service.') end end service //---------------------------------------------------------------------------------------------------------------------- // GetResourceChildren // // URLEndpoint - URL endpoint for the resource. If empty, the APIROOT resource will be returned. - [Optional] // ChildType - Type of child resource being queried. If empty or ALL then all children are returned. - [Optional] // // Returns an @FM delimited list of URL endpoints matching the indicated child type that are children to the indicated // URL endpoint. //---------------------------------------------------------------------------------------------------------------------- Service GetResourceChildren(URLEndpoint, ChildType=CHILDREN) Children = '' ResourceList = HTTP_Resource_Manager_Services('GetResourceList') If Error_Services('NoError') then // Loop through the resource list to find resource children. If ResourceList NE '' then Locate ChildType in 'ALL,RESOURCE,RESOURCE_ID,PROPERTY' using ',' setting ChildPos else ChildType = 'ALL' end ThisEndpointKeyID = HTTP_Resource_Manager_Services('GetEndpointResourceKeyID', URLEndpoint) ThisLevel = DCount(ThisEndpoint, '/') For Each ResourceItem in ResourceList using @FM Endpoint = ResourceItem<0, 2> EndpointLevel = DCount(Endpoint, '/') If EndpointLevel EQ 2 then EndpointParent = 'APIROOT' end else EndpointParent = Field(Endpoint, '/', 1, EndpointLevel - 1) end If EndpointParent _EQC ThisEndpointKeyID then EndpointType = ResourceItem<0, 5> If (EndpointType EQ ChildType) OR (ChildType EQ 'ALL') then Children := Endpoint : @FM end end Next ResourceItem end end Children[-1, 1] = '' Response = Children end service //---------------------------------------------------------------------------------------------------------------------- // GetResourceSignature // // URLEndpoint - URL endpoint for the resource. If empty, the APIROOT resource will be returned. - [Optional] // Method - HTTP method for the signature. This must be a valid method but it can also be empty. If empty, // the signature returned will simply omit the method. - [Optional] // // Returns the API calling signature for the indicated URL endpoint. This is used by the RunWebAPI service (a member of // HTTP_Services) when calling the Web API module. //---------------------------------------------------------------------------------------------------------------------- Service GetResourceSignature(URLEndpoint, Method=METHODS) Signature = '' URLEndpointKeyID = HTTP_Resource_Manager_Services('GetEndpointResourceKeyID', URLEndpoint) // Validate the HTTP method. Locate Method in ValidMethods$ using ',' setting MethodPos else Method = '' end If URLEndpointKeyID EQ 'APIROOT' then Signature = 'APIROOT.' end else ThisLevel = DCount(URLEndpointKeyID, '/') For LevelCnt = 2 to ThisLevel Class = HTTP_Resource_Manager_Services('GetResourceProperty', Field(URLEndpointKeyID, '/', 1, LevelCnt), 'CLASS') Begin Case Case Class EQ 'RESOURCE' // A resource starts a new signature. Signature = Field(URLEndpointKeyID, '/', LevelCnt, 1) : '.' Case Class EQ 'RESOURCE_ID' Signature := 'ID.' Case Otherwise$ Signature := Field(URLEndpointKeyID, '/', LevelCnt, 1) : '.' End Case Next LevelCnt end If Method NE '' then Signature := Method end else Signature[-1, 1] = '' end Response = Signature end service //---------------------------------------------------------------------------------------------------------------------- // GetEndpointResourceKeyID // // URLEndpoint - URL endpoint to be validated. If empty, the APIROOT resource will be returned. - [Optional] // AllowNew - Boolean flag indicating if a URL endpoint can be returned if new. Default is false. - [Optional] // // Returns the resource Key ID for the indicated URL endpoint. If AllowNew is true, a URL endpoint will be returned that // best matches the available endpoints but it will not be cached. //---------------------------------------------------------------------------------------------------------------------- Service GetEndpointResourceKeyID(URLEndpoint, AllowNew) ServiceKeyID := '*' : URLEndpoint ResourceKeyID = Memory_Services('GetValue', ServiceKeyID, True$, CacheTTL$, CacheName$) ResourceKeyIDFound = True$ ; // Assume true for now. If AllowNew NE True$ then AllowNew = False$ IsNew = False$ ; // Assume false for now. If ResourceKeyID EQ '' then If (URLEndpoint EQ '') OR (URLEndpoint _EQC 'APIROOT') then ResourceKeyID = 'APIROOT' end else // Make sure the URL endpoint is well formed. The intent is for the incoming URL endpoint to be forgiving but // internally it needs to match with the exact format of the endpoints used to uniquely identify each resource. Swap 'https' with '' in URLEndpoint Swap 'http' with '' in URLEndpoint HomeURL = HTTP_Services('GetHomeURL') Swap 'https' with '' in HomeURL Swap 'http' with '' in HomeURL APIURL = HTTP_Services('GetAPIRootURL', False$) If URLEndpoint[-1, 1] EQ '/' then URLEndpoint[-1, 1] = '' // Remove references to the Home or API URLs since these are not used when creating the resource endpoint // identifiers. Swap HomeURL with '' in URLEndpoint Swap APIURL with '' in URLEndpoint If URLEndpoint EQ '' then // An empty URL endpoint implies APIROOT. ResourceKeyID = 'APIROOT' end else // The URL endpoint needs to be walked one segment at a time to confirm that it matches the pattern of // an existing resource endpoint. Since the true URL endpoint might contains a resource ID, there needs // to be a check to see if the value of a given segment matches a defined resource or resource property // first. If not, then confirm there is a resource ID defined for this segment. ResourceList = HTTP_Resource_Manager_Services('GetResourceList') ResourceArray = SRP_Array('Rotate', ResourceList, @FM , @VM) ResourceEndpoints = ResourceArray<2> ResourceNames = ResourceArray<3> ResourceClasses = ResourceArray<5> // Since the stored resource endpoints contain user defined resource IDs, each of these need to be // converted to a generic '{ResourceID}' so simple comparison logic can work. For Each Class in ResourceClasses using @VM setting ClassPos If Class EQ 'RESOURCE_ID' then OrigResourceEndpoint = ResourceEndpoints<0, ClassPos> ResourceEndpoint = OrigResourceEndpoint ResourceName = ResourceNames<0, ClassPos> Swap ResourceName with '{ResourceID}' in ResourceEndpoint Swap OrigResourceEndpoint with ResourceEndpoint in ResourceEndpoints end Next Class Convert @Upper_Case to @Lower_Case in ResourceEndpoints Convert @Upper_Case to @Lower_Case in URLEndpoint // Remove any preceding "/" characters so the For Each will work // better. If URLEndpoint[1, 1] EQ '/' then URLEndpoint = URLEndpoint[2, 9999] MatchResourceKeyID = '' // Walk the URL endpoint provided and check for matches. Build the resource Key ID along the way. If a // given segment is unable to be matched to a defined resource endpoint, end the loop and clear the // resource Key ID. FinalSegment = False$ For Each Segment in URLEndpoint using '/' setting SegmentPos MatchResourceKeyID := '/' : Segment Locate MatchResourceKeyID in ResourceEndpoints using @VM setting URLPos then If Segment[1, 1] EQ '{' then ResourceKeyID := '/{resourceid}' end else ResourceKeyID := '/' : Segment end end else MatchResourceKeyID = Field(MatchResourceKeyID, '/', 1, SegmentPos) : '/{resourceid}' Locate MatchResourceKeyID in ResourceEndpoints using @VM setting URLPos then ResourceKeyID := '/{resourceid}' end else If AllowNew EQ True$ then ResourceKeyID := '/' : Segment IsNew = True$ end else FinalSegment = True$ end end end While (ResourceKeyIDFound EQ True$) AND (FinalSegment EQ False$) Next Segment end end If ResourceKeyIDFound EQ True$ then If IsNew NE True$ then Memory_Services('SetValue', ServiceKeyID, ResourceKeyID, CacheName$) end end else ResourceKeyID = '' end end Response = ResourceKeyID end service //---------------------------------------------------------------------------------------------------------------------- // GetResource // // URLEndpoint - URL endpoint for the resource. If empty, the APIROOT resource will be returned. - [Optional] // // Returns an array of resource property names and values for the indicated URL endpoint. //---------------------------------------------------------------------------------------------------------------------- Service GetResource(URLEndpoint) URLEndpointKeyID = HTTP_Resource_Manager_Services('GetEndpointResourceKeyID', URLEndpoint) ServiceKeyID := '*' : URLEndpointKeyID Resource = Memory_Services('GetValue', ServiceKeyID, True$, CacheTTL$, CacheName$) If Resource EQ '' then ResourceList = HTTP_Resource_Manager_Services('GetResourceList') If Error_Services('NoError') then // The URL endpoint needs to be walked one segment at a time to confirm that it matches the pattern of // an existing resource endpoint. Since the true URL endpoint might contains a resource ID, there needs // to be a check to see if the value of a given segment matches a defined resource or resource property // first. If not, then confirm there is a resource ID defined for this segment. ResourceArray = SRP_Array('Rotate', ResourceList, @FM , @VM) ResourceEndpoints = ResourceArray<2> ResourceNames = ResourceArray<3> ResourceClasses = ResourceArray<5> // Since the stored resource endpoints contain user defined resource IDs, each of these need to be // converted to a generic '{ResourceID}' so simple comparison logic can work. For Each Class in ResourceClasses using @VM setting ClassPos If Class EQ 'RESOURCE_ID' then OrigResourceEndpoint = ResourceEndpoints<0, ClassPos> ResourceEndpoint = OrigResourceEndpoint ResourceName = ResourceNames<0, ClassPos> Swap ResourceName with '{ResourceID}' in ResourceEndpoint Swap OrigResourceEndpoint with ResourceEndpoint in ResourceEndpoints end Next Class Convert @Upper_Case to @Lower_Case in ResourceEndpoints MatchURLEndpointKeyID = URLEndpointKeyID Convert @Upper_Case to @Lower_Case in MatchURLEndpointKeyID Locate MatchURLEndpointKeyID in ResourceEndpoints using @VM setting ResourcePos then ResourceArray = ResourceList Convert @VM to @FM in ResourceArray NameProperty = ResourceArray<3> ClassProperty = ResourceArray<5> OtherProperties = ResourceArray<19> Convert @SVM to @FM in OtherProperties Convert @TM to @VM in OtherProperties ResourceList = 'NAME' : @VM : NameProperty : @FM : 'CLASS' : @VM : ClassProperty : @FM : OtherProperties Resource = SRP_Array('Rotate', ResourceList, @FM, @VM) Memory_Services('SetValue', ServiceKeyID, Resource, CacheName$) end end end Response = Resource end service //---------------------------------------------------------------------------------------------------------------------- // SetResource // // URLEndpoint - URL endpoint for the resource. If empty, the APIROOT resource will be returned. - [Optional] // PropertyArray - An @FM/@VM delimited array of resource property names and values for the resource. - [Required] // // Updates the array of resource property names and values for the indicated URL endpoint. Note, this replaces the // existing property names and values. Other services should be used to update an existing array and then call this // service when the array is updated. The PropertyArray must have at least one property name and value. //---------------------------------------------------------------------------------------------------------------------- Service SetResource(URLEndpoint, PropertyArray) If PropertyArray NE '' then URLEndpointKeyID = HTTP_Resource_Manager_Services('GetEndpointResourceKeyID', URLEndpoint, True$) ResourceList = HTTP_Resource_Manager_Services('GetResourceList') If Error_Services('NoError') then ResourceArray = SRP_Array('Rotate', ResourceList, @FM , @VM) MatchURLEndpointKeyID = URLEndpointKeyID Convert @Upper_Case to @Lower_Case in MatchURLEndpointKeyID ResourceEndpoints = ResourceArray<2> Convert @Upper_Case to @Lower_Case in ResourceEndpoints Locate MatchURLEndpointKeyID in ResourceEndpoints By 'AL' using @VM setting ResourcePos then // This is an existing resource so update it. Resource = ResourceList Convert @VM to @FM in Resource end else // This is a new resource. ResourceList = Insert(ResourceList, ResourcePos, 0, 0, '') GoSub GetResourceTemplate end // Update the resource array with the property array data. PropertyNames = PropertyArray<1> PropertyValues = PropertyArray<2> Locate 'NAME' in PropertyNames using @VM setting PropertyPos then Name = PropertyValues<0, PropertyPos> PropertyNames = Delete(PropertyNames, 0, PropertyPos, 0) PropertyValues = Delete(PropertyValues, 0, PropertyPos, 0) end else // If no Name is provided, use the final segment of the URL endpoint. Name = URLEndpointKeyID[-1, 'B/'] PropertyArray = Insert(PropertyArray, 1, 1, 0, 'NAME') PropertyArray = Insert(PropertyArray, 2, 1, 0, Name) end Locate 'CLASS' in PropertyNames using @VM setting PropertyPos then Class = PropertyValues<0, PropertyPos> Locate Class in 'RESOURCE,RESOURCE_ID,PROPERTY' using ',' setting ClassPos else Class = 'RESOURCE' PropertyArray<2, PropertyPos> = Class end PropertyNames = Delete(PropertyNames, 0, PropertyPos, 0) PropertyValues = Delete(PropertyValues, 0, PropertyPos, 0) end else // All resources are required to have a class. If none is specified then set it to 'RESOURCE'. Class = 'RESOURCE' PropertyArray = Insert(PropertyArray, 1, 1, 0, 'CLASS') PropertyArray = Insert(PropertyArray, 2, 1, 0, Class) end Memory_Services('SetValue', ServiceModule : '*GetResource*' : URLEndpointKeyID, PropertyArray, CacheName$) Resource<03> = Name Resource<05> = Class OtherProperties = PropertyNames : @FM : PropertyValues OtherPropertiesList = SRP_Array('Rotate', OtherProperties, @FM, @VM) Convert @VM to @TM in OtherPropertiesList Convert @FM to @SVM in OtherPropertiesList Resource<19> = OtherPropertiesList Convert @FM to @VM in Resource ResourceList = Resource ResourcesKeyID = HTTP_Services('GetLocalAppKeyID', ResourcesKeyID$) Database_Services('WriteDataRow', SetupTable$, ResourcesKeyID, ResourceList, True$, False$, True$) end end else Error_Services('Add', 'PropertyArray was missing from the ' : Service : ' service.') end end service //---------------------------------------------------------------------------------------------------------------------- // IsValidEndpoint // // URLEndpoint - URL endpoint for the resource. If empty, a False value will be returned. - [Required] // // Returns a Boolean flag indicating if the indicated URL endpoint exists. //---------------------------------------------------------------------------------------------------------------------- Service IsValidEndpoint(URLEndpoint) IsValidEndpoint = False$ ; // Assume False for now. If URLEndpoint NE '' then URLEndpointKeyID = HTTP_Resource_Manager_Services('GetEndpointResourceKeyID', URLEndpoint) If URLEndpointKeyID NE '' then IsValidEndpoint = True$ end else Error_Services('Add', 'URLEndpoint argument was missing in the ' : Service : ' service.') end Response = IsValidEndpoint end service //---------------------------------------------------------------------------------------------------------------------- // GetResourceList // // Returns the resource list for the current application. //---------------------------------------------------------------------------------------------------------------------- Service GetResourceList() ResourceList = '' ResourcesKeyID = HTTP_Services('GetLocalAppKeyID', ResourcesKeyID$) If Error_Services('NoError') then ResourceList = Database_Services('ReadDataRow', SetupTable$, ResourcesKeyID, True$, CacheTTL$, False$) end Response = ResourceList end service //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Internal GoSubs //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //---------------------------------------------------------------------------------------------------------------------- // GetResourceTemplate // // Returns a template for a new resource. Most of this information is needed for the NDW_HTTP_FRAMEWORK_SETUP form's // Tree control. //---------------------------------------------------------------------------------------------------------------------- GetResourceTemplate: Resource = '' Resource<01> = DCount(URLEndpointKeyID, '/') Resource<02> = URLEndpointKeyID Resource<03> = URLEndpointKeyID[-1, 'B/'] Resource<04> = 'Text' : @SVM Resource<05> = '' Resource<06> = '' Resource<07> = 'RGB{68, 68, 68}' : @SVM : 'White' Resource<08> = 'Segoe UI' : @SVM : '-12' : @SVM : '400' : @SVM : '0' : @SVM : '0' : @SVM : '0' : @SVM : '1' : @SVM : '0' : @SVM : '0' : @SVM : '0' : @SVM : '0' : @SVM : '0' Resource<09> = 'Left' : @SVM : 'Center' Resource<10> = 'N' Resource<11> = 'None' Resource<12> = 1 Resource<13> = 24 Resource<14> = 0 Resource<15> = 1 Resource<16> = 'None' Resource<17> = 'Left' Resource<18> = 13 : @SVM : 13 return