diff --git a/dist/oe125/Diagnostic.pl b/dist/oe125/Diagnostic.pl new file mode 100644 index 00000000..339a3138 Binary files /dev/null and b/dist/oe125/Diagnostic.pl differ diff --git a/dist/oe125/Spark.pl b/dist/oe125/Spark.pl new file mode 100644 index 00000000..fc95cdf3 Binary files /dev/null and b/dist/oe125/Spark.pl differ diff --git a/src/OpenEdge/BusinessLogic/BusinessEntity.cls b/src/OpenEdge/BusinessLogic/BusinessEntity.cls index 1bed5c68..b03049db 100644 --- a/src/OpenEdge/BusinessLogic/BusinessEntity.cls +++ b/src/OpenEdge/BusinessLogic/BusinessEntity.cls @@ -1,22 +1,35 @@ /********************************************************************************************** -* Copyright (C) 2014-2019 by Progress Software Corporation. All rights reserved. * +* Copyright (C) 2014-2019, 2021 by Progress Software Corporation. All rights reserved. * * Prior versions of this work may contain portions contributed by participants of Possenet.* ***********************************************************************************************/ /*--------------------------------------------------------------------------------------------- File : BusinessEntity.cls - Syntax : + Syntax : Author(s) : Maura Regan Created : Tues Mar 04 11:50:26 EST 2014 - Notes : Abstract class that contains generic methods performing CRUD operations + Notes : Abstract class that contains generic methods performing CRUD operations for Busines Entities --------------------------------------------------------------------------------------------*/ - BLOCK-LEVEL ON ERROR UNDO, THROW. -USING OpenEdge.BusinessLogic.UpdateModeEnum. +using Ccs.BusinessLogic.IGetDataRequest. +using Ccs.BusinessLogic.IGetDataResponse. +using Ccs.BusinessLogic.IGetResultCountResponse. +using Ccs.BusinessLogic.IGetTableResultCountResponse. +using Ccs.BusinessLogic.JoinEnum. +using OpenEdge.BusinessLogic.GetDataRequest. +using OpenEdge.BusinessLogic.GetDataResponse. +using OpenEdge.BusinessLogic.GetDataTableRequest. +using OpenEdge.BusinessLogic.GetDataTableResponse. +using OpenEdge.BusinessLogic.GetResultCountResponse. +using OpenEdge.BusinessLogic.GetTableResultCountResponse. +using OpenEdge.BusinessLogic.Query.QueryBuilder. +using OpenEdge.BusinessLogic.UpdateModeEnum. +using OpenEdge.Core.Assert. +using OpenEdge.Core.AssertionFailedError. CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: - + /* The dataset currently in use */ DEFINE PROTECTED PROPERTY ProDataSet AS HANDLE NO-UNDO GET. SET. @@ -28,11 +41,11 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: SET (INPUT hDataSrc AS HANDLE, INPUT idx AS INTEGER): IF idx > ProDataSet:NUM-BUFFERS THEN UNDO, THROW NEW Progress.Lang.AppError("ProDataSource's EXTENT value must equal number of buffers in DataSet.", 0). - ProDataSource[idx] = hDataSrc. + ProDataSource[idx] = hDataSrc. END SET. - /* Stores skip-list entry for each table in dataset. Should be in table order as defined in DataSet. - * Each skip-list entry is a comma-separated list of field names, to be ignored in create stmt. */ + /* Stores skip-list entry for each table in dataset. Should be in table order as defined in DataSet. + * Each skip-list entry is a comma-separated list of field names, to be ignored in create stmt. */ DEFINE PROTECTED PROPERTY SkipList AS CHAR NO-UNDO EXTENT GET. SET (INPUT cSkip AS CHAR, INPUT idx AS INTEGER): @@ -44,28 +57,27 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /* BusinessEntity now supports 4 modes on how updates are performed: * TransactionalSubmit - where entire changeset is processed as single transaction. - * BulkSubmit - where each row change is processed as a separate transaction. - * CUD - client makes individual call to create, update or delete operation. + * BulkSubmit - where each row change is processed as a separate transaction. + * CUD - client makes individual call to create, update or delete operation. * CUD_NOBI - client makes individual call to create, update or delete operation, but * does not send/receive any BI data as part of request/response. * This mode must support a single transaction, since there is no bi data sent - */ - + */ DEFINE PRIVATE PROPERTY UpdateMode AS UpdateModeEnum NO-UNDO GET. - SET (INPUT mode AS UpdateModeEnum): - UpdateMode = mode. - END SET. + SET. + /* Constructor + + @param handle The prodataset used by this BE */ CONSTRUCTOR PROTECTED BusinessEntity(INPUT hDS AS HANDLE): ProDataSet = hDS. UpdateMode = UpdateModeEnum:UNDEFINED. END CONSTRUCTOR. - /*-------------------------------------------------------------------------------------------------------- - Purpose: Generic routine to read data for the dataset. - Notes: + Purpose: Generic routine to read data for the dataset. + Notes: ---------------------------------------------------------------------------------------------------------*/ METHOD PROTECTED VOID ReadData(INPUT cFilter AS CHARACTER): DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO. @@ -81,27 +93,24 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: SetFillWhereString(cWhere, ProDataSet:GET-TOP-BUFFER(1)). /* Finally, call FILL() for dataset */ - ProDataSet:FILL(). + ProDataSet:FILL(). - FINALLY: + FINALLY: THIS-OBJECT:DetachDataSources(). - END FINALLY. - - END METHOD. - + END FINALLY. + END METHOD. /*-------------------------------------------------------------------------------------------------------- - Purpose: Generic routine to read data for the dataset with no filter specified. - Notes: + Purpose: Generic routine to read data for the dataset with no filter specified. + Notes: ---------------------------------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID ReadData(): + METHOD PROTECTED VOID ReadData(): THIS-OBJECT:ReadData(""). END METHOD. - /*-------------------------------------------------------------------------------------------------------- - Purpose: Generic routine to read data for the dataset. - Notes: + Purpose: Generic routine to read data for the dataset. + Notes: ---------------------------------------------------------------------------------------------------------*/ METHOD PROTECTED VOID ReadData(INPUT cFilter AS CHARACTER EXTENT): DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO EXTENT . @@ -109,7 +118,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: THIS-OBJECT:AttachDataSources(). /* Get rid of any existing data in DataSet */ - EmptyDataSet(). + EmptyDataSet(). cWhere = AdjustWheres(cFilter). @@ -117,19 +126,19 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: SetFillWhereStrings(cWhere). /* Finally, call FILL() for dataset */ - ProDataSet:FILL(). + ProDataSet:FILL(). - FINALLY: + FINALLY: THIS-OBJECT:DetachDataSources(). - END FINALLY. + END FINALLY. - END METHOD. + END METHOD. /*-------------------------------------------------------------------------------------------------------- - Purpose: Generic routine to read data for a table within a dataset. - Notes: + Purpose: Generic routine to read data for a table within a dataset. + Notes: ---------------------------------------------------------------------------------------------------------*/ METHOD PROTECTED VOID ReadData(INPUT cFilter AS CHARACTER, INPUT hBuffer AS HANDLE): DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO. @@ -145,26 +154,26 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: SetFillWhereString(cWhere, hBuffer). /* Finally, call FILL() for buffer's table */ - hBuffer:FILL(). + hBuffer:FILL(). - FINALLY: + FINALLY: THIS-OBJECT:DetachDataSources(). - END FINALLY. + END FINALLY. - END METHOD. + END METHOD. /*-------------------------------------------------------------------------------------------------------- - Purpose: Generic routine to read data for a table within a dataset with no filter specified. - Notes: + Purpose: Generic routine to read data for a table within a dataset with no filter specified. + Notes: ---------------------------------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID ReadData(INPUT hBuffer AS HANDLE): + METHOD PROTECTED VOID ReadData(INPUT hBuffer AS HANDLE): THIS-OBJECT:ReadData("", hBuffer). END METHOD. /*-------------------------------------------------------------------------------------------------------- - Purpose: Generic routine to empty from the dataset. - Notes: + Purpose: Generic routine to empty from the dataset. + Notes: ---------------------------------------------------------------------------------------------------------*/ METHOD PRIVATE VOID EmptyDataSet(): @@ -179,12 +188,13 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END METHOD. /*------------------------------------------------------------------------------ - Purpose: Create one or more new records - Notes: - ------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID CreateData(INPUT-OUTPUT DATASET-HANDLE hDataSet): + Purpose: Create one or more new records + Notes: + ------------------------------------------------------------------------------*/ + METHOD PROTECTED VOID CreateData(INPUT-OUTPUT DATASET-HANDLE hDataSet): DEFINE VARIABLE hPDS AS HANDLE NO-UNDO. - DEFINE VARIABLE saveUndos AS LOGICAL EXTENT NO-UNDO. + {&_proparse_ prolint-nowarn(varusage)} + DEFINE VARIABLE saveUndos AS LOGICAL EXTENT NO-UNDO. /* if the Create-, Update- or DeleteData operation was called BY-REFERENCE we will have a different dataset than the one in the property. We want to work @@ -198,7 +208,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: THIS-OBJECT:AttachDataSources(). - DO TRANSACTION ON ERROR UNDO, THROW: + DO TRANSACTION ON ERROR UNDO, THROW: // ProcessBIData() determines if BI data was sent as part of request. // If not sent, it adds it. BI data is required when calling SAVE-ROW-CHANGES() IF ProcessBIData(ROW-CREATED) EQ TRUE THEN @@ -223,14 +233,15 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: if valid-handle(hPDS) then assign this-object:ProDataSet = hPDS. END FINALLY. - END METHOD. + END METHOD. /*------------------------------------------------------------------------------ - Purpose: Update one or more new records - Notes: - ------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID UpdateData(INPUT-OUTPUT DATASET-HANDLE hDataSet): + Purpose: Update one or more new records + Notes: + ------------------------------------------------------------------------------*/ + METHOD PROTECTED VOID UpdateData(INPUT-OUTPUT DATASET-HANDLE hDataSet): DEFINE VARIABLE hPDS AS HANDLE NO-UNDO. + {&_proparse_ prolint-nowarn(varusage)} DEFINE VARIABLE saveUndos AS LOGICAL EXTENT NO-UNDO. /* if the Create-, Update- or DeleteData operation was called BY-REFERENCE @@ -245,7 +256,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: THIS-OBJECT:AttachDataSources(). - DO TRANSACTION ON ERROR UNDO, THROW: + DO TRANSACTION ON ERROR UNDO, THROW: // ProcessBIData() determines if BI data was sent as part of request. // If not sent, it adds it. BI data is required when calling SAVE-ROW-CHANGES() IF ProcessBIData(ROW-MODIFIED) EQ TRUE THEN @@ -260,7 +271,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: ProcessTransactionalError(e). END. - FINALLY: + FINALLY: THIS-OBJECT:DetachDataSources(). // Reset undos to orig values @@ -269,16 +280,17 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /* reset if we used a different PDS */ if valid-handle(hPDS) then assign this-object:ProDataSet = hPDS. - END FINALLY. + END FINALLY. END METHOD. /*------------------------------------------------------------------------------ - Purpose: Delete one or more records - Notes: + Purpose: Delete one or more records + Notes: ------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID DeleteData(INPUT-OUTPUT DATASET-HANDLE hDataSet): + METHOD PROTECTED VOID DeleteData(INPUT-OUTPUT DATASET-HANDLE hDataSet): DEFINE VARIABLE hPDS AS HANDLE NO-UNDO. + {&_proparse_ prolint-nowarn(varusage)} DEFINE VARIABLE saveUndos AS LOGICAL EXTENT NO-UNDO. /* if the Create-, Update- or DeleteData operation was called BY-REFERENCE @@ -308,7 +320,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: ProcessTransactionalError(e). END. - FINALLY: + FINALLY: THIS-OBJECT:DetachDataSources(). // Reset undos to orig values @@ -317,23 +329,23 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /* reset if we used a different PDS */ if valid-handle(hPDS) then assign this-object:ProDataSet = hPDS. - END FINALLY. + END FINALLY. END METHOD. /*--------------------------------------------------------------------------------------- - Purpose: Checks if ProDataSet was sent with bi data, returning TRUE or FALSE. + Purpose: Checks if ProDataSet was sent with bi data, returning TRUE or FALSE. Notes: It validates that all temp-tables are consistent in their usage of before-image data, meaning either they all have it or don't. - If there is any inconsistency, an exception is thrown. + If there is any inconsistency, an exception is thrown. Also, if no bi data was sent, then it's created based on iRowState param. - Before-Image data is a requirement in order to call SAVE-ROW-CHANGES() + Before-Image data is a requirement in order to call SAVE-ROW-CHANGES() ----------------------------------------------------------------------------------------*/ METHOD PRIVATE LOGICAL ProcessBIData(INPUT iRowState AS INTEGER): - DEFINE VAR iIndex AS INTEGER NO-UNDO. + DEFINE VAR iIndex AS INTEGER NO-UNDO. DEFINE VAR hBuffer AS HANDLE NO-UNDO. DEFINE VAR hBeforeBuffer AS HANDLE NO-UNDO. DEFINE VAR lAvailable AS LOGICAL NO-UNDO. @@ -350,7 +362,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: hBeforeBuffer = hBuffer:BEFORE-BUFFER. IF hBeforeBuffer EQ ? THEN - UNDO, THROW NEW + UNDO, THROW NEW Progress.Lang.AppError("To use BusinessEntity CUD functionality, temp-tables must be specified with BEFORE-TABLE.", 0). // Check for before-image rows. Skip over tables that have no rows at all @@ -382,12 +394,12 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: DO WHILE lAvailable: /* Note: If a before-table row already exists, MARK-ROW-STATE() is a no-op */ lReturnValue = hBuffer:MARK-ROW-STATE(iRowState) - // NO-ERROR is required, otherwise this statement 'fails' like a FIND, aka not + // NO-ERROR is required, otherwise this statement 'fails' like a FIND, aka not // properly/well no-error. - IF NOT lReturnValue THEN - UNDO, THROW NEW Progress.Lang.AppError('Unable to ' + GetRowState(iRowState) + + IF NOT lReturnValue THEN + UNDO, THROW NEW Progress.Lang.AppError('Unable to ' + GetRowState(iRowState) + ' record. Cannot create corresponding row in BEFORE-TABLE.', 0). lAvailable = hAfterQuery[iIndex]:GET-NEXT() @@ -403,20 +415,21 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: IF VALID-HANDLE(hAfterQuery[iIndex]) THEN DELETE OBJECT hAfterQuery[iIndex]. END. + {&_proparse_ prolint-nowarn(returnfinally)} RETURN lHasBIData. - END FINALLY. + END FINALLY. END METHOD. /*---------------------------------------------------------------------------------------- Purpose: Saves/Submits one or more changed rows (creates, updates, and/or deletes) where each changed row is processed as a separate transaction. SaveRows() is similar to Submit(), but acts as a bulk submit, with separate - row transactions. - Notes: + row transactions. + Notes: - -----------------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID SaveRows(INPUT-OUTPUT DATASET-HANDLE hDataSet): + -----------------------------------------------------------------------------------------*/ + METHOD PROTECTED VOID SaveRows(INPUT-OUTPUT DATASET-HANDLE hDataSet): DEFINE VARIABLE hPDS AS HANDLE NO-UNDO. /* If the Create-, Update- or DeleteData operation was called BY-REFERENCE @@ -424,42 +437,43 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: on the one was sent, not the one that's there :) */ IF THIS-OBJECT:ProDataSet NE hDataSet THEN ASSIGN hPDS = THIS-OBJECT:ProDataSet - THIS-OBJECT:ProDataSet = hDataSet:HANDLE. + THIS-OBJECT:ProDataSet = hDataSet:HANDLE. THIS-OBJECT:AttachDataSources(). UpdateMode = UpdateModeEnum:BULK_SUBMIT. /* Do deletes first, next creates, and finally modifies */ - THIS-OBJECT:CommitData(ROW-DELETED). + THIS-OBJECT:CommitData(ROW-DELETED). THIS-OBJECT:CommitData(ROW-CREATED). - THIS-OBJECT:CommitData(ROW-MODIFIED). + THIS-OBJECT:CommitData(ROW-MODIFIED). - FINALLY: + FINALLY: THIS-OBJECT:DetachDataSources(). /* reset if we used a different PDS */ if valid-handle(hPDS) then assign this-object:ProDataSet = hPDS. - END FINALLY. + END FINALLY. END METHOD. /*------------------------------------------------------------------------------ Purpose: Submits one or more changed records (creates, updates, and/or deletes) - where the changed record set is processed as a single transaction. - Notes: - ------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID Submit(INPUT-OUTPUT DATASET-HANDLE hDataSet): + where the changed record set is processed as a single transaction. + Notes: + ------------------------------------------------------------------------------*/ + METHOD PROTECTED VOID Submit(INPUT-OUTPUT DATASET-HANDLE hDataSet): DEFINE VARIABLE hPDS AS HANDLE NO-UNDO. - DEFINE VARIABLE saveUndos AS LOGICAL EXTENT NO-UNDO. + {&_proparse_ prolint-nowarn(varusage)} + DEFINE VARIABLE saveUndos AS LOGICAL EXTENT NO-UNDO. /* If the Create-, Update- or DeleteData operation was called BY-REFERENCE we will have a different dataset than the one in the property. We want to work on the one was sent, not the one that's there :) */ IF THIS-OBJECT:ProDataSet NE hDataSet THEN ASSIGN hPDS = THIS-OBJECT:ProDataSet - THIS-OBJECT:ProDataSet = hDataSet:HANDLE. + THIS-OBJECT:ProDataSet = hDataSet:HANDLE. // UNDO attrs needs to be false to preserve ERROR and REJECTED SetUndos(saveUndos, FALSE). @@ -470,9 +484,9 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: DO TRANSACTION ON ERROR UNDO, THROW: /* Do deletes first, next creates, and finally modifies */ - THIS-OBJECT:CommitData(ROW-DELETED). + THIS-OBJECT:CommitData(ROW-DELETED). THIS-OBJECT:CommitData(ROW-CREATED). - THIS-OBJECT:CommitData(ROW-MODIFIED). + THIS-OBJECT:CommitData(ROW-MODIFIED). END. CATCH e AS CLASS Progress.Lang.Error: @@ -493,20 +507,19 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END METHOD. /*----------------------------------------------------------------------------------------- - Purpose: ProcessTransactionalError() is called when we are processing a single + Purpose: ProcessTransactionalError() is called when we are processing a single transaction, and an error occurs for one of the row changes. When this occurs, all rows should have REJECTED set to TRUE, - and the error message should be set on the offending row. - Notes: If there is no bi data being sent between client and server (CUD_NOBI), + and the error message should be set on the offending row. + Notes: If there is no bi data being sent between client and server (CUD_NOBI), then we throw the error (resulting in http 500 error), - and we also must back out all changes. - ------------------------------------------------------------------------------------------*/ - METHOD PROTECTED VOID ProcessTransactionalError(INPUT err AS Progress.Lang.Error): + and we also must back out all changes. + ------------------------------------------------------------------------------------------*/ + METHOD PROTECTED VOID ProcessTransactionalError(INPUT err AS Progress.Lang.Error): - DEFINE VAR iIndex AS INTEGER NO-UNDO. + DEFINE VAR iIndex AS INTEGER NO-UNDO. DEFINE VAR hBuffer AS HANDLE NO-UNDO. DEFINE VAR hBeforeBuffer AS HANDLE NO-UNDO. - DEFINE VAR lReturnValue AS LOGICAL NO-UNDO. DEFINE VAR lAvailable AS LOGICAL NO-UNDO. DEFINE VAR hBeforeQuery AS HANDLE EXTENT NO-UNDO. DEFINE VAR cErrMessage AS CHAR NO-UNDO. @@ -533,8 +546,8 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: CREATE QUERY hBeforeQuery[iIndex]. hBeforeQuery[iIndex]:SET-BUFFERS(hBeforeBuffer). - lReturnValue = hBeforeQuery[iIndex]:QUERY-PREPARE(SUBSTITUTE('FOR EACH &1', hBeforeBuffer:NAME)). - lReturnValue = hBeforeQuery[iIndex]:QUERY-OPEN(). + hBeforeQuery[iIndex]:QUERY-PREPARE(SUBSTITUTE('FOR EACH &1', hBeforeBuffer:NAME)). + hBeforeQuery[iIndex]:QUERY-OPEN(). lAvailable = hBeforeQuery[iIndex]:GET-FIRST(). DO WHILE lAvailable: @@ -542,8 +555,8 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: IF hBeforeBuffer:ERROR EQ TRUE THEN DO: hBeforeBuffer:ERROR-STRING = GetRowErrorMessage(hBeforeBuffer, cErrMessage). END. - lAvailable = hBeforeQuery[iIndex]:GET-NEXT(). - END. + lAvailable = hBeforeQuery[iIndex]:GET-NEXT(). + END. END. END. END. @@ -560,14 +573,14 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: IF VALID-HANDLE(hBeforeQuery[iIndex]) THEN DELETE OBJECT hBeforeQuery[iIndex]. END. - END FINALLY. + END FINALLY. END METHOD. /*------------------------------------------------------------------------------------------------ Purpose: If errMessage param is not set, create an error message for the current bi row - Notes: - -------------------------------------------------------------------------------------------------*/ + Notes: + -------------------------------------------------------------------------------------------------*/ METHOD PRIVATE CHAR GetRowErrorMessage(INPUT hBeforeBuffer AS HANDLE, INPUT errMessage AS CHAR): // Want to ensure that we always return an error message when a create, update, or delete record fails // when calling SAVE-ROW-CHANGES @@ -582,17 +595,17 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /*--------------------------------------------------------------------------------------- - Purpose: Generic routine to save or commit data for a dataset. - Notes: + Purpose: Generic routine to save or commit data for a dataset. + Notes: ----------------------------------------------------------------------------------------*/ METHOD PROTECTED VOID CommitData(INPUT iRowState AS INTEGER): - DEFINE VAR iIndex AS INTEGER NO-UNDO. + DEFINE VAR iIndex AS INTEGER NO-UNDO. DEFINE VAR iTopBufferCnt AS INTEGER NO-UNDO. DEFINE VAR hBuffer AS HANDLE NO-UNDO. DEFINE VAR cSkip AS CHAR NO-UNDO. - iTopBufferCnt = ProDataSet:NUM-TOP-BUFFERS. + iTopBufferCnt = ProDataSet:NUM-TOP-BUFFERS. DO iIndex = 1 TO iTopBufferCnt: hBuffer = ProDataSet:GET-TOP-BUFFER (iIndex). @@ -600,7 +613,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: CASE iRowState: WHEN ROW-CREATED THEN DO: /* Commit creates for parent. First get index into entire list of buffers */ - cSkip = GetSkipListEntry(GetBufferIndex(hBuffer)). + cSkip = GetSkipListEntry(GetBufferIndex(hBuffer)). CommitRows(hBuffer, cSkip, iRowState). /* Now commit creates for any child buffer */ @@ -620,9 +633,9 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /*------------------------------------------------------------------------------ - Purpose: Attach Data Sources to DataSet's buffers - Notes: - ------------------------------------------------------------------------------*/ + Purpose: Attach Data Sources to DataSet's buffers + Notes: + ------------------------------------------------------------------------------*/ METHOD PROTECTED VOID AttachDataSources(): ValidateDataSources(). @@ -641,9 +654,9 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END METHOD. /*------------------------------------------------------------------------------ - Purpose: Attach Data Sources to DataSet's buffers - Notes: - ------------------------------------------------------------------------------*/ + Purpose: Attach Data Sources to DataSet's buffers + Notes: + ------------------------------------------------------------------------------*/ METHOD PROTECTED VOID AttachDataSources(INPUT cFieldList AS CHAR EXTENT): DEFINE VARIABLE hCurBuffer AS HANDLE NO-UNDO. @@ -664,9 +677,9 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /*------------------------------------------------------------------------------ - Purpose: Detach Data Sources from DataSet's buffers - Notes: - ------------------------------------------------------------------------------*/ + Purpose: Detach Data Sources from DataSet's buffers + Notes: + ------------------------------------------------------------------------------*/ METHOD PRIVATE VOID DetachDataSources(): DEFINE VARIABLE hCurBuffer AS HANDLE NO-UNDO. @@ -683,11 +696,11 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END METHOD. /*----------------------------------------------------------------------------------- - Purpose: Must verify that ProDataSource contains a data source entry for each + Purpose: Must verify that ProDataSource contains a data source entry for each table specified in dataset. Also verify that they are valid handles. - A data source entry can be set to UNKNOWN as well, as allow for that. - Notes: - ------------------------------------------------------------------------------------*/ + A data source entry can be set to UNKNOWN as well, as allow for that. + Notes: + ------------------------------------------------------------------------------------*/ METHOD PROTECTED VOID ValidateDataSources(): DEFINE VARIABLE iIndex AS INTEGER NO-UNDO. @@ -697,16 +710,16 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: REPEAT iIndex = 1 TO ProDataSet:NUM-BUFFERS: IF ProDataSource[iIndex] NE ? AND NOT VALID-HANDLE(ProDataSource[iIndex]) THEN - UNDO, THROW NEW Progress.Lang.AppError("ProDataSource extent value must be a valid DataSource Handle.", 0). + UNDO, THROW NEW Progress.Lang.AppError("ProDataSource extent value must be a valid DataSource Handle.", 0). END. END METHOD. /*------------------------------------------------------------------------------------------------- - Purpose: Must verify that cFieldList's EXTENT value is same as number of buffers in dataset - Notes: - --------------------------------------------------------------------------------------------------*/ + Purpose: Must verify that cFieldList's EXTENT value is same as number of buffers in dataset + Notes: + --------------------------------------------------------------------------------------------------*/ METHOD PROTECTED VOID ValidateFieldLists(INPUT-OUTPUT cFieldList AS CHAR EXTENT): DEFINE VARIABLE iIndex AS INTEGER NO-UNDO. @@ -714,17 +727,17 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: IF ProDataSet:NUM-BUFFERS NE EXTENT(cFieldList) THEN UNDO, THROW NEW Progress.Lang.AppError("cFieldList's EXTENT value must equal number of buffers in DataSet.", 0). - REPEAT iIndex = 1 TO ProDataSet:NUM-BUFFERS: + REPEAT iIndex = 1 TO ProDataSet:NUM-BUFFERS: IF cFieldList[iIndex] EQ ? THEN - cFieldList[iIndex] = "". + cFieldList[iIndex] = "". END. END METHOD. /*----------------------------------------------------------------------------------------------- - Purpose: Returns the skip-list entry from array for specified buffer index. - Notes: Skip-list entries in array should be in temp-table order as defined in DataSet. + Purpose: Returns the skip-list entry from array for specified buffer index. + Notes: Skip-list entries in array should be in temp-table order as defined in DataSet. ------------------------------------------------------------------------------------------------*/ METHOD PRIVATE CHARACTER GetSkipListEntry (INPUT iBufferIndex AS INTEGER): @@ -735,7 +748,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: cSkip = SkipList[iBufferIndex]. END. - RETURN cSkip. + RETURN cSkip. END METHOD. @@ -743,8 +756,8 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /*------------------------------------------------------------------------------------------ Purpose: Walks through specifed dataset's data-relations to find child tables for - specified parent table. For each child table, it then calls commitRows(). - Notes: + specified parent table. For each child table, it then calls commitRows(). + Notes: ---------------------------------------------------------------------------------------------*/ METHOD PRIVATE VOID CommitChildBuffers(INPUT iParentBufferIndex AS INTEGER, INPUT iRowState AS INTEGER): @@ -756,7 +769,7 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: DEFINE VAR cSkip AS CHAR NO-UNDO. DEFINE VAR iChildBufferIndex AS INTEGER NO-UNDO. - hParentBuffer = ProDataSet:GET-BUFFER-HANDLE (iParentBufferIndex). + hParentBuffer = ProDataSet:GET-BUFFER-HANDLE (iParentBufferIndex). iRelationCnt = ProDataSet:NUM-RELATIONS. DO iIndex = 1 TO iRelationCnt: @@ -776,8 +789,8 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END METHOD. /*------------------------------------------------------------------------------------------ - Purpose: Walks through dataset's buffers and returns index for specified buffer. - Notes: + Purpose: Walks through dataset's buffers and returns index for specified buffer. + Notes: ---------------------------------------------------------------------------------------------*/ METHOD PRIVATE INTEGER GetBufferIndex(INPUT hBuffer AS HANDLE): @@ -799,8 +812,8 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /*------------------------------------------------------------------------------------------ - Purpose: Generic routine for creating/updating/deleting rows for specified table - Notes: + Purpose: Generic routine for creating/updating/deleting rows for specified table + Notes: ---------------------------------------------------------------------------------------------*/ METHOD PRIVATE VOID CommitRows(INPUT hBuffer AS HANDLE, INPUT cSkip AS CHAR, @@ -808,39 +821,37 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: DEFINE VARIABLE hBeforeBuffer AS HANDLE NO-UNDO. DEFINE VARIABLE hBeforeQuery AS HANDLE NO-UNDO. - DEFINE VARIABLE hAfterQuery AS HANDLE NO-UNDO. DEFINE VARIABLE lAvailable AS LOGICAL NO-UNDO. - DEFINE VARIABLE lReturnValue AS LOGICAL NO-UNDO. DEFINE VARIABLE cErrMessage AS CHAR NO-UNDO. DEFINE VARIABLE appError AS Progress.Lang.AppError NO-UNDO. hBeforeBuffer = hBuffer:BEFORE-BUFFER. IF hBeforeBuffer EQ ? THEN - UNDO, THROW NEW + UNDO, THROW NEW Progress.Lang.AppError("In order to use BusinessEntity CUD functionality, temp-tables must be specified with BEFORE-TABLE.", 0). CREATE QUERY hBeforeQuery. hBeforeQuery:SET-BUFFERS(hBeforeBuffer). /* We're only concerned with rows with iRowState */ - lReturnValue = hBeforeQuery:QUERY-PREPARE(SUBSTITUTE('FOR EACH &1 WHERE ROW-STATE(&1) EQ &2', hBeforeBuffer:NAME, iRowState)). - lReturnValue = hBeforeQuery:QUERY-OPEN(). + hBeforeQuery:QUERY-PREPARE(SUBSTITUTE('FOR EACH &1 WHERE ROW-STATE(&1) EQ &2', hBeforeBuffer:NAME, iRowState)). + hBeforeQuery:QUERY-OPEN(). lAvailable = hBeforeQuery:GET-FIRST(). - /* Once here, we have before-table rows, with first row in before-buffer. + /* Once here, we have before-table rows, with first row in before-buffer. * For CUD, CUD_NOBI, and TRANSACTIONAL_SUBMIT modes, all changes are processed in single transaction. - * + * * Note: For CUD_NOBI, we can only process changes in a single transaction, since there is no bi data - * exchanged between client and server. If error occurs, we throw exception (resulting in http 500 error), - * so we must back out all changes. + * exchanged between client and server. If error occurs, we throw exception (resulting in http 500 error), + * so we must back out all changes. */ IF UpdateMode NE UpdateModeEnum:BULK_SUBMIT THEN DO: DO WHILE lAvailable ON ERROR UNDO, THROW: hBeforeBuffer:SAVE-ROW-CHANGES(1, cSkip). lAvailable = hBeforeQuery:GET-NEXT(). END. - END. + END. /* For BULK_SUBMIT mode, each row change is processed as a separate transaction */ ELSE DO: DO WHILE lAvailable ON ERROR UNDO, LEAVE: @@ -875,8 +886,8 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: /*-------------------------------------------------------------------------------------------------- - Purpose: - Notes: + Purpose: + Notes: -------------------------------------------------------------------------------------------------*/ METHOD PRIVATE CHARACTER GetRowState(INPUT iRowState AS INTEGER): DEFINE VARIABLE cRowState AS CHARACTER NO-UNDO. @@ -895,12 +906,12 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END CASE. RETURN cRowState . - END METHOD. + END METHOD. /*-------------------------------------------------------------------------------------------------- - Purpose: - Notes: + Purpose: + Notes: -------------------------------------------------------------------------------------------------*/ METHOD PRIVATE CHARACTER AdjustWhere(INPUT cFilter AS CHARACTER): DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO. @@ -911,12 +922,12 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: cWhere = "WHERE " + cFilter. RETURN cWhere. - END METHOD. + END METHOD. /*-------------------------------------------------------------------------------------------------- - Purpose: - Notes: + Purpose: + Notes: -------------------------------------------------------------------------------------------------*/ METHOD PRIVATE CHARACTER EXTENT AdjustWheres(INPUT cFilter AS CHARACTER EXTENT): DEFINE VARIABLE cWhere AS CHARACTER NO-UNDO EXTENT. @@ -931,27 +942,34 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: END. RETURN cWhere. - END METHOD. - - + END METHOD. + /* Adds a FILL-WHERE-STRING to a datasource @param character The where (filter) string to apply to buffer @param handpe A buffer handle in the dataset */ - METHOD PROTECTED VOID SetFillWhereString(INPUT cWhere AS CHARACTER, + METHOD PROTECTED VOID SetFillWhereString(INPUT cWhere AS CHARACTER, INPUT hBuffer AS HANDLE): DEFINE VARIABLE hDataSrc AS HANDLE NO-UNDO. /* Set FILL-WHERE-STRING if applicable */ - IF cWhere NE "" AND cWhere NE ? AND VALID-HANDLE(hBuffer:DATA-SOURCE) THEN DO: - hDataSrc = hBuffer:DATA-SOURCE. - hDataSrc:FILL-WHERE-STRING = cWhere. - END. + IF cWhere NE "" AND cWhere NE ? AND VALID-HANDLE(hBuffer:DATA-SOURCE) THEN + // to combine data-relation fill-where-clause queries- "natural phrase" + "user query" = "merged" + // If there are relations between tables in a dataset, the AVM adds a filter clause to the FILL-WHERE-STRING + // when the datasources are attached. + // We want to preserve those where clauses since we don't want to + // a) return more records from the DB than match the combined where, especially with networked DBs + // b) hold more records in memory in the session than needed + assign hDataSrc = hBuffer:data-source + hDataSrc:fill-where-string = new QueryBuilder():MergeQueryStrings(cWhere, + JoinEnum:And, + hDataSrc:fill-where-string) + . END METHOD. /*-------------------------------------------------------------------------------------------------- - Purpose: - Notes: + Purpose: + Notes: -------------------------------------------------------------------------------------------------*/ METHOD PRIVATE VOID SetFillWhereStrings(INPUT cWhere AS CHARACTER EXTENT): @@ -962,22 +980,22 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: iBufferCnt = ProDataSet:NUM-BUFFERS. DO iIndex = 1 TO iBufferCnt: hBuffer = ProDataSet:GET-BUFFER-HANDLE(iIndex). - SetFillWhereString(cWhere[iIndex], hBuffer). + SetFillWhereString(cWhere[iIndex], hBuffer). END. - END METHOD. + END METHOD. /*-------------------------------------------------------------------------------------------------- Purpose: This method is used to set the UNDO attribute for all temp-tables in the ProDataSet. - With the addition of the the one transaction capability, the UNDO attributes need to + With the addition of the the one transaction capability, the UNDO attributes need to be set to FALSE to preserve the ERROR and REJECTED attributes, which are now sent to backend. - If the lUndos array parameter has been set appropriately, the method will use it to + If the lUndos array parameter has been set appropriately, the method will use it to restore the UNDO attributes (ignoring undoValue parameter). If lUndos has not been set, the method will set the UNDO attributes using undoValue, and return the original UNDO attr values in lUndos. - Notes: + Notes: ---------------------------------------------------------------------------------------------------*/ METHOD PRIVATE VOID SetUndos(INPUT-OUTPUT lUndos AS LOGICAL EXTENT, INPUT undoValue AS LOGICAL): @@ -999,17 +1017,164 @@ CLASS OpenEdge.BusinessLogic.BusinessEntity ABSTRACT: IF lSaveUndos EQ TRUE THEN DO: lUndos[iIndex] = hBuffer:TABLE-HANDLE:UNDO. - hBuffer:TABLE-HANDLE:UNDO = undoValue. + hBuffer:TABLE-HANDLE:UNDO = undoValue. END. /* Resetting them */ ELSE DO: - hBuffer:TABLE-HANDLE:UNDO = lUndos[iIndex]. - END. + hBuffer:TABLE-HANDLE:UNDO = lUndos[iIndex]. + END. END. - END METHOD. - + END METHOD. + /* Generic read data routine using a filter object (instead of strings). + This supports CCS-compliant BE's. + + @param IGetDataRequest The filter to apply. + @param IGetDataResponse Response object. */ + method protected IGetDataResponse ReadData(input pGetRequest as IGetDataRequest): + define variable tbl as handle no-undo. + define variable cnt as integer no-undo. + define variable idx as integer no-undo. + define variable loop as integer no-undo. + define variable bufferList as character no-undo. + define variable startRowid as rowid no-undo. + define variable qb as QueryBuilder no-undo. + define variable gdr as GetDataResponse no-undo. + define variable gdtr as GetDataTableResponse no-undo. + + Assert:NotNull(pGetRequest, 'Get request'). + + /* Get rid of any existing data in DataSet */ + EmptyDataSet(). + AttachDataSources(). + + assign cnt = ProDataSet:num-buffers + gdr = new GetDataResponse(cnt) + qb = new QueryBuilder() + // pre-fill the spaces: we need an entry for the name and the serialize-name + bufferList = fill(',':u, 2 * cnt - 1) + . + do loop = 1 to cnt: + assign tbl = ProDataSet:get-buffer-handle(loop) + entry(loop, bufferList) = tbl:serialize-name + entry(loop + cnt, bufferList) = tbl:name + // create a response for each buffer in the dataset + gdtr = new GetDataTableResponse(tbl:serialize-name) + gdr:TableResponses[loop] = gdtr + . + end. + + assign cnt = extent(pGetRequest:TableRequests). + do loop = 1 to cnt: + assign idx = lookup(pGetRequest:TableRequests[loop]:TableName, bufferList). + if idx gt cnt then + assign idx = idx - cnt. + else + // use the first top buffer if no name is specified + if idx eq 0 + and pGetRequest:TableRequests[loop]:TableName eq '':u + then + assign idx = lookup(ProDataSet:get-top-buffer(1):serialize-name, bufferList). + + // if the table is in this dataset, set the table request values to the input + if idx gt 0 then + do: + {&_proparse_ prolint-nowarn(overflow)} + assign tbl = ProDataSet:get-buffer-handle(idx) + tbl:batch-size = integer(pGetRequest:TableRequests[loop]:NumRecords) + startRowid = to-rowid(pGetRequest:TableRequests[loop]:PagingContext) + + // set the ROWID (not the input param value) to the PreviousPagingContext, since that's what is used + cast(gdr:TableResponses[idx], GetDataTableResponse):PreviousPagingContext = string(startRowid) + . + if not startRowid eq ? + and valid-handle(tbl:data-source) + then + assign tbl:data-source:restart-rowid = startRowid. + + // apply the filter: empty will return WHERE TRUE + SetFillWhereString(qb:BuildQueryString(pGetRequest:TableRequests[loop], yes, yes, tbl), + tbl). + end. + end. + + // get all the data + DoFill(ProDataSet). + + assign cnt = ProDataSet:num-buffers. + do loop = 1 to cnt: + assign tbl = ProDataSet:get-buffer-handle(loop). + if valid-handle(tbl:data-source) + and not tbl:data-source:next-rowid eq ? + then + assign cast(gdr:TableResponses[loop], GetDataTableResponse):NextPagingContext = string(tbl:data-source:next-rowid). + end. + + return gdr. + finally: + DetachDataSources(). + end finally. + end method. + + /* Performs the dataset or buffer FILL() operation. + + @param handle A valid DATASET or BUFFER handle. */ + method protected void DoFill(input pDataset as handle): + Assert:NotNull(pDataset, 'Handle'). + + if not pDataset:type eq 'dataset':u + and not pDataset:type eq 'buffer':u + then + undo, throw new AssertionFailedError(substitute('Handle must be a DATASET or BUFFER, not &1', pDataset:type)). + + pDataset:fill(). + end method. + + /* Returns the count of the total number of result records currently in a dataset. + + @param handle The dataset whose records to count + @return IGetResultCountResponse The IGetResultCountResponse instance for the request */ + method protected IGetResultCountResponse CountDatasetRecords(input pData as handle): + define variable loop as integer no-undo. + define variable cnt as integer no-undo. + define variable gdrc as GetResultCountResponse no-undo. + + Assert:NotNull(pData, 'Dataset handle'). + + assign cnt = pData:num-buffers + gdrc = new GetResultCountResponse(cnt) + . + do loop = 1 to cnt: + assign gdrc:ResultCounts[loop] = CountBufferRecords(pData:get-buffer-handle(loop)). + end. + + return gdrc. + end method. + + /* Returns the count of the total number of result records currently in a buffer + + @param handle The buffer whose records to count + @return IGetTableResultCountResponse The IGetTableResultCountResponse instance for the request */ + method protected IGetTableResultCountResponse CountBufferRecords(input pData as handle): + define variable qry as handle no-undo. + define variable gtrc as IGetTableResultCountResponse no-undo. + + Assert:NotNull(pData, 'Buffer handle'). + + create query qry. + qry:set-buffers(pData). + qry:query-prepare(substitute('preselect each &1 ':u, pData:name)). + + qry:query-open(). + assign gtrc = new GetTableResultCountResponse(pData:serialize-name, + qry:num-results, + // Exact=true because we open the query (per the interface description) + yes). + qry:query-close(). + + return gtrc. + finally: + delete object qry no-error. + end finally. + end method. END CLASS. - - - diff --git a/src/OpenEdge/BusinessLogic/Filter/AblFilterParser.cls b/src/OpenEdge/BusinessLogic/Filter/AblFilterParser.cls index ad3ba510..dddc0b63 100644 --- a/src/OpenEdge/BusinessLogic/Filter/AblFilterParser.cls +++ b/src/OpenEdge/BusinessLogic/Filter/AblFilterParser.cls @@ -1,5 +1,5 @@ /************************************************ -Copyright (c) 2018, 2020 by Progress Software Corporation. All rights reserved. +Copyright (c) 2018, 2020-2021 by Progress Software Corporation. All rights reserved. *************************************************/ /*------------------------------------------------------------------------ File : AblFilterParser @@ -15,6 +15,7 @@ block-level on error undo, throw. using Ccs.BusinessLogic.IGetDataRequest. using Ccs.BusinessLogic.IGetDataTableRequest. using Ccs.BusinessLogic.IQueryEntry. +using Ccs.BusinessLogic.IQueryGroup. using Ccs.BusinessLogic.IQuerySortEntry. using Ccs.BusinessLogic.JoinEnum. using Ccs.BusinessLogic.QueryOperatorEnum. @@ -33,8 +34,6 @@ using OpenEdge.BusinessLogic.QueryOperatorHelper. using OpenEdge.BusinessLogic.QueryPredicate. using OpenEdge.BusinessLogic.QuerySortEntry. using OpenEdge.Core.Assert. -using OpenEdge.Core.Collections.IList. -using OpenEdge.Core.Collections.List. using OpenEdge.Core.Collections.ObjectStack. using OpenEdge.Core.String. using OpenEdge.Core.StringConstant. @@ -44,31 +43,31 @@ using Progress.Json.ObjectModel.JsonObject. using Progress.Lang.AppError. class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: - + /* Default constructor */ constructor public AblFilterParser(): super(). end constructor. - + /* Constructor. - + @param longchar (mandatory) A table name for which to construct the filter */ constructor public AblFilterParser(input pTable as character): super(pTable). end constructor. - + /* Constructor. - + @param longchar[] (mandatory) An array of table name for which to construct the filter */ constructor public AblFilterParser(input pTable as character extent): super(pTable). end constructor. - + /* Parses an ABL BY expression into an array of IQuerySortEntry. - + If the string is malformed (ie can't be used as an ABL sort), then an indeterminate array is returned. mallformed might be something like "by eq 21" or "by". - + @param longchar The standard ABL BY expression. This can be a complete WHERE ... BY string or just the BY portion @return IQuerySortEntry[] An array of sort entries. Will be indeterminate if the input value is empty or null, or if the string isn't of the format "BY [BY ]" */ @@ -83,10 +82,10 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: define variable fieldName as character no-undo. define variable sortDirection as character no-undo. define variable word as character no-undo. - + if String:IsNullOrEmpty(pSortBy) then return querySortEntry. - + // pad the sort phrase with a space to make our calculations easier assign pSortBy = StringConstant:SPACE + pSortBy startPos = index(pSortBy, ' by ':u) @@ -112,31 +111,18 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: startPos = 0 . end. //EXPR-LOOP - + assign pSortBy = trim(replace(pSortBy, ' ':u, StringConstant:SPACE)) cnt = num-entries(pSortBy, StringConstant:SPACE) . - - BY-LOOP: - do loop = 1 to cnt: - assign word = entry(loop, pSortBy, StringConstant:SPACE). - - // count the occurrences of "by" - if word eq 'by':u then - assign elems = elems + 1. - end. //BY-LOOP - - extent(querySortEntry) = elems. - elems = 0. /* Reset before running loop again. */ - WORD-LOOP: do loop = 1 to cnt: assign word = entry(loop, pSortBy, StringConstant:SPACE). - + // if there are double spaces, skip if word eq '':u then next WORD-LOOP. - + // we want to start at BY and go from there // if this is not a BY then we've got a malformed string, // and we don't return anything @@ -145,7 +131,7 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: assign extent(querySortEntry) = ?. return querySortEntry. end. - + // BY or BY assign fieldName = entry(loop + 1, pSortBy, StringConstant:SPACE). //is there an order? @@ -153,7 +139,7 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: assign sortDirection = entry(loop + 2, pSortBy, StringConstant:SPACE). else assign sortDirection = '':u. - + // another by or end-of-string if sortDirection begins 'desc':u then assign sortOrder = SortOrderEnum:Descending @@ -168,70 +154,81 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: assign sortOrder = SortOrderEnum:Ascending loop = loop + 1 . - - assign elems = elems + 1 - querySortEntry[elems] = new QuerySortEntry(fieldName, sortOrder) + assign elems = elems + 1 + extent(querySortEntry) = elems + querySortEntry[elems] = new QuerySortEntry(fieldName, sortOrder) . - end. //WORD-LOOP - + end. //WORD-LOOP + return querySortEntry. end method. - + /* Parses a where string into a query entry - either a group or a predicate. - + If the parsing runs into problems (eg a malformed string) a null IQueryEntry is returned. Splitting of a string into groups using "()" is not supported. - + @param longchar The WHERE string to parse into parts @return IQueryEntry A query group, query predicate or unknown (if the string cannot be parsed) */ method protected IQueryEntry ParseWhereString(input pWhere as longchar): - define variable groupStack as ObjectStack no-undo. define variable qryPredicate as QueryPredicate no-undo. define variable operator as QueryOperatorEnum no-undo. define variable fieldValue as IPrimitiveHolder no-undo. define variable qryGroup as QueryGroup no-undo. + define variable parentGroup as QueryGroup no-undo. + define variable topGroup as QueryGroup no-undo. define variable haveValue as logical initial no no-undo. define variable haveName as logical initial no no-undo. define variable haveOperator as logical initial no no-undo. define variable inSingleQuotes as logical initial no no-undo. define variable inDoubleQuotes as logical initial no no-undo. - define variable inParens as logical initial no no-undo. define variable isQuotedNull as logical initial no no-undo. + define variable isTilde as logical initial no no-undo. define variable fieldName as character no-undo. define variable chrValue as character no-undo. define variable strValue as character no-undo. define variable nextChr as character no-undo. + define variable prevChr as character no-undo. define variable loop as integer no-undo. define variable len as integer no-undo. - define variable numPred as integer no-undo. - + define variable joinWith as JoinEnum no-undo. + define variable entries as IQueryEntry extent no-undo. + define variable joinEnumNames as character no-undo. + // WHERE table.field {and|or|,} assign pWhere = trim(pWhere). - // don't use TRIM since it doesn't always do what you think - if pWhere begins 'where ':u then - assign pWhere = substring(pWhere, 7). - + // don't use TRIM since it doesn't always do what you think with multi-character "trim-chars" expressions. + if pWhere begins 'where':u then + assign pWhere = trim(substring(pWhere, 6)). + assign len = length(pWhere). if String:IsNullOrEmpty(pWhere) then return qryPredicate. - + CHR-LOOP: - do loop = 1 to len: + do loop = 1 to len + on error undo, throw: // read chars until we hit a space or an operator. - assign chrValue = substring(pWhere, loop, 1). - + assign chrValue = substring(pWhere, loop, 1) + isTilde = (chrValue eq StringConstant:TILDE) + . // complete the string, since we'll process it below if loop eq len then do: + assign nextChr = '':u. // are we quoted? Close quotes if necessary if chrValue eq StringConstant:SINGLE_QUOTE then do: - if inSingleQuotes then + if not prevChr eq StringConstant:TILDE + and inSingleQuotes + then assign inSingleQuotes = no isQuotedNull = (strValue eq '?':u) . else - if not inDoubleQuotes then + if not prevChr eq StringConstant:TILDE + and not inDoubleQuotes + then assign inSingleQuotes = yes strValue = strValue + chrValue . @@ -239,34 +236,41 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: else if chrValue eq StringConstant:DOUBLE_QUOTE then do: - if inDoubleQuotes then + if not prevChr eq StringConstant:TILDE + and inDoubleQuotes + then assign inDoubleQuotes = no isQuotedNull = (strValue eq '?':u) . else - if not inSingleQuotes then + if not prevChr eq StringConstant:TILDE + and not inSingleQuotes + then assign inDoubleQuotes = yes strValue = strValue + chrValue . end. else - if chrValue eq ')':u then + if chrValue eq ')':u + and not inDoubleQuotes + and not inSingleQuotes + then do: - if inParens then - assign inParens = no. + /* NO-OP */ end. else assign strValue = strValue + chrValue. - end. - + end. // loop= len + else + assign nextChr = substring(pWhere, loop + 1, 1). + // we're at the end of a 'word'; strValue (usually) contains the word if chrValue eq StringConstant:SPACE or loop eq len then do: // we're in a quoted value - if inParens - or inDoubleQuotes + if inDoubleQuotes or inSingleQuotes then do: @@ -274,8 +278,12 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: next CHR-LOOP. end. else - // if this is the second space in a row, it's meaningless. skip - if strValue eq StringConstant:SPACE then + // if this is the second space in a row, it's meaningless. skip unless we're at the end of the + // string. In this case the actual last character may be a quote, that we're ignoring, and the + // quoted value is blank + if strValue eq StringConstant:SPACE + and not loop eq len + then next CHR-LOOP. else // if we hit a BY phrase, or we're at a new stanza, we're done with this WHERE @@ -295,34 +303,37 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: else if strValue eq 'and':u or strValue eq 'or':u + or strValue eq 'not':u then do: - if valid-object(qryPredicate) then - assign qryPredicate:Join = JoinEnum:GetEnum(strValue). - - //build a group and add the existing predicate + assign joinWith = GetJoin(strValue, joinWith). + if not valid-object(qryGroup) then - assign qryGroup = new QueryGroup() - qryGroup:Join = JoinEnum:GetEnum(strValue) - numPred = 1 - strValue = '':u - - extent(qryGroup:Entries) = numPred - qryGroup:Entries[numPred] = qryPredicate - qryPredicate = ? + assign qryGroup = new QueryGroup() + topGroup = qryGroup + parentGroup = qryGroup . + AddToGroup(qryGroup, qryPredicate, ?). + assign qryPredicate = ? + strValue = '':u + . //nothing more to do here next CHR-LOOP. - end. // AND|OR - + end. // AND|OR|NOT + //else - + // deal with words if not haveName then - assign fieldName = strValue - haveName = yes - strValue = '':u - . + do: + if trim(strValue) eq string(JoinEnum:Not) then + assign joinWith = JoinEnum:Not. + else + assign fieldName = strValue + haveName = yes + . + assign strValue = '':u. + end. else if not haveOperator then assign operator = QueryOperatorHelper:ToEnum(strValue) @@ -331,72 +342,171 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: . else if not haveValue then - do: - if strValue eq '?':u - and not isQuotedNull - then - assign fieldValue = String:Unknown(). - else - assign fieldValue = new String(strValue). - assign haveValue = yes + fieldValue = GetFieldValue(strValue, isQuotedNull, operator) strValue = '':u isQuotedNull = no . - end. end. // SPACE or end of loop else do: // are we quoted? if chrValue eq StringConstant:SINGLE_QUOTE then do: - if inSingleQuotes then + if not prevChr eq StringConstant:TILDE + and inSingleQuotes + then assign inSingleQuotes = no isQuotedNull = (strValue eq '?':u) + fieldValue = GetFieldValue(strValue, isQuotedNull, operator) + strValue = '':u + isQuotedNull = no + haveValue = yes . else - if not inDoubleQuotes then + if not prevChr eq StringConstant:TILDE + and not inDoubleQuotes + then assign inSingleQuotes = yes isQuotedNull = no . else assign strValue = strValue + chrValue. - end. + end. // ' else if chrValue eq StringConstant:DOUBLE_QUOTE then do: - if inDoubleQuotes then + if not prevChr eq StringConstant:TILDE + and inDoubleQuotes then assign inDoubleQuotes = no isQuotedNull = (strValue eq '?':u) + fieldValue = GetFieldValue(strValue, isQuotedNull, operator) + strValue = '':u + isQuotedNull = no + haveValue = yes . else - if not inSingleQuotes then + if not prevChr eq StringConstant:TILDE + and not inSingleQuotes then assign inDoubleQuotes = yes isQuotedNull = no . else assign strValue = strValue + chrValue. - end. + end. // " else if chrValue eq '(':u then do: - if not inParens then - assign inParens = true. + if not inSingleQuotes + and not inDoubleQuotes + then + do: + // If there are preceeding characters, then we may be in a function. + // If we are in a function, then return an unknown value. + // + // Since "for each tt where f1='a'and(f1 gt '')" and "where f1='a'and(f2>'')" + // are legal ABL, we try to resolve those into query entries. + if not strValue eq '':u then + do: + if joinEnumNames eq '':u then + assign joinEnumNames = get-class(JoinEnum):GetEnumNames(). + + if lookup(strValue, joinEnumNames) eq 0 then + return ?. + + if not valid-object(qryGroup) then + assign qryGroup = new QueryGroup() + topGroup = qryGroup + parentGroup = qryGroup + . + else + // the previous group is now the parent + assign parentGroup = qryGroup. + + // use any existing join + AddToGroup(qryGroup, qryPredicate, joinWith). + + // the JOIN applies to the following group, not the current predicate + assign joinWith = GetJoin(strValue, joinWith) + qryPredicate = ? + strValue = '':u + . + end. + + // first ( or there's no other group yet + if not valid-object(topGroup) then + assign topGroup = new QueryGroup() + parentGroup = topGroup + . + else + // the previous group is now the parent + assign parentGroup = qryGroup. + + // Create a group if this is a non-quoted ( + assign qryGroup = new QueryGroup(). + strValue = '':u + . + // add it to its parent + AddToGroup(parentGroup, qryGroup, joinWith). + + // clear the join, since this group may have its own, or not + assign joinWith = ?. + next CHR-LOOP. + end. + assign strValue = strValue + chrValue. - end. + end. // ( else if chrValue eq ')':u then do: - if inParens then - assign inParens = false. - assign strValue = strValue + chrValue. - end. + if not inDoubleQuotes + and not inSingleQuotes + then + do: + if haveName + and haveOperator + then + do: + assign fieldValue = GetFieldValue(strValue, isQuotedNull, operator) + strValue = '':u + isQuotedNull = no + haveName = no + haveOperator = no + haveValue = no + qryPredicate = new QueryPredicate(fieldName, operator, fieldValue) + . + // add the predicate to the current group + if AddToGroup(qryGroup, qryPredicate, joinWith) then + assign qryPredicate = ? + joinWith = ? + . + end. + // Now that the group is closed, reset the current group to the parent + assign qryGroup = parentGroup + parentGroup = GetGroupParent(topGroup, qryGroup) + . + end. + else + assign strValue = strValue + chrValue. + end. // ) + else + if isTilde then + do: + // if we're escaping something then don't write the tilde + if nextChr eq StringConstant:TILDE + or nextChr eq StringConstant:SINGLE_QUOTE + or nextChr eq StringConstant:DOUBLE_QUOTE + or nextChr eq StringConstant:CURLY_OPEN + then + next CHR-LOOP. + else + assign strValue = strValue + chrValue. + end. // ~ else // deal with custnum>23 if not haveName and not inSingleQuotes and not inDoubleQuotes - and not inParens then case chrValue: // allowed: <, >, =, <=, >=, <> @@ -404,33 +514,29 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: when '=':u or when '>':u then do: - assign fieldName = strValue - haveName = yes - strValue = '':u + assign fieldName = strValue + strValue = '':u + haveName = yes + haveOperator = yes . - if loop eq len then - assign nextChr = '':u. - else - assign nextChr = substring(pWhere, loop + 1, 1). - if nextChr eq '=':u or nextChr eq '>':u then + do: assign operator = QueryOperatorHelper:ToEnum(chrValue + nextChr) loop = loop + 1 . + // set nextChr because we bumped the loop value up + if loop eq len then + assign nextChr = '':u. + else + assign nextChr = substring(pWhere, loop + 1, 1). + end. else assign operator = QueryOperatorHelper:ToEnum(chrValue). - - assign haveOperator = yes. - + // if this is table.field= 20 (space after oprator) then skip the space, // since we're dealing with it here - if loop eq len then - assign nextChr = '':u. - else - assign nextChr = substring(pWhere, loop + 1, 1). - if nextChr eq StringConstant:SPACE then assign loop = loop + 1. end. @@ -440,7 +546,7 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: else assign strValue = strValue + chrValue. end. - + if haveName and haveOperator and haveValue @@ -451,41 +557,187 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: haveValue = no qryPredicate = new QueryPredicate(fieldName, operator, fieldValue) . - // for the end of the loop - if valid-object(qryGroup) then - assign numPred = numPred + 1 - extent(qryGroup:Entries) = numPred - qryGroup:Entries[numPred] = qryPredicate - qryPredicate = ? + // if it's added to a group, then we don't need the predicate any more + if AddToGroup(qryGroup, qryPredicate, joinWith) then + assign qryPredicate = ? + joinWith = ? . + else + // for NOT cases + if valid-object(joinWith) then + assign qryPredicate:Join = joinWith. end. + + finally: + assign prevChr = chrValue. + end finally. end. //CHR-LOOP - + // we return one of an IQueryPredicate (single clause) or an IQueryGroup (many clauses) + if valid-object(topGroup) then + assign qryGroup = topGroup. + if valid-object(qryGroup) then do: - if valid-object(qryPredicate) then - assign numPred = numPred + 1 - extent(qryGroup:Entries) = numPred - qryGroup:Entries[numPred] = qryPredicate - . + // We added a group (due to a ( probably) , but it's empty. So there's nothing to resolve here. + if extent(qryGroup:Entries) eq ? then + return ?. + if extent(qryGroup:Entries) eq 1 then + do: + // a group started by ( will have a valid Entries[1] but that won't have anything + if type-of(qryGroup:Entries[1], IQueryGroup) then + do: + assign entries = cast(qryGroup:Entries[1], IQueryGroup):Entries. + if extent(entries) eq ? then + return ?. + end. + + // if we have a join but no predicate, then something went wrong parsing the + // last predicate; return null in this case. + // "WHERE bal = 1 AND TRUE" is an example of this. + if valid-object(joinWith) + and not valid-object(qryPredicate) + then + return ?. + + // in case there's a stray predicate + AddToGroup(qryGroup, qryPredicate, joinWith). + + // if the string is just "(x=y)" then we only have/need the one group + if extent(qryGroup:Entries) eq 1 then + return qryGroup:Entries[1]. + end. + return qryGroup. end. else // we've been able to parse the whole where clause - if strValue eq '':u then + if strValue eq '':u + and not haveName + and not haveOperator + and not haveValue + and not valid-object(joinWith) + then return qryPredicate. else return ?. - + // if there's an error parsing the string, assume it's rubbish and return null. - catch e as Progress.Lang.Error: + {&_proparse_ prolint-nowarn(varusage)} + catch uncaught as Progress.Lang.Error: return ?. end catch. end method. - + + /* Indicates whether a string can be used with a MATCHES operator: + it needs to have an unescaped * or . in the string. + + @param character The expression to evaluate + @return logical TRUE if the expression contains at least one * or .; FALSE otherwise */ + method private logical IsMatchesExpression(input pExpr as character): + var integer pos. + + if pExpr eq ? + or pExpr eq '':u + then + return false. + + // look for unescaped * first + assign pos = index(pExpr, '*':u). + do while pos gt 0: + if pos ge 2 + and substring(pExpr, pos - 1, 1) eq OpenEdge.Core.StringConstant:TILDE + then + assign pos = index(pExpr, '*':u, pos + 1). + else + return true. + end. + + // then for unescaped . + assign pos = index(pExpr, '.':u). + do while pos gt 0: + if pos ge 2 + and substring(pExpr, pos - 1, 1) eq OpenEdge.Core.StringConstant:TILDE + then + assign pos = index(pExpr, '.':u, pos + 1). + else + return true. + end. + + return false. + end method. + + /* Reads the whole tree of groups in a group to find a particular group's parent group. + + @param QueryGroup The parent group + @param QueryGroup The group whose parent to find + @return QueryGroup The parent group, or NULL if none found. */ + method private QueryGroup GetGroupParent(input pParent as QueryGroup, + input pGroup as QueryGroup): + define variable loop as integer no-undo. + define variable cnt as integer no-undo. + define variable qg as QueryGroup no-undo. + + if not valid-object(pParent) then + return ?. + if pParent:Equals(pGroup) then + return pParent. + + assign cnt = extent(pParent:Entries). + ENTRY-LOOP: + do loop = 1 to cnt: + if pParent:Entries[loop]:Equals(pGroup) then + return pParent. + else + if type-of(pParent:Entries[loop], QueryGroup) then + do: + qg = GetGroupParent(cast(pParent:Entries[loop], QueryGroup), pGroup). + if valid-object(qg) then + return qg. + end. + end. + + return ?. + end method. + + /* Adds a query entry to an existing group. + + @param QueryGroup A group to which to add an entry to. + @param IQueryEntry A new query entry. May be a QueryPredicate or a QueryGroup + @param JoinEnum How the new entry joins (AND/OR/etc) to any previous entries in the group + @return logical TRUE if the query entry was added; FALSE otherwise */ + method private logical AddToGroup(input pParentGroup as QueryGroup, + input pEntry as IQueryEntry, + input pJoin as JoinEnum ): + define variable idx as integer no-undo. + + if not valid-object(pParentGroup) + or not valid-object(pEntry) + then + return false. + + assign idx = extent(pParentGroup:Entries) + 1. + // ? + anything = ? + if idx eq ? then + assign idx = 1. + + assign extent(pParentGroup:Entries) = idx + pParentGroup:Entries[idx] = pEntry + . + if valid-object(pJoin) then + do: + if type-of(pEntry, QueryGroup) then + assign cast(pEntry, QueryGroup):Join = pJoin. + else + if type-of(pEntry, QueryPredicate) then + assign cast(pEntry, QueryPredicate):Join = pJoin. + end. + + return true. + end method. + /* Parses an SORT-BY phrase and returns an array of IQuerySortEntry objects. - + @param character The SORT-BY phrase @return IQuerySortEntry[] An array of sort phrases. An indeterminate array is returned if the input phrase is empty */ method override public IQuerySortEntry extent ParseSortBy(input pSortBy as Progress.Lang.Object): @@ -494,17 +746,17 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: define variable sortByArray as character extent no-undo. define variable cnt as integer no-undo. define variable loop as integer no-undo. - + case true: when not valid-object(pSortBy) then return qrySort. - + when type-of(pSortBy, ILongcharHolder) or when type-of(pSortBy, ICharacterHolder) then assign sortByString = dynamic-property(pSortBy, 'Value':u) qrySort = ParseSortString(sortByString) . - + when type-of(pSortBy, ICharacterArrayHolder) or when type-of(pSortBy, ILongcharArrayHolder) then do: @@ -513,22 +765,22 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: and cnt ge 1 then assign cnt = extent(sortByArray). - + do loop = 1 to cnt: assign sortByString = sortByString + StringConstant:SPACE + sortByArray[loop]. end. assign qrySort = ParseSortString(sortByString). end. - + otherwise return qrySort. end case. - + return qrySort. end method. - + /* Parses where/filter phrase and returns an IQueryEntry object for a single table - + @param P.L.Object The filter/where clause phrase @return IQueryEntry The query entry.We return one of an IQueryPredicate (single clause) or an IQueryGroup (many clauses) */ @@ -539,17 +791,17 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: define variable cnt as integer no-undo. define variable loop as integer no-undo. define variable stringData as longchar extent no-undo. - + case true: when not valid-object(pWhere) then return qryEntry. - + when type-of(pWhere, ILongcharHolder) or when type-of(pWhere, ICharacterHolder) then assign whereString = dynamic-property(pWhere, 'Value':u) qryEntry = ParseWhereString(whereString) . - + when type-of(pWhere, ICharacterArrayHolder) or when type-of(pWhere, ILongcharArrayHolder) then do: @@ -557,13 +809,13 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: cnt = extent(stringData) . if cnt eq 1 then - assign qryEntry = ParseWhereString(string(stringData[1])). + assign qryEntry = ParseWhereString(stringData[1]). else if not cnt eq ? then do: assign qryGrp = new QueryGroup(cnt). do loop = 1 to cnt: - assign qryEntry = ParseWhereString(string(stringData[loop])). + assign qryEntry = ParseWhereString(stringData[loop]). //yuk, but the interfaces are defined READ-ONLY if type-of(qryEntry, QueryPredicate) then assign cast(qryEntry, QueryPredicate):Join = JoinEnum:And. @@ -575,16 +827,16 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: assign qryEntry = qryGrp. end. end. - + otherwise return qryEntry. end case. - + return qryEntry. end method. - + /* Reads and processes (parses) the filter. - + @param P.L.Object The filter data @param IGetDataRequest A new filter object */ method override public IGetDataRequest Parse(input pData as Progress.Lang.Object): @@ -594,77 +846,112 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: define variable cnt as integer no-undo. define variable loop as integer no-undo. define variable stringData as longchar extent no-undo. - + define variable tableName as character no-undo. + define variable nameCnt as integer no-undo. + assign dataRequest = new GetDataRequest(). - + case true: when not valid-object(pData) then return dataRequest. - + when type-of(pData, ICharacterHolder) or when type-of(pData, ILongcharHolder) then + do: assign dataRequest = new GetDataRequest(1) tableRequest = new GetDataTableRequest(FilterTable[1]) dataRequest:TableRequests[1] = tableRequest qryDef = new QueryDefinition() tableRequest:QueryDefinition = qryDef - qryDef:QuerySelection = ParseWhereString(dynamic-property(pData, 'Value':u)) + extent(stringData) = 1 + stringData[1] = dynamic-property(pData, 'Value':u) + qryDef:QuerySelection = ParseWhereString(stringData[1]) . - + // if we can't parse the input string, then just set as a string value + if not valid-object(qryDef:QuerySelection) then + {&_proparse_ prolint-nowarn(overflow)} + assign tableRequest:QueryDefinition = ? + tableRequest:QueryString = string(stringData[1]) + . + end. + when type-of(pData, ICharacterArrayHolder) or when type-of(pData, ILongcharArrayHolder) then do: assign stringData = dynamic-property(pData, 'Value':u) cnt = extent(stringData) + nameCnt = extent(FilterTable) . if not cnt eq ? and cnt ge 1 then assign extent(dataRequest:TableRequests) = cnt. - + do loop = 1 to cnt: - assign tableRequest = new GetDataTableRequest(FilterTable[loop]) + // if there's a FilterTable for the query, use that. + // if not, use blank to indicate "we don't know" + if nameCnt ge loop then + assign tableName = FilterTable[loop]. + else + assign tableName = '':u. + + assign tableRequest = new GetDataTableRequest(tableName) dataRequest:TableRequests[loop] = tableRequest qryDef = new QueryDefinition() tableRequest:QueryDefinition = qryDef - qryDef:QuerySelection = ParseWhereString(string(stringData[loop])) + qryDef:QuerySelection = ParseWhereString(stringData[loop]) . + // if we can't parse the input string, then just set as a string value + if not valid-object(qryDef:QuerySelection) then + {&_proparse_ prolint-nowarn(overflow)} + assign tableRequest:QueryDefinition = ? + tableRequest:QueryString = string(stringData[loop]) + . end. end. - + when type-of(pData, JsonObject) then assign extent(dataRequest:TableRequests) = 1 dataRequest:TableRequests[1] = ParseTableRequest(FilterTable[1], cast(pData, JsonObject)) . - + when type-of(pData, JsonArray) then do: - assign cnt = cast(pData, JsonArray):Length. + assign cnt = cast(pData, JsonArray):Length + nameCnt = extent(FilterTable) + . if not cnt eq ? and cnt ge 1 then assign extent(dataRequest:TableRequests) = cnt. - + do loop = 1 to cnt: - assign dataRequest:TableRequests[loop] = ParseTableRequest(FilterTable[1], + // if there's a FilterTable for the query, use that. + // if not, use blank to indicate "we don't know" + if nameCnt ge loop then + assign tableName = FilterTable[loop]. + else + assign tableName = '':u. + + assign dataRequest:TableRequests[loop] = ParseTableRequest(tableName, cast(pData, JsonArray):GetJsonObject(loop)). end. end. - + otherwise return error new AppError( substitute('Unsupported object type: &1', pData:GetClass():TypeName) , 0). end case. - + return cast(dataRequest, IGetDataRequest). end method. - + /* Reads a single table's request - + This method knows which properties in the input JSON are for the where clause, for the sort-by etc - + @param character The table name to which this filter applies @param JsonObject The input filter @return IGetDataTableRequest A single table Get Request */ @@ -675,32 +962,130 @@ class OpenEdge.BusinessLogic.Filter.AblFilterParser inherits FilterParser: define variable names as character extent no-undo. define variable loop as integer no-undo. define variable cnt as integer no-undo. - + define variable hasQry as logical extent 2 initial false no-undo. + define variable qryStr as character no-undo. + Assert:NotNull(pTable, 'Table name'). - + assign tableRequest = new GetDataTableRequest(pTable). - + if not valid-object(pData) then return tableRequest. - + assign qryDef = new QueryDefinition() tableRequest:QueryDefinition = qryDef names = pData:GetNames() cnt = extent(names) . NAMES-LOOP: - do loop = 1 to cnt: + do loop = 1 to cnt + while (not hasQry[1] or not hasQry[2]): + if not pData:GetType(names[loop]) eq JsonDataType:STRING then + next NAMES-LOOP. + // loop-de-loop cos JSON is case sensitive and ABL is not - if names[loop] eq 'where':u - and pData:GetType(names[loop]) eq JsonDataType:STRING - then - do: - assign qryDef:QuerySelection = ParseWhereString(pData:GetCharacter(names[loop])). - leave NAMES-LOOP. + case names[loop]: + when 'where':u then + if not hasQry[1] then + do: + assign qryStr = pData:GetCharacter(names[loop]) + qryDef:QuerySelection = ParseWhereString(qryStr) + hasQry[1] = true + . + if not valid-object(qryDef:QuerySelection) then + assign tableRequest:QueryString = qryStr + tableRequest:QueryDefinition = ? + . + end. + when 'by':u then + if not hasQry[2] then + assign qryDef:QuerySort = ParseSortString(pData:GetCharacter(names[loop])) + hasQry[2] = true + . + end case. + end. // NAMES-LOOP + + // if the query string failed to parse into an IQueryEntry, add the + // sort phrases to the string + if hasQry[1] + and hasQry[2] + and not valid-object(qryDef:QuerySelection) + then + do: + assign cnt = extent(qryDef:QuerySort). + // add the BY clauses from the QueryDefinition + do loop = 1 to cnt: + assign tableRequest:QueryString = tableRequest:QueryString + + ' by ':u + + qryDef:QuerySort[loop]:FieldName. + if valid-object(qryDef:QuerySort[loop]:SortOrder) + and qryDef:QuerySort[loop]:SortOrder eq SortOrderEnum:Descending + then + assign tableRequest:QueryString = tableRequest:QueryString + ' descending ':u. end. end. - + return cast(tableRequest, IGetDataTableRequest). end method. - -end class. \ No newline at end of file + + /* Returns a field value from the current parsed string + + @param character The parsed character + @param logical TRUE if the value is a quoted null + @param QueryOperatorEnum An optional operator + @return String The String containing the value */ + method private String GetFieldValue(input pVal as character, + input pQuotedNull as logical, + input pOperator as QueryOperatorEnum): + define variable fldVal as String no-undo. + + if pVal eq '?':u + and not pQuotedNull + then + assign fldVal = String:Unknown(). + else + do: + // for matches, make sure there's a * or . for matching in the string + // this turns is into an 'ending' since begins is its own operator + if valid-object(pOperator) + and pOperator eq QueryOperatorEnum:Matches + and not IsMatchesExpression(pVal) + then + // add * instead of . since that's a broader match + assign pVal = '*':u + pVal. + + assign fldVal = new String(pVal). + end. + + return fldVal. + end method. + + /* Determines the join value. + + If there is an existing join value passed in, + and it's value is either "And" or "Or", and the current string + value is "not", then return a new "AndNot" or "OrNot". + + If there is an existing join value passed in, and it is not "And" or + "Or", then that value is returned. + + If no join value is passed in, then use the string value to determine the + enum. + + @param character A word representing a join + @param JoinEnum The current join, if any + @return JoinEnum The new join. */ + method private JoinEnum GetJoin (input pVal as character, + input pJoin as JoinEnum): + case pJoin: + when ? then return JoinEnum:GetEnum(pVal). + when JoinEnum:And then return JoinEnum:AndNot. + when JoinEnum:Or then return JoinEnum:OrNot. + otherwise return pJoin. + end case. + + // should never get here, but have a value anyway + return JoinEnum:None. + end method. + +end class. diff --git a/src/OpenEdge/BusinessLogic/Filter/FilterParser.cls b/src/OpenEdge/BusinessLogic/Filter/FilterParser.cls new file mode 100644 index 00000000..3472e5b8 --- /dev/null +++ b/src/OpenEdge/BusinessLogic/Filter/FilterParser.cls @@ -0,0 +1,73 @@ +/************************************************ +Copyright (c) 2019 by Progress Software Corporation. All rights reserved. +*************************************************/ +/*------------------------------------------------------------------------ + File : FilterParser + Purpose : A parent/abstract filter parse class for creating + objects data requests for a CCS Business Entity's getData() + and getResultCount() operations + Syntax : + Description : + Author(s) : pjudge + Created : 2016-12-07 + Notes : + ----------------------------------------------------------------------*/ +block-level on error undo, throw. + +using Ccs.BusinessLogic.IGetDataRequest. +using Ccs.BusinessLogic.IQueryEntry. +using Ccs.BusinessLogic.IQuerySortEntry. +using OpenEdge.Core.Assert. + +class OpenEdge.BusinessLogic.Filter.FilterParser abstract: + /* An ordered set of table names used to create this filter. + Typically just one, but potentially more */ + define public property FilterTable as character extent no-undo + get. + private set. + + /* Default constructor. */ + constructor public FilterParser(): + this-object('':u). + end constructor. + + /* Constructor. + + @param character (mandatory) A table name for which to construct the filter */ + constructor public FilterParser(input pTable as character): + super(). + Assert:NotNull(pTable, 'Table name'). + assign extent(FilterTable) = 1 + this-object:FilterTable[1] = pTable + . + end constructor. + + /* Constructor. + + @param character[] (mandatory) An array of table name for which to construct the filter */ + constructor public FilterParser(input pTable as character extent): + super(). + Assert:NotNull(pTable, 'Table name'). + assign this-object:FilterTable = pTable. + end constructor. + + /* Reads and processes (parses) a complete data filter. + + @param P.L.Object The filter data + @param IGetDataRequest A new filter object */ + method abstract public IGetDataRequest Parse(input pData as Progress.Lang.Object). + + /* Parses where/filter phrase and returns an IQueryEntry object for a single table + + @param P.L.Object The filter/where clause data + @return IQueryEntry The query entry.We return one of an IQueryPredicate (single clause) + or an IQueryGroup (many clauses) */ + method abstract public IQueryEntry ParseWhere(input pWhere as Progress.Lang.Object). + + /* Parses an SORT-BY phrase and returns an array of IQuerySortEntry objects. + + @param P.L.Object The SORT-BY data + @return IQuerySortEntry[] An array of sort phrases. An indeterminate array is returned if the input phrase is empty */ + method abstract public IQuerySortEntry extent ParseSortBy(input pSortBy as Progress.Lang.Object). + +end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/Filter/FilterParserBuilder.cls b/src/OpenEdge/BusinessLogic/Filter/FilterParserBuilder.cls new file mode 100644 index 00000000..1e76bc46 --- /dev/null +++ b/src/OpenEdge/BusinessLogic/Filter/FilterParserBuilder.cls @@ -0,0 +1,218 @@ +/* ************************************************************************************************************************* +Copyright (c) 2018, 2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +************************************************************************************************************************** */ +/*------------------------------------------------------------------------ + File : FilterParserBuilder + Purpose : Contains a registry of objects used to parse JSON or other (typically) + string-based filters and create strongly-typed objects + Description : + Author(s) : pjudge + Created : 2018-08-05 + ----------------------------------------------------------------------*/ +block-level on error undo, throw. + +using OpenEdge.BusinessLogic.Filter.FilterParser. +using OpenEdge.BusinessLogic.Filter.FilterParserBuilder. +using OpenEdge.BusinessLogic.Filter.FilterParserRegistry. +using OpenEdge.Core.Assert. +using OpenEdge.Core.ISupportInitialize. +using OpenEdge.Core.Util.BuilderRegistry. +using OpenEdge.Core.Util.ConfigBuilder. +using Progress.Json.ObjectModel.JsonObject. +using Progress.Json.ObjectModel.JsonDataType. +using Progress.Json.ObjectModel.JsonArray. + +class OpenEdge.BusinessLogic.Filter.FilterParserBuilder inherits ConfigBuilder: + /* (mandatory) The filter pattern for which we create the Parser */ + define public property FilterPattern as character no-undo + get. + private set. + + /** Each concrete builder will implement this differently */ + define public property Parser as FilterParser no-undo + get(): + return BuildParser(). + end get. + + /** Registry for mapping build types to their implementations */ + define static public property Registry as BuilderRegistry no-undo + get(): + define variable oRegistry as BuilderRegistry no-undo. + if not valid-object(FilterParserBuilder:Registry) then + do: + assign oRegistry = new BuilderRegistry(get-class(FilterParserBuilder)). + oRegistry:Put(get-class(FilterParserBuilder):TypeName, get-class(FilterParserBuilder)). + assign FilterParserBuilder:Registry = oRegistry. + end. + return FilterParserBuilder:Registry. + end get. + private set. + + /* Constructor + + @param character The filter type that we're constructing a parser for */ + constructor public FilterParserBuilder (input pPattern as character): + Assert:NotNullOrEmpty(pPattern, 'Filter pattern'). + + assign this-object:FilterPattern = pPattern. + end constructor. + + /* Returns a parser builder for a give filter pattern. + + @param character The filter pattern name + @return FilterParserBuilder A builder for a parser for that pattern. */ + method static public FilterParserBuilder Build(input pPattern as character): + define variable builderType as Progress.Lang.Class no-undo. + define variable builder as FilterParserBuilder no-undo. + + Assert:NotNull(pPattern, 'Filter data'). + + assign builderType = FilterParserBuilder:Registry:Get(get-class(FilterParserBuilder):TypeName). + if valid-object(builderType) then + do: + Assert:IsType(builderType, get-class(FilterParserBuilder)). + + builder = dynamic-new string(builderType:TypeName)(pPattern). + end. + else + builder = new FilterParserBuilder(pPattern). + + if type-of(builder, ISupportInitialize) then + cast(builder, ISupportInitialize):Initialize(). + + return builder. + end method. + + /* Builds a parser for a JSON filter. This array version loops through the + array and passes any OBJECT or STRING entries to the relevant Build() + method. + + @param JsonArray The entire filter + @return FilterParserBuilder The filter parser builder to use */ + method static public FilterParserBuilder Build(input pFilter as JsonArray): + define variable builder as FilterParserBuilder no-undo. + define variable loop as integer no-undo. + define variable cnt as integer no-undo. + + Assert:NotNull(pFilter, 'JSON Filter'). + + assign cnt = pFilter:Length. + do loop = 1 to cnt + while not valid-object(builder): + + case pFilter:GetType(loop): + when JsonDataType:ARRAY then + assign builder = FilterParserBuilder:Build(pFilter:GetJsonArray(loop)). + when JsonDataType:OBJECT then + assign builder = FilterParserBuilder:Build(pFilter:GetJsonObject(loop)). + when JsonDataType:STRING then + assign builder = FilterParserBuilder:Build(pFilter:GetCharacter(loop)). + end case. + end. + + return builder. + end method. + + /* Builds a parser for a JSON filter + + Specialised JSON filter parsers are used. To figure out which on to + use + 1. Look for a string property called mappingType and use that property value + 2. Loop for a property in the JSON that matches one of the + registered filter parsers. + 3. Return without raising error. + + @param JsonObject The entire filter + @return FilterParserBuilder The filter parser builder to use */ + method static public FilterParserBuilder Build(input pFilter as JsonObject): + define variable builder as FilterParserBuilder no-undo. + define variable keyCnt as integer no-undo. + define variable keyMax as integer no-undo. + // AS CHARACTER suffers from ADAS-7500 + define variable propNames as longchar extent no-undo. + + Assert:NotNull(pFilter, 'JSON Filter'). + + // 1. shortcut via a string mappingType property + if pFilter:Has('mappingType':u) + and pFilter:GetType('mappingType':u) eq JsonDataType:STRING + then + assign builder = FilterParserBuilder:Build(pFilter:GetCharacter('mappingType':u)). + + // we found a builder. If not we'll try the slow way. + if valid-object(builder) then + return builder. + + assign propNames = pFilter:GetNames() + keyMax = extent(propNames) + . + // no data + if extent(propNames) eq ? then + return builder. + + // 2. Loop through the list of properties, by property name (not value) + do keyCnt = 1 to keyMax + while not valid-object(builder): + {&_proparse_ prolint-nowarn(overflow)} + assign builder = FilterParserBuilder:Build(string(propNames[keyCnt])). + end. + + // 3. Return + return builder. + end method. + + /* Builds a parser for a JSON filter + + @param character The name of the parser to find + @return FilterParser A usable parser, if one exists. May return NULL */ + method protected FilterParser BuildParser(): + define variable parserType as class Progress.Lang.Class no-undo. + define variable fp as FilterParser no-undo. + define variable filterTable as character extent no-undo. + + assign parserType = FilterParserRegistry:Registry:Get(this-object:FilterPattern). + if not valid-object(parserType) then + return parser. + + if HasOption('filterTable':u) then + assign filterTable = GetOptionStringArrayValue('filterTable':u). + + // no option or empty data + if extent(filterTable) eq ? then + assign extent(filterTable) = 1 + filterTable[1] = '':u + . + fp = dynamic-new string(parserType:TypeName) (filterTable). + + if type-of(fp, ISupportInitialize) then + cast(fp, ISupportInitialize):Initialize(). + + return fp. + end method. + + /* Sets the table for which this filter applies, if any + + @param character A table name for this filter + @return FilterParserBuilder this object */ + method public FilterParserBuilder TableName(input pTable as character): + define variable tables as character extent 1 no-undo. + + Assert:NotNull(pTable, 'Table name'). + assign tables[1] = pTable. + + return TableName(tables). + end method. + + /* Sets the table for which this filter applies, if any + + @param character[] Table names for this filter + @return FilterParserBuilder this object */ + method public FilterParserBuilder TableName(input pTable as character extent): + Assert:NotNull(pTable, 'Table name'). + + SetOption('filterTable':u, pTable). + + return this-object. + end method. + +end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/Filter/FilterParserRegistry.cls b/src/OpenEdge/BusinessLogic/Filter/FilterParserRegistry.cls new file mode 100644 index 00000000..bdf4e922 --- /dev/null +++ b/src/OpenEdge/BusinessLogic/Filter/FilterParserRegistry.cls @@ -0,0 +1,52 @@ +/* ************************************************************************************************************************* +Copyright (c) 2018 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +************************************************************************************************************************** */ +/*------------------------------------------------------------------------ + File : FilterParserRegistry + Purpose : Contains a registry of objects used to parse JSON or other (typically) + string-based filters and create strongly-typed objects + Description : + Author(s) : pjudge + Created : 2018-08-05 + ----------------------------------------------------------------------*/ +block-level on error undo, throw. + +using OpenEdge.BusinessLogic.Filter.AblFilterParser. +using OpenEdge.BusinessLogic.Filter.FilterParser. +using OpenEdge.BusinessLogic.Filter.FilterParserRegistry. +using OpenEdge.BusinessLogic.Filter.JfpFilterParser. +using OpenEdge.BusinessLogic.Filter.KendoFilterParser. +using OpenEdge.Core.Util.BuilderRegistry. + +class OpenEdge.BusinessLogic.Filter.FilterParserRegistry: + + /** Registry for mapping build types to their implementations */ + define static public property Registry as BuilderRegistry no-undo + get(): + define variable oRegistry as BuilderRegistry no-undo. + if not valid-object(FilterParserRegistry:Registry) then + do: + assign oRegistry = new BuilderRegistry(get-class(FilterParser)). + InitializeRegistry(oRegistry). + assign FilterParserRegistry:Registry = oRegistry. + end. + return FilterParserRegistry:Registry. + end get. + private set. + + /** Adds initial values into the registry + + @param BuilderRegistry The registry to populate */ + method static private void InitializeRegistry(input poRegistry as BuilderRegistry): + // Register known parsers + poRegistry:Put('WHERE':u, get-class(AblFilterParser)). + poRegistry:Put('ABL':u, get-class(AblFilterParser)). + + poRegistry:Put('ablFilter':u, get-class(JfpFilterParser)). + poRegistry:Put('JFP':u, get-class(JfpFilterParser)). + + poRegistry:Put('field':u, get-class(KendoFilterParser)). + poRegistry:Put('KENDO':u, get-class(KendoFilterParser)). + end method. + +end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/Filter/JfpFilterParser.cls b/src/OpenEdge/BusinessLogic/Filter/JfpFilterParser.cls index 027d06a6..8524ebe7 100644 --- a/src/OpenEdge/BusinessLogic/Filter/JfpFilterParser.cls +++ b/src/OpenEdge/BusinessLogic/Filter/JfpFilterParser.cls @@ -1,15 +1,16 @@ /************************************************ -Copyright (c) 2016 by Progress Software Corporation. All rights reserved. -*************************************************/ +Copyright (c) 2016, 2021 by Progress Software Corporation. All rights reserved. + +*************************************************/ /*------------------------------------------------------------------------ File : JfpFilterParser Purpose : Parse for JSON data in the JSON Filter Pattern - Syntax : - Description : + Syntax : + Description : Author(s) : pjudge Created : Wed Dec 07 14:11:10 EST 2016 Notes : * https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/dvwsv/updating-business-entities-for-access-by-telerik.html - * ablFilter — Contains the text of an ABL WHERE string (not including the WHERE keyword itself) on which to filter the + * ablFilter — Contains the text of an ABL WHERE string (not including the WHERE keyword itself) on which to filter the OpenEdge BusinessEntity query that returns the result set, as in the following examples: *"ablFilter" : "(State = 'MA') OR (State = 'GA')" *"ablFilter" : "Name BEGINS 'A'" @@ -26,16 +27,17 @@ Copyright (c) 2016 by Progress Software Corporation. All rights reserved. block-level on error undo, throw. using Ccs.BusinessLogic.IGetDataTableRequest. +using Ccs.BusinessLogic.SortOrderEnum. using OpenEdge.BusinessLogic.Filter.AblFilterParser. using OpenEdge.BusinessLogic.GetDataTableRequest. using OpenEdge.BusinessLogic.QueryDefinition. using Progress.Json.ObjectModel.JsonDataType. using Progress.Json.ObjectModel.JsonObject. -class OpenEdge.BusinessLogic.Filter.JfpFilterParser inherits AblFilterParser: +class OpenEdge.BusinessLogic.Filter.JfpFilterParser inherits AblFilterParser: /* Default constructor */ - constructor public JfpFilterParser(): + constructor public JfpFilterParser(): super(). end constructor. @@ -64,8 +66,18 @@ class OpenEdge.BusinessLogic.Filter.JfpFilterParser inherits AblFilterParser: input pData as JsonObject): define variable tableRequest as GetDataTableRequest no-undo. define variable qryDef as QueryDefinition no-undo. + define variable qryStr as character no-undo. + define variable loop as integer no-undo. + define variable cnt as integer no-undo. + + // If the table name isn't passed in, try to get from the filter + if pTable eq '':u + and pData:Has('tableRef':u) + and pData:GetType('tableRef':u) eq JsonDataType:STRING + then + assign pTable = pData:GetCharacter('tableRef':u). - assign tableRequest = new GetDataTableRequest(pTable). // we don't know the table name here + assign tableRequest = new GetDataTableRequest(pTable). if not valid-object(pData) then return tableRequest. @@ -82,7 +94,28 @@ class OpenEdge.BusinessLogic.Filter.JfpFilterParser inherits AblFilterParser: if pData:Has('ablFilter':u) and pData:GetType('ablFilter':u) eq JsonDataType:STRING then - assign qryDef:QuerySelection = ParseWhereString(pData:GetCharacter('ablFilter':u)). + do: + assign qryStr = pData:GetCharacter('ablFilter':u) + qryDef:QuerySelection = ParseWhereString(qryStr) + . + if not valid-object(qryDef:QuerySelection) then + do: + assign tableRequest:QueryDefinition = ? + tableRequest:QueryString = qryStr + cnt = extent(qryDef:QuerySort) + . + // add the BY clauses from the QueryDefinition + do loop = 1 to cnt: + assign tableRequest:QueryString = tableRequest:QueryString + + ' by ':u + + qryDef:QuerySort[loop]:FieldName. + if valid-object(qryDef:QuerySort[loop]:SortOrder) + and qryDef:QuerySort[loop]:SortOrder eq SortOrderEnum:Descending + then + assign tableRequest:QueryString = tableRequest:QueryString + ' descending ':u. + end. + end. + end. if pData:Has('id':u) and pData:GetType('id':u) eq JsonDataType:STRING diff --git a/src/OpenEdge/BusinessLogic/Filter/KendoFilterParser.cls b/src/OpenEdge/BusinessLogic/Filter/KendoFilterParser.cls new file mode 100644 index 00000000..140ea96e --- /dev/null +++ b/src/OpenEdge/BusinessLogic/Filter/KendoFilterParser.cls @@ -0,0 +1,381 @@ +/* ************************************************************************************************************************* +Copyright (c) 2019, 2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +************************************************************************************************************************** */ +/*------------------------------------------------------------------------ + File : KendoFilterParser + Purpose : + Syntax : + Description : + Author(s) : pjudge & dugrau + Created : Wed Dec 07 14:11:10 EST 2016 + Notes : + ----------------------------------------------------------------------*/ +block-level on error undo, throw. + +using Ccs.BusinessLogic.IGetDataRequest. +using Ccs.BusinessLogic.IGetDataTableRequest. +using Ccs.BusinessLogic.IQueryEntry. +using Ccs.BusinessLogic.IQuerySortEntry. +using Ccs.BusinessLogic.JoinEnum. +using Ccs.BusinessLogic.QueryOperatorEnum. +using Ccs.Common.Support.IPrimitiveHolder. +using OpenEdge.BusinessLogic.Filter.FilterParser. +using OpenEdge.BusinessLogic.GetDataRequest. +using OpenEdge.BusinessLogic.GetDataTableRequest. +using OpenEdge.BusinessLogic.QueryDefinition. +using OpenEdge.BusinessLogic.QueryGroup. +using OpenEdge.BusinessLogic.QueryOperatorHelper. +using OpenEdge.BusinessLogic.QueryPredicate. +using OpenEdge.BusinessLogic.QuerySortEntry. +using OpenEdge.Core.StringConstant. +using Progress.Json.ObjectModel.JsonArray. +using Progress.Json.ObjectModel.JsonConstruct. +using Progress.Json.ObjectModel.JsonDataType. +using Progress.Json.ObjectModel.JsonObject. +using Progress.Lang.AppError. + +class OpenEdge.BusinessLogic.Filter.KendoFilterParser inherits FilterParser: + + /* Default constructor */ + constructor public KendoFilterParser (): + super(). + end constructor. + + /* Constructor. + + @param character (mandatory) A table name for which to construct the filter */ + constructor public KendoFilterParser (input pTable as character): + super(pTable). + end constructor. + + /* Constructor. + + @param character[] (mandatory) An array of table name for which to construct the filter */ + constructor public KendoFilterParser (input pTable as character extent): + super(pTable). + end constructor. + + method protected IQueryEntry ParsePredicate (input pJoin as JoinEnum, + input pFilter as JsonObject): + define variable oClause as QueryPredicate no-undo. + define variable oFieldValue as IPrimitiveHolder no-undo. + define variable oJoinAs as JoinEnum no-undo. + define variable oOperator as QueryOperatorEnum no-undo. + define variable cOperator as character no-undo. + + /* Field name must be present, otherwise we must skip. */ + if not pFilter:Has("field":u) + or not pFilter:GetType("field":u) eq JsonDataType:STRING + then + return ?. + + /* Field operator must be present, otherwise we must skip. */ + if not pFilter:Has("operator":u) + or not pFilter:GetType("operator":u) eq JsonDataType:STRING + then + return ?. + + /* Default the join to that of the overall group (as passed in). */ + assign oJoinAs = pJoin. + + /* Field Value */ + case pFilter:GetType("value":u): + when JsonDataType:ARRAY or + when JsonDataType:OBJECT then + undo, throw new AppError(substitute("Unsupported filter value type of JsonObject/JsonArray for field &1", + pFilter:GetCharacter("field":u)), 0). + + when JsonDataType:NULL then + assign oFieldValue = new OpenEdge.Core.String(StringConstant:UNKNOWN). + + // we don"t at this point know what the underlying schmema and data type are, so stringify it all + otherwise + assign oFieldValue = new OpenEdge.Core.String(pFilter:GetJsonText("value":u)). + end case. + + assign cOperator = pFilter:GetCharacter("operator":u). + case cOperator: + /* First map complex operators. */ + + when "isnull":u then + assign oOperator = QueryOperatorEnum:Eq + oFieldValue = new OpenEdge.Core.String(StringConstant:UNKNOWN) + . + when "isnotnull":u then + assign oOperator = QueryOperatorEnum:Ne + oFieldValue = new OpenEdge.Core.String(StringConstant:UNKNOWN) + . + when "isempty":u then + assign oOperator = QueryOperatorEnum:Eq + oFieldValue = new OpenEdge.Core.String("":u) + . + when "isnotempty":u then + assign oOperator = QueryOperatorEnum:Ne + oFieldValue = new OpenEdge.Core.String("":u) + . + when "isnullorempty":u then + /* @TODO: This may need more work. */ + assign oOperator = QueryOperatorEnum:Eq + oFieldValue = new OpenEdge.Core.String("":u) + . + when "isnotnullorempty":u then + /* @TODO: This may need more work. */ + assign oOperator = QueryOperatorEnum:Ne + oFieldValue = new OpenEdge.Core.String("":u) + . + when "endswith":u then + do: + assign oOperator = QueryOperatorEnum:Matches. + cast(oFieldValue, OpenEdge.Core.String):Prepend("*":u). + end. + when "contains":u then + do: + assign oOperator = QueryOperatorEnum:Contains. + cast(oFieldValue, OpenEdge.Core.String):Append("*":u). + cast(oFieldValue, OpenEdge.Core.String):Prepend("*":u). + end. + when "doesnotcontain":u then + do: + assign oOperator = QueryOperatorEnum:Contains. + cast(oFieldValue, OpenEdge.Core.String):Append("*":u). + cast(oFieldValue, OpenEdge.Core.String):Prepend("*":u). + + /* Convert current join into a "and/or not" .*/ + if oJoinAs eq JoinEnum:And then + assign oJoinAs = JoinEnum:AndNot. + else + assign oJoinAs = JoinEnum:OrNot. + end. + + /* Simpler things are just mapped (EQ, NEQ, LTE, etc.) */ + otherwise + assign oOperator = QueryOperatorHelper:ToEnum(cOperator). + end case. + + assign oClause = new QueryPredicate(pFilter:GetCharacter("field":u), oOperator, oFieldValue) + oClause:Join = oJoinAs /* Use the determined join value. */ + . + + return oClause. + + finally: + delete object oOperator no-error. + delete object oJoinAs no-error. + end finally. + end method. + + method protected IQueryEntry ParseWhere (input pGroup as QueryGroup, + input pFilter as JsonObject): + define variable iFilters as integer no-undo. + define variable iLoop as integer no-undo. + define variable oFilters as JsonArray no-undo. + define variable oFilter as JsonObject no-undo. + + /** + * Note: This is a recursive method, whereby the QueryGroup passed in may + * consist of only query predicates (field, operator, value) or another + * sub-group entirely. For the case of the latter, the newly-found group + * will be passed again into this method for parsing. This allows for the + * case of nested queries to be generated from the KendoUI query. + */ + + /* Check for and assign the overall join for this group. */ + if pFilter:Has("logic":u) + and pFilter:GetType("logic":u) eq JsonDataType:STRING + then + assign pGroup:Join = JoinEnum:GetEnum(pFilter:GetCharacter("logic":u)). + + /* Check for and extract the filters for this group. */ + if pFilter:Has("filters":u) + and pFilter:GetType("filters":u) eq JsonDataType:ARRAY + then + assign oFilters = pFilter:GetJsonArray("filters":u). + + /* No need to proceed if filters list is empty. */ + if oFilters:length eq 0 then return pGroup. + + /* Set extent of QueryGroup entries equal to number of filters at this level. */ + extent(pGroup:Entries) = oFilters:length. + + /* Iterate over the filters array as provided. */ + assign iFilters = oFilters:Length. + FILTER-LOOP: + do iLoop = 1 to iFilters + on error undo, throw: + if not oFilters:GetType(iLoop) eq JsonDataType:OBJECT then next FILTER-LOOP. + + assign oFilter = oFilters:GetJsonObject(iLoop). + if oFilter:Has("logic") then + /* If the current object contains a "logic" property then this entry is a new group. */ + assign pGroup:Entries[iLoop] = this-object:ParseWhere(new QueryGroup(), oFilter). + else + /* Otherwise, the current object is destined to become a new QueryPredicate entry. */ + assign pGroup:Entries[iLoop] = this-object:ParsePredicate(pGroup:Join, oFilter). + + finally: + delete object oFilter no-error. + end finally. + end. /* iLoop */ + + return pGroup. + + finally: + delete object oFilters no-error. + end finally. + end method. + + /* Parses where/filter phrase and returns an IQueryEntry object for a single table + http://docs.telerik.com/kendo-ui/api/javascript/data/datasource#configuration-filter + + @param P.L.Object The filter/where clause data + @return IQueryEntry The query entry. We return one of an IQueryPredicate (single clause) + or an IQueryGroup (many clauses) */ + method override public IQueryEntry ParseWhere (input pWhere as Progress.Lang.Object): + define variable oJsonFilter as JsonObject no-undo. + + if not valid-object(pWhere) or not type-of(pWhere, JsonConstruct) then + return new QueryGroup(). + + /* Normalize the incoming filter data as a JSON object with a join logic. */ + if type-of(pWhere, JsonArray) then + do: + assign oJsonFilter = new JsonObject(). + oJsonFilter:Add("logic":u, JoinEnum:And:ToString()). + oJsonFilter:Add("filters":u, cast(pWhere, JsonArray)). + end. + else if type-of(pWhere, JsonObject) then + assign oJsonFilter = cast(pWhere, JsonObject). + + /* Return the final QueryGroup (IQueryEntry) as built by the expanded parser. */ + return this-object:ParseWhere(new QueryGroup(), oJsonFilter). + end method. + + /* Parses an SORT-BY phrase and returns an array of IQuerySortEntry objects. + http://docs.telerik.com/kendo-ui/api/javascript/data/datasource#configuration-sort + + @param P.L.Object The SORT-BY data + @return IQuerySortEntry[] An array of sort phrases. An indeterminate array is returned if the input phrase is empty */ + method override public IQuerySortEntry extent ParseSortBy (input pSortBy as Progress.Lang.Object): + define variable oSortEntry as QuerySortEntry no-undo extent. + define variable oSortObj as JsonObject no-undo. + define variable iX as integer no-undo. + + if not valid-object(pSortBy) or not type-of(pSortBy, JsonConstruct) then + return oSortEntry. + + if type-of(pSortBy, JsonArray) and cast(pSortBy, JsonArray):Length gt 0 then do: + /** + * An extent must be specified, even if some of the fields are not valid for sorting. + * Since we shouldn't have more sort entries than items in the array, use that count. + */ + extent(oSortEntry) = cast(pSortBy, JsonArray):length. + + do iX = 1 to cast(pSortBy, JsonArray):length: + assign oSortObj = cast(pSortBy, JsonArray):GetJsonObject(iX). + /* Add a new sort entry object with field and direction. */ + oSortEntry[iX] = new QuerySortEntry(oSortObj:GetCharacter("field":u), if oSortObj:GetCharacter("dir":u) eq "asc":u + then Ccs.BusinessLogic.SortOrderEnum:Ascending + else Ccs.BusinessLogic.SortOrderEnum:Descending). + end. /* do iX */ + end. /* valid-object */ + + return oSortEntry. + end method. + + /* Reads and processes (parses) the filter. + + @param P.L.Object The filter data + @param IGetDataRequest A new filter object */ + method override public IGetDataRequest Parse (input pData as Progress.Lang.Object): + define variable dataRequest as GetDataRequest no-undo. + + assign dataRequest = new GetDataRequest(). + + /* + * { + * "mappingType": "kendo", + * "filter": ~{"logic": "and|or", "filters": []}, + * "sort": [], + * "skip": #, + * "top": # + * } + */ + + /* If no valid JSON object available as input, simply create an empty object. */ + if not valid-object(pData) + or not type-of(pData, JsonConstruct) + then + assign pData = new JsonObject(). + + if type-of(pData, JsonObject) and extent(this-object:FilterTable) ge 1 then + do: + /* There should be only 1 table (for now) and filter data should be a JsonObject. */ + assign extent(dataRequest:TableRequests) = 1 + dataRequest:TableRequests[1] = this-object:ParseTableRequest(this-object:FilterTable[1], + cast(pData, JsonObject)) + . + end. + + return dataRequest. + end method. + + /* Reads a single table's request. + + This method knows which properties in the input JSON are for the where clause, for the sort-by etc + + @param character The table name to which this filter applies + @param JsonObject The input filter + @return IGetDataTableRequest A single table Get Request */ + method protected IGetDataTableRequest ParseTableRequest (input pTable as character, + input pData as JsonObject): + define variable tableRequest as GetDataTableRequest no-undo. + define variable qryDef as QueryDefinition no-undo. + + assign tableRequest = new GetDataTableRequest(pTable). // we don't know the table name here + + if not valid-object(pData) then + return tableRequest. + + assign qryDef = new QueryDefinition() + tableRequest:QueryDefinition = qryDef + . + + /* Should be "sort" though for backwards compatibility look for "orderBy" just in case. */ + if pData:Has("sort":u) + and pData:GetType("sort":u) eq JsonDataType:ARRAY + then + assign qryDef:QuerySort = this-object:ParseSortBy(pData:GetJsonArray("sort":u)). + else if pData:Has("orderBy":u) + and pData:GetType("orderBy":u) eq JsonDataType:ARRAY + then + assign qryDef:QuerySort = this-object:ParseSortBy(pData:GetJsonArray("orderBy":u)). + + /* Should have either a "filter" object or "filters" array. */ + if pData:Has("filter":u) + and pData:GetType("filter":u) eq JsonDataType:OBJECT + then + assign qryDef:QuerySelection = this-object:ParseWhere(pData:GetJsonObject("filter":u)). + else if pData:Has("filters":u) + and pData:GetType("filters":u) eq JsonDataType:ARRAY + then + assign qryDef:QuerySelection = this-object:ParseWhere(pData:GetJsonArray("filters":u)). + + if pData:Has("id":u) + and pData:GetType("id":u) eq JsonDataType:STRING + then + assign tableRequest:PagingContext = pData:GetCharacter("id":u). + + if pData:Has("top":u) + and pData:GetType("top":u) eq JsonDataType:NUMBER + then + assign tableRequest:NumRecords = pData:GetInt64("top":u). + + if pData:Has("skip":u) + and pData:GetType("skip":u) eq JsonDataType:NUMBER + then + assign tableRequest:Skip = pData:GetInt64("skip":u). + + return cast(tableRequest, IGetDataTableRequest). + end method. + +end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/GetDataRequest.cls b/src/OpenEdge/BusinessLogic/GetDataRequest.cls index a8c29cba..d309e8a9 100644 --- a/src/OpenEdge/BusinessLogic/GetDataRequest.cls +++ b/src/OpenEdge/BusinessLogic/GetDataRequest.cls @@ -115,7 +115,13 @@ class OpenEdge.BusinessLogic.GetDataRequest implements IGetDataRequest, IJsonSer // CUSTOM PARAMETER if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:customParameter), JsonDataType:OBJECT) then - assign this-object:CustomParameter = JsonSerializer:Deserialize(data:GetJsonObject(string(JsonPropertyNameEnum:customParameter))). + do: + assign data = data:GetJsonObject(string(JsonPropertyNameEnum:customParameter)) + this-object:CustomParameter = JsonSerializer:Deserialize(data) + . + if not valid-object(this-object:CustomParameter) then + assign this-object:CustomParameter = data. + end. end method. /* Serializes this object to JSON diff --git a/src/OpenEdge/BusinessLogic/GetDataResponse.cls b/src/OpenEdge/BusinessLogic/GetDataResponse.cls index a148b8c9..d5f2ab05 100644 --- a/src/OpenEdge/BusinessLogic/GetDataResponse.cls +++ b/src/OpenEdge/BusinessLogic/GetDataResponse.cls @@ -94,8 +94,14 @@ class OpenEdge.BusinessLogic.GetDataResponse implements IGetDataResponse, IJsonS end. // the custom response - if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:customResponse), JsonDataType:OBJECT) then - assign this-object:CustomResponse = JsonSerializer:Deserialize(data:GetJsonObject(string(JsonPropertyNameEnum:customResponse))). + if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:customResponse), JsonDataType:OBJECT) then + do: + assign data = data:GetJsonObject(string(JsonPropertyNameEnum:customResponse)) + this-object:CustomResponse = JsonSerializer:Deserialize(data) + . + if not valid-object(this-object:CustomResponse) then + assign this-object:CustomResponse = data. + end. end method. /* Serializes this object to JSON diff --git a/src/OpenEdge/BusinessLogic/GetDataTableRequest.cls b/src/OpenEdge/BusinessLogic/GetDataTableRequest.cls index dbe9082f..0877fe8f 100644 --- a/src/OpenEdge/BusinessLogic/GetDataTableRequest.cls +++ b/src/OpenEdge/BusinessLogic/GetDataTableRequest.cls @@ -1,19 +1,20 @@ /* ************************************************************************************************************************* -Copyright (c) 2018-2020 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +Copyright (c) 2018-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : GetDataTableRequest - Purpose : - Syntax : - Description : + Purpose : + Syntax : + Description : Author(s) : pjudge Created : 2018-06-15 - Notes : + Notes : ----------------------------------------------------------------------*/ block-level on error undo, throw. using Ccs.BusinessLogic.IGetDataTableRequest. using Ccs.BusinessLogic.IQueryDefinition. +using Ccs.BusinessLogic.IQueryEntry. using Ccs.BusinessLogic.IQuerySortEntry. using OpenEdge.BusinessLogic.IO.JsonPropertyNameEnum. using OpenEdge.BusinessLogic.QueryDefinition. @@ -24,33 +25,32 @@ using OpenEdge.Core.Json.IJsonSerializer. using OpenEdge.Core.Json.JsonConverter. using OpenEdge.Core.Json.JsonPropertyHelper. using OpenEdge.Core.Json.JsonSerializer. +using OpenEdge.Core.String. +using OpenEdge.Core.StringConstant. using Progress.Json.ObjectModel.JsonConstruct. using Progress.Json.ObjectModel.JsonDataType. using Progress.Json.ObjectModel.JsonObject. -using OpenEdge.Core.String. -using OpenEdge.Core.StringConstant. -using Ccs.BusinessLogic.IQueryEntry. class OpenEdge.BusinessLogic.GetDataTableRequest implements IGetDataTableRequest, IJsonSerializer: /* Returns the number of records requested by the caller of the Business Entity getData method */ define public property NumRecords as int64 no-undo get. set. /* Returns the paging context */ - define public property PagingContext as character no-undo get. set. + define public property PagingContext as character no-undo get. set. /* Returns the abstract query defintion for this request */ - define public property QueryDefinition as IQueryDefinition no-undo get. set. + define public property QueryDefinition as IQueryDefinition no-undo get. set. /* Returns the Query String for this table */ - define public property QueryString as character no-undo get. set. + define public property QueryString as character no-undo get. set. /* Returns the number of records to skip */ - define public property Skip as int64 no-undo get. set. + define public property Skip as int64 no-undo get. set. /* Returns the name of the ProDataset Table */ - define public property TableName as character no-undo + define public property TableName as character no-undo get. - private set. + private set. /* Default constructor */ constructor public GetDataTableRequest (): @@ -100,7 +100,7 @@ class OpenEdge.BusinessLogic.GetDataTableRequest implements IGetDataTableRequest /* Turns JSON into an object. - This method expects a JSON object - - If the "query" property is a string, this method tries to convert it to a IQueryDefinition object + - If the "query" property is a string, this method tries to convert it to a IQueryDefinition object @param JsonConstruct The input JSON */ method public void FromJson(input pJson as JsonConstruct): @@ -108,11 +108,7 @@ class OpenEdge.BusinessLogic.GetDataTableRequest implements IGetDataTableRequest define variable fp as FilterParser no-undo. define variable qd as QueryDefinition no-undo. define variable qryWhere as character no-undo. - define variable qrySort as character no-undo. define variable qryString as String no-undo. - define variable pos as integer extent 2 no-undo. - define variable qse as IQuerySortEntry extent no-undo. - define variable qe as IQueryEntry no-undo. if not valid-object(pJson) or not type-of(pJson, JsonObject) @@ -149,7 +145,7 @@ class OpenEdge.BusinessLogic.GetDataTableRequest implements IGetDataTableRequest assign this-object:QueryDefinition = cast(JsonSerializer:ToAblObject(data:GetJsonObject(string(JsonPropertyNameEnum:query)), get-class(IQueryDefinition), get-class(QueryDefinition)), - IQueryDefinition). + IQueryDefinition). end method. end class. diff --git a/src/OpenEdge/BusinessLogic/GetTableResultCountResponse.cls b/src/OpenEdge/BusinessLogic/GetTableResultCountResponse.cls index 75534a88..8b884123 100644 --- a/src/OpenEdge/BusinessLogic/GetTableResultCountResponse.cls +++ b/src/OpenEdge/BusinessLogic/GetTableResultCountResponse.cls @@ -3,10 +3,10 @@ Copyright (c) 2018-2020 by Progress Software Corporation and/or one of its subsi ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : GetTableResultCountResponse - Description : + Description : Author(s) : pjudge Created : 2018-06-15 - Notes : + Notes : ----------------------------------------------------------------------*/ block-level on error undo, throw. @@ -19,12 +19,12 @@ using Progress.Json.ObjectModel.JsonConstruct. using Progress.Json.ObjectModel.JsonDataType. using Progress.Json.ObjectModel.JsonObject. -class OpenEdge.BusinessLogic.GetTableResultCountResponse implements IGetTableResultCountResponse, IJsonSerializer: +class OpenEdge.BusinessLogic.GetTableResultCountResponse implements IGetTableResultCountResponse, IJsonSerializer: /* Returns is the result is exact (TRUE) or Guessed or Cached (FALSE) */ - define public property Exact as logical no-undo get. set. + define public property Exact as logical no-undo get. set. /* Returns the number of results for this table */ - define public property NumResults as int64 no-undo get. set. + define public property NumResults as int64 no-undo get. set. /* Returns the name of the table this result belongs to */ define public property TableName as character no-undo @@ -33,6 +33,7 @@ class OpenEdge.BusinessLogic.GetTableResultCountResponse implements IGetTableRes /* Default constructor */ constructor public GetTableResultCountResponse(): + super(). end constructor. /* Constructor @@ -92,13 +93,13 @@ class OpenEdge.BusinessLogic.GetTableResultCountResponse implements IGetTableRes assign data = cast(pData, JsonObject). if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:tableName), JsonDataType:STRING) then - assign TableName = data:GetCharacter(string(JsonPropertyNameEnum:tableName)). + assign TableName = data:GetCharacter(string(JsonPropertyNameEnum:tableName)). if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:exact), JsonDataType:BOOLEAN) then - assign Exact = data:GetLogical(string(JsonPropertyNameEnum:exact)). + assign Exact = data:GetLogical(string(JsonPropertyNameEnum:exact)). if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:numResults), JsonDataType:NUMBER) then - assign NumResults = data:GetInt64(string(JsonPropertyNameEnum:numResults)). + assign NumResults = data:GetInt64(string(JsonPropertyNameEnum:numResults)). end method. end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/IO/QueryEntryDeserializer.cls b/src/OpenEdge/BusinessLogic/IO/QueryEntryDeserializer.cls index 06d26637..0c66f484 100644 --- a/src/OpenEdge/BusinessLogic/IO/QueryEntryDeserializer.cls +++ b/src/OpenEdge/BusinessLogic/IO/QueryEntryDeserializer.cls @@ -4,12 +4,12 @@ Copyright (c) 2020 by Progress Software Corporation and/or one of its subsidiari /*------------------------------------------------------------------------ File : QueryEntryDeserializer Purpose : Helper class to deserialize a JSON object into an instance of - IQueryEntry - either a IQueryGroup or IQueryPredicate - Syntax : - Description : + IQueryEntry - either a IQueryGroup or IQueryPredicate + Syntax : + Description : Author(s) : pjudge Created : 2020-09-17 - Notes : + Notes : ----------------------------------------------------------------------*/ block-level on error undo, throw. @@ -25,10 +25,11 @@ using Progress.Json.ObjectModel.JsonObject. class OpenEdge.BusinessLogic.IO.QueryEntryDeserializer: /* Default constructor is private to avoid instantiation */ - constructor private QueryEntryDeserializer(): + constructor private QueryEntryDeserializer(): + /* Default constructor is private to avoid instantiation */ end constructor. - /* Attempts to convert a JSON object into a IQueryEntry (either an IQueryGroup or IQueryPredicate). + /* Attempts to convert a JSON object into a IQueryEntry (either an IQueryGroup or IQueryPredicate). @param JsonObject The serialised JSON data. May be null/unknown. @return IQueryEntry The query entry representing that JSON. Either an IQueryGroup or IQueryPredicate. Always @@ -41,7 +42,7 @@ class OpenEdge.BusinessLogic.IO.QueryEntryDeserializer: get-class(IQueryGroup), get-class(QueryGroup)), IQueryEntry). - if valid-object(qe) + if valid-object(qe) and type-of(qe, IQueryGroup) then do: diff --git a/src/OpenEdge/BusinessLogic/Query/QueryBuilder.cls b/src/OpenEdge/BusinessLogic/Query/QueryBuilder.cls index 90f133db..aa59decc 100644 --- a/src/OpenEdge/BusinessLogic/Query/QueryBuilder.cls +++ b/src/OpenEdge/BusinessLogic/Query/QueryBuilder.cls @@ -1,5 +1,5 @@ /* ************************************************************************************************************************* -Copyright (c) 2019-2020 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +Copyright (c) 2019-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : QueryBuilder @@ -37,12 +37,17 @@ using Ccs.Common.Support.ILogicalArrayHolder. using Ccs.Common.Support.ILogicalHolder. using Ccs.Common.Support.ILongcharArrayHolder. using Ccs.Common.Support.ILongcharHolder. +using Ccs.Common.Support.IRowidHolder. +using OpenEdge.Core.Assert. +using OpenEdge.Core.DataTypeEnum. using OpenEdge.Core.StringConstant. +using OpenEdge.Core.TimeStamp. +using Ccs.Common.Support.IPrimitiveHolder. class OpenEdge.BusinessLogic.Query.QueryBuilder: /* Returns a table/buffer handle for a given name from an input schema - + @param handle The input schema (dataset, buffer, table) @param character The table name. Can be the 'real' name or the serialize-name @return handle A buffer handle for the given name. */ @@ -50,12 +55,12 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: input pcTableName as character): define variable iBuffer as integer no-undo. define variable hTable as handle no-undo. - + if not valid-handle(phSchema) or phSchema:type ne "dataset" then return phSchema. /* Return handle as-is if not a dataset. */ - + if (pcTableName gt "") ne true then - return phSchema:get-buffer-handle(1). /* No table name, just get the top buffer by default. */ + return phSchema:get-top-buffer(1). /* No table name, just get the top buffer by default. */ else do: /** * We need to avoid throwing an error if we request a table that does not exist. @@ -135,39 +140,43 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: input pcDataType as character ): /* http://docs.telerik.com/kendo-ui/api/javascript/data/datasource#configuration-filter.operator */ define variable cTemplate as character no-undo. - - case poOperator: - when QueryOperatorEnum:Eq then - if pcDataType eq "character" then - assign cTemplate = '&1 eq "&2"'. - else - assign cTemplate = '&1 eq &2'. - when QueryOperatorEnum:Ne then - if pcDataType eq "character" then - assign cTemplate = '&1 ne "&2"'. - else - assign cTemplate = '&1 ne &2'. - when QueryOperatorEnum:Lt then - assign cTemplate = '&1 lt &2'. - when QueryOperatorEnum:Le then - assign cTemplate = '&1 le &2'. - when QueryOperatorEnum:Gt then - assign cTemplate = '&1 gt &2'. - when QueryOperatorEnum:Ge then - assign cTemplate = '&1 ge &2'. - when QueryOperatorEnum:Begins then - assign cTemplate = '&1 begins "&2"'. - when QueryOperatorEnum:Contains then - assign cTemplate = '&1 matches "&2"'. /* Value should already be appended/prepended with '*'. */ - when QueryOperatorEnum:Matches then - assign cTemplate = '&1 matches "&2"'. /* Value should already be appended/prepended with '*'. */ + + case pcDataType: + when 'character':u then + case poOperator: + when QueryOperatorEnum:Eq or + when QueryOperatorEnum:Ne or + when QueryOperatorEnum:Lt or + when QueryOperatorEnum:Le or + when QueryOperatorEnum:Gt or + when QueryOperatorEnum:Ge or + when QueryOperatorEnum:Begins or + when QueryOperatorEnum:Matches then + assign cTemplate = '&1 ' + string(poOperator) + ' "&2"'. + when QueryOperatorEnum:Contains then + assign cTemplate = '&1 matches "&2"'. + otherwise + undo, throw new Progress.Lang.AppError(substitute("Unsupported operator '&1' for filter data type &2.", + string(poOperator), pcDataType), 0). + end case. otherwise - undo, throw new Progress.Lang.AppError(substitute("Unknown operator '&1' for filter.", poOperator:ToString()), 0). + case poOperator: + when QueryOperatorEnum:Eq or + when QueryOperatorEnum:Ne or + when QueryOperatorEnum:Lt or + when QueryOperatorEnum:Le or + when QueryOperatorEnum:Gt or + when QueryOperatorEnum:Ge then + assign cTemplate = '&1 ' + string(poOperator) + ' &2'. + otherwise + undo, throw new Progress.Lang.AppError(substitute("Unsupported operator '&1' for filter data type &2.", + string(poOperator), pcDataType), 0). + end case. end case. - - return cTemplate. + + return lc(cTemplate). end method. /* MapOperator */ - + /* Creates a templated join phrase for a clause @param character The current query string @@ -269,8 +278,8 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: define variable hField as handle no-undo. define variable cClause as character no-undo. define variable cField as character no-undo. - define variable cOper as character no-undo. define variable cType as character no-undo. + define variable dtz as datetime-tz no-undo. /** * This should be the raw (public) field as provided by the front-end and must be mapped to a @@ -333,82 +342,136 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: when type-of(poClause:Value, ILogicalHolder) or when type-of(poClause:Value, ILogicalArrayHolder) then assign cType = "logical". - + + when type-of(poClause:Value, IRowidHolder) then + assign cType = "rowid". + otherwise assign cType = "character". end case. end. - + /** * Based on the datatype, convert the value and obtain a valid operation string for the clause. * The string contains the replacement operators for a "substitute" using the field name and value. */ + if valid-object(poClause:Value) then case cType: - when "character" then do: - /* Assumes the Value should only be of Longchar/Character type. */ - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value:ToString()). - end. /* character */ - - when "date" then do: - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, date(poClause:Value:ToString())). - else if type-of(poClause:Value, IDateHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value). - end. /* date */ - - when "datetime" then do: - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, datetime(poClause:Value:ToString())). - else if type-of(poClause:Value, IDateTimeHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value). - end. /* datetime */ - - when "datetime-tz" then do: - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, datetime-tz(poClause:Value:ToString())). - else if type-of(poClause:Value, IDateTimeTzHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value). - end. /* datetime-tz */ - - when "decimal" then do: - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, decimal(poClause:Value:ToString())). - else if type-of(poClause:Value, IDecimalHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value). - end. /* decimal */ - - when "integer" then do: - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, integer(poClause:Value:ToString())). - else if type-of(poClause:Value, IIntegerHolder) or type-of(poClause:Value, IInt64Holder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value). - end. /* integer */ - - when "logical" then do: - if type-of(poClause:Value, ILongcharHolder) or type-of(poClause:Value, ICharacterHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, logical(poClause:Value:ToString())). - else if type-of(poClause:Value, ILogicalHolder) then - assign cClause = substitute(this-object:MapOperator(poClause:Operator, cType), - cField, poClause:Value). - end. /* logical */ + when "character":u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + then + assign cClause = string(dynamic-property(poClause:Value, 'Value':u)). + else + // hope for the best + assign cClause = string(poClause:Value). + + when "date":u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + then + assign cClause = string(date(ParseIsoDate(string(dynamic-property(poClause:Value, 'Value':u))))). + else + if type-of(poClause:Value, IDateHolder) + or type-of(poClause:Value, IDateTimeHolder) + or type-of(poClause:Value, IDateTimeTzHolder) + then + assign cClause = string(date(dynamic-property(poClause:Value, 'Value':u))). + + when "datetime":u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + then + assign dtz = ParseIsoDate(string(dynamic-property(poClause:Value, 'Value':u))) + cClause = substitute('datetime(&1, &2)':u, + date(dtz), + mtime(dtz)) + . + else + if type-of(poClause:Value, IDateTimeTzHolder) then + assign dtz = cast(poClause:Value, IDateTimeTzHolder):Value + cClause = substitute('datetime(&1, &2)':u, + date(dtz), + mtime(dtz)). + else + if type-of(poClause:Value, IDateTimeHolder) then + assign cClause = substitute('datetime(&1, &2)':u, + date(cast(poClause:Value, IDateTimeHolder):Value), + mtime(cast(poClause:Value, IDateTimeHolder):Value)). + else + if type-of(poClause:Value, IDateHolder) then + assign cClause = substitute('datetime(&1)':u, + cast(poClause:Value, IDateHolder):Value). + + when "datetime-tz":u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + then + assign dtz = ParseIsoDate(string(dynamic-property(poClause:Value, 'Value':u))) + cClause = substitute('datetime-tz(&1, &2, &3)':u, + date(dtz), + mtime(dtz), + timezone(dtz)) + . + else + if type-of(poClause:Value, IDateTimeTzHolder) then + assign dtz = cast(poClause:Value, IDateTimeTzHolder):Value + cClause = substitute('datetime-tz(&1, &2, &3)':u, + date(dtz), + mtime(dtz), + timezone(dtz)). + else + if type-of(poClause:Value, IDateTimeHolder) then + assign cClause = substitute('datetime-tz(&1, &2)':u, + date(cast(poClause:Value, IDateTimeHolder):Value), + mtime(cast(poClause:Value, IDateTimeHolder):Value)). + else + if type-of(poClause:Value, IDateHolder) then + assign cClause = substitute('datetime-tz(&1)':u, + cast(poClause:Value, IDateHolder):Value). + + when "decimal":u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + or type-of(poClause:Value, IInt64Holder) + or type-of(poClause:Value, IIntegerHolder) + or type-of(poClause:Value, IDecimalHolder) + then + assign cClause = string(dynamic-property(poClause:Value, 'Value':u)). + + when "integer":u or + when 'int64':u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + // this may be too large a value for an INTEGER but that's a problem for later in the flow + or type-of(poClause:Value, IInt64Holder) + or type-of(poClause:Value, IIntegerHolder) + then + assign cClause = string(dynamic-property(poClause:Value, 'Value':u)). + + when "logical":u then + // see the doc for LOGICAL() function on how values are converted? + if type-of(poClause:Value, IPrimitiveHolder) then + assign cClause = string(logical(dynamic-property(poClause:Value, 'Value':u))). + + when 'rowid':u then + if type-of(poClause:Value, ILongcharHolder) + or type-of(poClause:Value, ICharacterHolder) + or type-of(poClause:Value, IRowidHolder) + then + assign cClause = substitute('to-rowid("&1")':u, + string(dynamic-property(poClause:Value, 'Value':u)) ). end case. - + + if not cClause eq '':u then + assign cClause = substitute(MapOperator(poClause:Operator, cType), + cField, + cClause). + return cClause. - catch err as Progress.Lang.Error: + {&_proparse_ prolint-nowarn(varusage)} + catch uncaught as Progress.Lang.Error: return "". /* Return empty clause if an error is encountered. */ end catch. end method. /* BuildClause */ @@ -452,10 +515,10 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: /* Builds a query string for a single table from a IGetDataTableRequest object. - This method maps the input names to the input handle's SERIALIZE-NAMEs and + This method maps the input names to the input handle's SERIALIZE-NAMEs and uses the 'real' names to build the where clause - @param IGetDataTableRequest A valid + @param IGetDataTableRequest A valid @param handle a Dataset or buffer (TT or DB) handle @param logical TRUE if the FILTER (where) expression must be included, FALSE if not @param logical TRUE if the SORT expression must be included , FALSE if not @@ -482,7 +545,7 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: // allow for ?, which defaults to true if not pInclFilter eq false then do: - // If the QueryString is set, use it. + // If the QueryString is set, use it. if not pFilter:QueryString eq ? and not pFilter:QueryString eq '':u then @@ -536,7 +599,7 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: else assign sortOrder = '':u. - assign querySort = substitute('&1 by &2&3':u, + assign querySort = substitute('&1 by &2&3':u, querySort, fieldName, sortOrder). @@ -552,6 +615,79 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: return qryString. end method. + /* Builds an array of query strings from a IGetDataRequest object. This method maps + the input names to the input dataset's SERIALIZE-NAMEs and uses the 'real' + names to build the where clauses. The output array matches the order of the buffers + in the dataset, which may differ from the order of the tablerequest array. + + @param IGetDataRequest A valid query definition + @param handle A Dataset or buffer (TT or DB) handle + @return character[] An array of WHERE clauses, matching the number of table requests */ + method public character extent BuildQueryStrings(input pFilter as IGetDataRequest, + input pSchema as handle): + define variable qryString as character extent no-undo. + define variable loop as integer no-undo. + define variable cnt as integer no-undo. + define variable tbl as handle no-undo. + define variable bufferList as character no-undo. + define variable delim as character no-undo. + define variable idx as integer no-undo. + + Assert:NotNull(pFilter, 'Filter object'). + + if valid-handle(pSchema) then + do: + if pSchema:type eq 'temp-table':u + or pSchema:type eq 'buffer':u + then + do: + assign extent(qryString) = 1 + qryString[1] = BuildQueryString(pFilter, pSchema) + . + return qryString. + end. + // only support datasets in here + if not pSchema:Type eq 'dataset':u then + assign pSchema = ?. + end. + + // Cycle through all the table requests for this filter. + assign cnt = extent(pFilter:TableRequests) + extent(qryString) = cnt + . + // move the valid-handle check outside of the loop, so we only do it once. instead of on each loop. + if valid-handle(pSchema) then + do: + assign delim = '':u. + do loop = 1 to pSchema:num-buffers: + assign bufferList = bufferList + delim + pSchema:get-buffer-handle(loop):serialize-name + delim = ',':u + . + end. + + do loop = 1 to cnt: + assign idx = lookup(pFilter:TableRequests[loop]:TableName, bufferList). + // use the first top buffer if no name is specified + if idx eq 0 + and pFilter:TableRequests[loop]:TableName eq '':u + then + assign idx = lookup(pSchema:get-top-buffer(1):serialize-name, bufferList). + + // if the table is in this dataset + if idx gt 0 then + assign tbl = this-object:GetTableBuffer(pSchema, entry(idx, bufferList)) + qryString[idx] = BuildQueryString(pFilter:TableRequests[loop], yes, yes, tbl) + . + end. + end. + else + do loop = 1 to cnt: + assign qryString[loop] = BuildQueryString(pFilter:TableRequests[loop], yes, yes, ?). + end. + + return qryString. + end method. + /* Builds a query string from a IGetDataRequest object. This method maps the input names to the input handle's SERIALIZE-NAMEs and uses the 'real' names to build the where clause @@ -561,10 +697,10 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: @return character A complete WHERE clause */ method public character BuildQueryString (input pFilter as IGetDataRequest, input pSchema as handle): - define variable cQueryString as character no-undo. - define variable bhTable as handle no-undo. - define variable iLoop as integer no-undo. - define variable cSortBy as character no-undo extent. + define variable cQueryString as character no-undo. + define variable bhTable as handle no-undo. + define variable iLoop as integer no-undo. + define variable cSortBy as character no-undo extent. define variable tblName as character no-undo. define variable tblQry as character no-undo. define variable cnt as integer no-undo. @@ -576,7 +712,8 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: do iLoop = 1 to cnt: if valid-handle(pSchema) and pSchema:type eq "dataset" then assign bhTable = this-object:GetTableBuffer(pSchema, pFilter:TableRequests[iLoop]:TableName). - else if valid-handle(pSchema) and pSchema:type eq "temp-table" then + else + if valid-handle(pSchema) and pSchema:type eq "temp-table" then assign bhTable = pSchema. /* Use given temp-table handle as-is. */ assign tblQry = BuildQueryString(pFilter:TableRequests[iLoop], yes, no, bhTable) @@ -606,11 +743,11 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: /* Merges 2 query strings with specified join. The resulting query string has the format "WHERE ( ) ( ) BY BY " - or + or "WHERE TRUE BY BY " if only BY clauses are passed in. - Each input string is put in parentheses, except when + Each input string is put in parentheses, except when i) the input strings are both ? ii) one or both of the input strings is empty iii) there are no where clauses @@ -621,7 +758,7 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: - if the join is NOT, use JoinEnum:AndNot - For other values, use as specified - - if both strings are null, then shortcut and return + - if both strings are null, then shortcut and return ? and ? = where true ? or ? = where true ? and not ? = where false @@ -641,12 +778,11 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: input pQueryString2 as character): define variable mergedQry as character no-undo. define variable startPos as integer no-undo. - define variable endPos as integer no-undo. define variable byExpr as character no-undo. - // Default Join is AND - if not valid-object(pJoin) - or pJoin eq JoinEnum:None + // Default Join is AND + if not valid-object(pJoin) + or pJoin eq JoinEnum:None then assign pJoin = JoinEnum:And. else @@ -654,7 +790,7 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: assign pJoin = JoinEnum:AndNot. // If only nulls are passed in, shortcut the join - if pQueryString1 eq ? + if pQueryString1 eq ? and pQueryString2 eq ? then do: @@ -664,11 +800,31 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: return 'where true':u. end. + // we know that the other string is not ? because of the above. if pQueryString1 eq ? then - assign pQueryString1 = 'where true':u. + do: + if trim(pQueryString2) begins 'where ':u then + return pQueryString2. + else + if pQueryString2 eq '':u then + return 'where true':u. + else + return 'where ':u + pQueryString2. + end. + + //assign pQueryString1 = 'where true':u. if pQueryString2 eq ? then - assign pQueryString2 = 'where true':u. + do: + if trim(pQueryString1) begins 'where ':u then + return pQueryString1. + else + if pQueryString1 eq '':u then + return 'where true':u. + else + return 'where ':u + pQueryString1. + end. + //assign pQueryString2 = 'where true':u. // nothing to merge from string2, so return string1 if pQueryString2 eq '':u then @@ -742,9 +898,83 @@ class OpenEdge.BusinessLogic.Query.QueryBuilder: else assign mergedQry = 'where true':u. - if not byExpr eq '':u then - assign mergedQry = mergedQry + ' ':u + trim(byExpr). + return mergedQry + byExpr. + end method. + + /* Converts a string (ISO, hopefully) into a datetime-tz. + This method does not thrown any errors, and will return the UNKNOWN value + if the input value cannot be converted into a datetime-tz. + + @param character A string containing a ISO or ABL formatted date/time/tz + @param datetime-tz A datetime-tz value representing the string, or the UNKNOWN value. */ + method protected datetime-tz ParseIsoDate(input pIso as character): + define variable dtz as datetime-tz no-undo. + define variable cnt as integer no-undo. + define variable pos as integer no-undo. + + assign cnt = length(pIso, 'raw':u) + pos = index(pIso, '-':u) + . + // check for potential dates + if pos eq 5 then + case cnt: + // DATE YYYY-MM-DD + when 10 then + if index(pIso, '-':u, pos + 1) gt 0 then + do: + assign dtz = datetime-tz(integer(substring(pIso, 6, 2)), + integer(substring(pIso, 9, 2)), + integer(substring(pIso, 1, 4)), + 0, + 0 ) + no-error. + if error-status:error then + assign dtz = ? + no-error. + end. + // DATETIME YYYY-MM-DDTHH:MM:SS.SSS + // 12345678901234567890123 + when 23 then + if index(pIso, '-':u, pos + 1) gt 0 + and index(pIso, 'T':u, pos + 1) gt 0 + then + do: + assign dtz = datetime-tz(integer(substring(pIso, 6, 2)), + integer(substring(pIso, 9, 2)), + integer(substring(pIso, 1, 4)), + integer(substring(pIso, 12, 2)), + integer(substring(pIso, 15, 2)), + integer(substring(pIso, 18, 2)), + integer(substring(pIso, 21, 3)) ) + no-error. + if error-status:error then + assign dtz = ? + no-error. + end. + // DATETIME-TZ YYYY-MM-DDTHH:MM:SS.SSS+HH:MM + when 29 then + if index(pIso, '-':u, pos + 1) gt 0 + and index(pIso, 'T':u, pos + 1) gt 0 + then + do: + assign dtz = TimeStamp:ToABLDateTimeTzFromISO(pIso) + no-error. + if error-status:error then + assign dtz = ? + no-error. + end. + end case. + + // could also be ABL format 01/07/2021 15:42:30.477-05:00 + if dtz eq ? then + do: + assign dtz = datetime-tz(pIso) + no-error. + if error-status:error then + assign dtz = ? + no-error. + end. - return mergedQry. + return dtz. end method. -end class. \ No newline at end of file +end class. diff --git a/src/OpenEdge/BusinessLogic/QueryDefinition.cls b/src/OpenEdge/BusinessLogic/QueryDefinition.cls index 5b1ed2eb..86800612 100644 --- a/src/OpenEdge/BusinessLogic/QueryDefinition.cls +++ b/src/OpenEdge/BusinessLogic/QueryDefinition.cls @@ -57,7 +57,6 @@ class OpenEdge.BusinessLogic.QueryDefinition implements IQueryDefinition, IJsonS input pSort as IQuerySortEntry extent): this-object(pEntry). - Assert:HasDeterminateExtent(pSort, 'Query sort entries'). assign this-object:QuerySort = pSort. end constructor. diff --git a/src/OpenEdge/BusinessLogic/QueryGroup.cls b/src/OpenEdge/BusinessLogic/QueryGroup.cls index 603cd73a..2de47747 100644 --- a/src/OpenEdge/BusinessLogic/QueryGroup.cls +++ b/src/OpenEdge/BusinessLogic/QueryGroup.cls @@ -1,14 +1,14 @@ /* ************************************************************************************************************************* -Copyright (c) 2018-2020 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +Copyright (c) 2018-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : QueryGroup - Purpose : - Syntax : - Description : + Purpose : + Syntax : + Description : Author(s) : pjudge Created : 2018-06-15 - Notes : + Notes : ----------------------------------------------------------------------*/ block-level on error undo, throw. @@ -25,6 +25,7 @@ using Progress.Json.ObjectModel.JsonArray. using Progress.Json.ObjectModel.JsonConstruct. using Progress.Json.ObjectModel.JsonDataType. using Progress.Json.ObjectModel.JsonObject. +using OpenEdge.Core.StringConstant. class OpenEdge.BusinessLogic.QueryGroup implements IQueryGroup, IJsonSerializer: @@ -44,37 +45,46 @@ class OpenEdge.BusinessLogic.QueryGroup implements IQueryGroup, IJsonSerializer: constructor public QueryGroup(): super(). // set a default value - assign this-object:Join = JoinEnum:None. + assign this-object:Join = JoinEnum:None. end constructor. /* Constructor @param integer The number of query entries in this group */ constructor public QueryGroup(input pNumEntries as integer): - super (). + this-object(). - Assert:IsPositive(pNumEntries, 'Num query entries '). + Assert:IsPositive(pNumEntries, 'Num query entries '). assign extent(Entries) = pNumEntries. end constructor. - + method override public character ToString(): define variable queryString as character no-undo. define variable cnt as integer no-undo. define variable loop as integer no-undo. + define variable delim as character no-undo. - assign cnt = extent(this-object:Entries). - + assign queryString = '(':u + cnt = extent(this-object:Entries) + delim = '':u + . do loop = 1 to cnt: - queryString = substitute('&1 &2 (&3)', - trim(queryString), - if queryString eq "" then "" else this-object:Join:ToString(), - trim(this-object:Entries[loop]:ToString())). + assign queryString = substitute('&1&2&3':u, queryString, delim, string(this-object:Entries[loop])) + delim = StringConstant:SPACE + . end. - - return trim(queryString). + assign queryString = queryString + ')':u. + + // optionally, prefix this group with its join + if valid-object(this-object:Join) + and not this-object:Join eq JoinEnum:None + then + assign queryString = string(this-object:Join) + ' ':u + queryString. + + return queryString. end method. - /* SERIALIZATION METHOD:returns a JsonConstruct (JsonDataType:OBJECT or JsonDataType:ARRAY) representation + /* SERIALIZATION METHOD:returns a JsonConstruct (JsonDataType:OBJECT or JsonDataType:ARRAY) representation of this object. @return JsonConstruct A JSON representation of this object. May be unknown (JsonDataType:NULL). */ @@ -84,7 +94,7 @@ class OpenEdge.BusinessLogic.QueryGroup implements IQueryGroup, IJsonSerializer: assign data = new JsonObject(). data:Add(string(JsonPropertyNameEnum:entries), JsonConverter:ToArray(this-object:Entries)). - data:Add(string(JsonPropertyNameEnum:join), string(this-object:Join)). + data:Add(string(JsonPropertyNameEnum:join), string(this-object:Join)). return data. end method. @@ -99,7 +109,7 @@ class OpenEdge.BusinessLogic.QueryGroup implements IQueryGroup, IJsonSerializer: define variable cnt as integer no-undo. if not valid-object(pJson) - or not type-of(pJson, JsonObject) + or not type-of(pJson, JsonObject) then return. diff --git a/src/OpenEdge/BusinessLogic/QueryOperatorHelper.cls b/src/OpenEdge/BusinessLogic/QueryOperatorHelper.cls new file mode 100644 index 00000000..660230ef --- /dev/null +++ b/src/OpenEdge/BusinessLogic/QueryOperatorHelper.cls @@ -0,0 +1,80 @@ +/************************************************ +Copyright (c) 2018, 2021 by Progress Software Corporation. All rights reserved. +*************************************************/ +/*------------------------------------------------------------------------ + File : QueryOperatorHelper + Purpose : + Syntax : + Description : + Author(s) : pjudge + Created : 2018-06-15 + Notes : + ----------------------------------------------------------------------*/ +block-level on error undo, throw. + +using Ccs.BusinessLogic.QueryOperatorEnum. +using OpenEdge.BusinessLogic.QueryOperatorHelper. + +class OpenEdge.BusinessLogic.QueryOperatorHelper: + + {&_proparse_ prolint-nowarn(varusage)} + var static private character mOperatorNames. + + /* Static constructor */ + constructor static QueryOperatorHelper(): + assign mOperatorNames = get-class(QueryOperatorEnum):GetEnumNames(). + end constructor. + + /* Helper method to turn a string operator into an enum + + @param character The operator + @param QueryOperatorEnum An enum operator. UNKNOWN/NULL if it cannot be converted into an enum */ + method static public QueryOperatorEnum ToEnum(input pOperator as character): + case pOperator: + when '':u or + when ? then + return ?. + + when '=':u or + when 'eq':u then + return QueryOperatorEnum:Eq. + + when '>':u or + when 'gt':u then + return QueryOperatorEnum:Gt. + + when '>=':u or + when 'ge':u or + when 'gte':u then + return QueryOperatorEnum:Ge. + + when '<':u or + when 'lt':u then + return QueryOperatorEnum:Lt. + + when '<=':u or + when 'le':u or + when 'lte':u then + return QueryOperatorEnum:Le. + + when '<>':u or + when 'neq':u then + return QueryOperatorEnum:Ne. + + when 'startswith':u or + when 'beginswith':u then + return QueryOperatorEnum:Begins. + + when 'endswith':u then + return QueryOperatorEnum:Matches. + + otherwise + // if this is an actual operator then return the enum + if lookup(pOperator, QueryOperatorHelper:mOperatorNames) eq 0 then + return ?. + else + return QueryOperatorEnum:GetEnum(pOperator). + end case. + end method. + +end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/QueryPredicate.cls b/src/OpenEdge/BusinessLogic/QueryPredicate.cls index fc678d8f..6f28c5c9 100644 --- a/src/OpenEdge/BusinessLogic/QueryPredicate.cls +++ b/src/OpenEdge/BusinessLogic/QueryPredicate.cls @@ -1,14 +1,14 @@ /* ************************************************************************************************************************* -Copyright (c) 2018-2020 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +Copyright (c) 2018-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : QueryPredicate - Purpose : - Syntax : - Description : + Purpose : + Syntax : + Description : Author(s) : pjudge Created : 2018-06-15 - Notes : + Notes : ----------------------------------------------------------------------*/ block-level on error undo, throw. @@ -30,22 +30,28 @@ using Progress.Json.ObjectModel.JsonObject. class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSerializer: /* Returns the name of the field for this query predicate */ - define public property FieldName as character no-undo + define public property FieldName as character no-undo get. - private set. + private set. /* Returns the operator for this query predicate */ - define public property Join as JoinEnum no-undo get. set. + define public property Join as JoinEnum no-undo + get. + set(input pJoin as JoinEnum): + Assert:NotNull(pJoin, 'Join operator'). + + assign this-object:Join = pJoin. + end set. /* Returns the operator for this query predicate */ define public property Operator as QueryOperatorEnum no-undo get. - private set. + private set. /* Returns a single value for this query predicate */ - define public property Value as IPrimitiveHolder no-undo + define public property Value as IPrimitiveHolder no-undo get. - private set. + private set. /* Returns a list of values for this query predicate */ define public property Values as IPrimitiveArrayHolder no-undo @@ -54,16 +60,18 @@ class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSer /* Default constructor - for JSON deserialization only */ constructor public QueryPredicate(): - end method. + super(). + end constructor. /* Constructor - @param character (mandatory) The field name - @param QueryOperatorEnum The operator + @param character (mandatory) The field name + @param QueryOperatorEnum The operator @param IPrimitiveHolder The predicate value */ constructor public QueryPredicate (input pFieldName as character, input pOperator as QueryOperatorEnum, input pValue as IPrimitiveHolder): + this-object(). Assert:NotNullOrEmpty(pFieldName, 'Predicate field name'). Assert:NotNull(pValue, 'Predicate value holder '). @@ -82,12 +90,13 @@ class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSer /* Constructor - @param character (mandatory) The field name - @param QueryOperatorEnum The operator + @param character (mandatory) The field name + @param QueryOperatorEnum The operator @param IPrimitiveArrayHolder The predicate value */ constructor public QueryPredicate (input pFieldName as character, input pOperator as QueryOperatorEnum, input pValues as IPrimitiveArrayHolder): + this-object(). Assert:NotNullOrEmpty(pFieldName, 'Predicate field name'). Assert:NotNull(pValues, 'Predicate values holder '). @@ -104,15 +113,22 @@ class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSer end constructor. method override public character ToString(): - return trim(substitute('&1 &2 &3 &4', + define variable strVal as character no-undo. + + assign strVal = trim(substitute('&1 &2 &3':u, this-object:FieldName, - this-object:Operator:ToString(), - (if valid-object(this-object:Value) then this-object:Value:ToString() else if valid-object(this-object:Values) then this-object:Values:ToString() else ?), - (if valid-object(this-object:Join) then this-object:Join:ToString() else '':u))). + string(this-object:Operator), + (if valid-object(this-object:Value) then string(this-object:Value) else string(this-object:Values)))). + // optionally, prefix this predicate with its join + if valid-object(this-object:Join) + and not this-object:Join eq JoinEnum:None + then + assign strVal = string(this-object:Join) + ' ':u + strVal. + + return strVal. end method. - - - /* SERIALIZATION METHOD:returns a JsonConstruct (JsonDataType:OBJECT or JsonDataType:ARRAY) representation + + /* SERIALIZATION METHOD:returns a JsonConstruct (JsonDataType:OBJECT or JsonDataType:ARRAY) representation of this object. @return JsonConstruct A JSON representation of this object. May be unknown (JsonDataType:NULL). */ @@ -126,7 +142,7 @@ class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSer data:Add(string(JsonPropertyNameEnum:operator), string(this-object:Operator)). if valid-object(this-object:Values) then - data:Add(string(JsonPropertyNameEnum:value), JsonConverter:ToArray(this-object:Values)). + data:Add(string(JsonPropertyNameEnum:value), JsonConverter:ToArray(this-object:Values)). else JsonConverter:AddToObject(JsonPropertyNameEnum:value, this-object:Value, data). @@ -140,7 +156,7 @@ class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSer define variable data as JsonObject no-undo. if not valid-object(pJson) - or not type-of(pJson, JsonObject) + or not type-of(pJson, JsonObject) then return. @@ -161,13 +177,13 @@ class OpenEdge.BusinessLogic.QueryPredicate implements IQueryPredicate, IJsonSer // VALUE(S) if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:value), JsonDataType:ARRAY) then assign this-object:Values = JsonConverter:ToPrimitiveArray(data:GetJsonArray(string(JsonPropertyNameEnum:value))) - this-object:Value = ? + this-object:Value = ? . else if not JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:value), JsonDataType:NULL) then assign this-object:Value = JsonConverter:ToScalar(data, string(JsonPropertyNameEnum:value)) - this-object:Values = ? - . + this-object:Values = ? + . end method. end class diff --git a/src/OpenEdge/BusinessLogic/UpdateDataRequest.cls b/src/OpenEdge/BusinessLogic/UpdateDataRequest.cls index ec12191c..1dd7fe9f 100644 --- a/src/OpenEdge/BusinessLogic/UpdateDataRequest.cls +++ b/src/OpenEdge/BusinessLogic/UpdateDataRequest.cls @@ -53,8 +53,6 @@ class OpenEdge.BusinessLogic.UpdateDataRequest implements IUpdateDataRequest, IJ @param JsonConstruct This object represented as JSON */ method public void FromJson(input pJson as JsonConstruct): define variable data as JsonObject no-undo. - define variable serialisedData as JsonConstruct no-undo. - define variable customType as Progress.Lang.Class no-undo. if not valid-object(pJson) or not type-of(pJson, JsonObject) @@ -69,7 +67,13 @@ class OpenEdge.BusinessLogic.UpdateDataRequest implements IUpdateDataRequest, IJ // CUSTOM REQUEST if JsonPropertyHelper:HasTypedProperty(data, string(JsonPropertyNameEnum:customRequest), JsonDataType:OBJECT) then - assign this-object:CustomRequest = JsonSerializer:Deserialize(data:GetJsonObject(string(JsonPropertyNameEnum:customRequest))). + do: + assign data = data:GetJsonObject(string(JsonPropertyNameEnum:customRequest)) + this-object:CustomRequest = JsonSerializer:Deserialize(data) + . + if not valid-object(this-object:CustomRequest) then + assign this-object:CustomRequest = data. + end. end method. end class. \ No newline at end of file diff --git a/src/OpenEdge/BusinessLogic/UpdateModeEnum.cls b/src/OpenEdge/BusinessLogic/UpdateModeEnum.cls new file mode 100644 index 00000000..8f4ab85c --- /dev/null +++ b/src/OpenEdge/BusinessLogic/UpdateModeEnum.cls @@ -0,0 +1,37 @@ + +/*************************************************************************** + * Copyright (c) 2017 by Progress Software Corporation. All rights reserved. +****************************************************************************/ + +/*-------------------------------------------------------------------------- + File : UpdateModeEnum + Purpose : Update modes supported by abstract BusinessEntity + Syntax : + Description : + Author(s) : Maura Regan + Created : Tue May 23 09:18:45 EST 2017 + Notes : + -------------------------------------------------------------------------*/ + + +/* BusinessEntity now supports 4 modes on how updates are performed: + * + * TRANSACTIONAL_SUBMIT: Entire changeset is processed as single transaction + * BULK_SUBMIT: Each row change is processed as a separate transaction + * CUD: Client makes individual call to create, update or delete operation + * CUD_NOBI: Client makes individual call to create, update or delete operation, but + * does not send/receive any BI data as part of request/response + + */ + + +BLOCK-LEVEL ON ERROR UNDO, THROW. + +ENUM OpenEdge.BusinessLogic.UpdateModeEnum: + DEFINE ENUM UNDEFINED + TRANSACTIONAL_SUBMIT + BULK_SUBMIT + CUD + CUD_NOBI + . +END ENUM. diff --git a/src/OpenEdge/Core/Assert.cls b/src/OpenEdge/Core/Assert.cls index 2d97d789..d9131c30 100644 --- a/src/OpenEdge/Core/Assert.cls +++ b/src/OpenEdge/Core/Assert.cls @@ -1,15 +1,15 @@ /************************************************ -Copyright (c) 2013-2020 by Progress Software Corporation +Copyright (c) 2013-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. *************************************************/ /** ------------------------------------------------------------------------ File : Assert - Purpose : General assertions of truth. - Syntax : - Description : + Purpose : General assertions of truth. + Syntax : + Description : @author pjudge Created : Wed Mar 03 10:08:57 EST 2010 - Notes : * This version based on the AutoEdge|TheFactory version + Notes : * This version based on the AutoEdge|TheFactory version ---------------------------------------------------------------------- */ block-level on error undo, throw. @@ -29,157 +29,123 @@ class OpenEdge.Core.Assert: end method. method public static void Equals(input a as rowid, input b as rowid): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. + {&_proparse_ prolint-nowarn(recidkeyword)} method public static void Equals(input a as recid, input b as recid): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as handle, input b as handle): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as longchar, input b as longchar): - define variable failMessage as character no-undo. + /* Use a longchar for the message, so that substitute is working entirely on longchar variables. */ + define variable subMessage as longchar no-undo initial "Expected: &1 but was: &2":u. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute(subMessage, a, b), 0). end method. method public static void Equals(input a as decimal, input b as decimal): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as int64, input b as int64): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as integer, input b as integer): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as character, input b as character): - define variable failMessage as character no-undo. if not a = b then - do: - failMessage = substitute("Expected: &1 but was: &2", a, b). - return error new AssertionFailedError(failMessage, 0). - end. + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as date, input b as date): if not a eq b then - return error new AssertionFailedError( - substitute("Expected: &1 but was: &2", a, b), - 0). + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as datetime, input b as datetime): if not a eq b then - return error new AssertionFailedError( - substitute("Expected: &1 but was: &2", a, b), - 0). + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as datetime-tz, input b as datetime-tz): if not a eq b then - return error new AssertionFailedError( - substitute("Expected: &1 but was: &2", a, b), - 0). + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void Equals(input a as logical, input b as logical): if not a eq b then - return error new AssertionFailedError( - substitute("Expected: &1 but was: &2", a, b), - 0). + return error new AssertionFailedError(substitute("Expected: &1 but was: &2":u, a, b), 0). end method. method public static void NotEqual(input a as character, input b as character): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void NotEqual(input a as decimal, input b as decimal): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void NotEqual(input a as handle, input b as handle): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void NotEqual(input a as int64, input b as int64): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void NotEqual(input a as integer, input b as integer): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void NotEqual(input a as longchar, input b as longchar): + /* Use a longchar for the message, so that substitute is working entirely on longchar variables. */ + define variable subMessage as longchar no-undo initial "&1 and &2 are equal":u. if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute(subMessage, a, b), 0). end method. method public static void NotEqual(input a as Object, input b as Object): AssertObject:NotEqual(a, b). end method. + {&_proparse_ prolint-nowarn(recidkeyword)} method public static void NotEqual(input a as recid, input b as recid): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void NotEqual(input a as rowid, input b as rowid): if a eq b then - return error new AssertionFailedError(substitute('&1 and &2 are equal', a, b), 0). + return error new AssertionFailedError(substitute('&1 and &2 are equal':u, a, b), 0). end method. method public static void IsTrue(input a as logical): if not (a eq true) then - return error new AssertionFailedError(substitute("Expected: TRUE but was: &1", a), 0). + return error new AssertionFailedError(substitute("Expected: TRUE but was: &1":u, a), 0). end method. method public static void IsFalse(input a as logical): if not (a eq false) then - return error new AssertionFailedError(substitute("Expected: FALSE but was: &1", a), 0). + return error new AssertionFailedError(substitute("Expected: FALSE but was: &1":u, a), 0). end method. method public static void NotNull(input poArgument as Object , pcName as char): @@ -215,8 +181,8 @@ class OpenEdge.Core.Assert: end method. method public static void NotNull(pcArgument as character, pcName as character): - if pcArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be unknown', pcName), 0). + if pcArgument eq ? then + undo, throw new AssertionFailedError(substitute('&1 cannot be unknown':u, pcName), 0). end method. method public static void NotNull(pcArgument as character): @@ -224,8 +190,8 @@ class OpenEdge.Core.Assert: end method. method public static void NotNull(prArgument as raw, pcName as character): - if prArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be unknown', pcName), 0). + if prArgument eq ? then + undo, throw new AssertionFailedError(substitute('&1 cannot be unknown':u, pcName), 0). end method. method public static void NotNull(prArgument as raw): @@ -233,8 +199,8 @@ class OpenEdge.Core.Assert: end method. method public static void IsNull(prArgument as raw, pcName as character): - if prArgument ne ? then - undo, throw new AssertionFailedError(substitute('&1 must be unknown', pcName), 0). + if prArgument ne ? then + undo, throw new AssertionFailedError(substitute('&1 must be unknown':u, pcName), 0). end method. method public static void IsNull(prArgument as raw): @@ -242,8 +208,8 @@ class OpenEdge.Core.Assert: end method. method public static void IsNull(input pcArgument as character, input pcName as character): - if pcArgument ne ? then - undo, throw new AssertionFailedError(substitute('&1 must be unknown', pcName), 0). + if pcArgument ne ? then + undo, throw new AssertionFailedError(substitute('&1 must be unknown':u, pcName), 0). end method. method public static void IsNull(input pcArgument as character): @@ -251,8 +217,8 @@ class OpenEdge.Core.Assert: end method. method public static void IsNull(input pcArgument as longchar, input pcName as character): - if pcArgument ne ? then - undo, throw new AssertionFailedError(substitute('&1 must be unknown', pcName), 0). + if pcArgument ne ? then + undo, throw new AssertionFailedError(substitute('&1 must be unknown':u, pcName), 0). end method. method public static void IsNull(input pcArgument as longchar): @@ -261,13 +227,13 @@ class OpenEdge.Core.Assert: method public static void NotNull(pcArgument as longchar, pcName as character): if pcArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be unknown', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be unknown':u, pcName), 0). end method. method public static void NotNull(pcArgument as longchar): NotNull(pcArgument, "argument"). end method. - + method public static void NotNullOrEmpty(input poArgument as ICollection, pcName as char): AssertObject:NotNullOrEmpty(poArgument, pcName). end method. @@ -320,9 +286,18 @@ class OpenEdge.Core.Assert: (strong assumption) and we're OK, Jack. If the lengths match, we are ok to convert and we try to trim. */ (iRawLength eq length(pcArgument) and trim(pcArgument) ne '':u) then - undo, throw new AssertionFailedError(substitute('&1 must be empty', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be empty':u, pcName), 0). end method. + method public static void IsEmpty(input pArgument as memptr, pcName as character): + if get-size(pArgument) ne 0 then + undo, throw new AssertionFailedError(substitute('&1 must be empty':u, pcName), 0). + end method. + + method public static void IsEmpty(input phArgument as memptr): + IsEmpty(phArgument, "argument"). + end method. + method public static void NotEmpty(input phArgument as handle): NotEmpty(phArgument, "argument"). end method. @@ -344,14 +319,14 @@ class OpenEdge.Core.Assert: end method. method public static void NotNullOrEmpty(pcArgument as character, pcName as character): - IF pcArgument EQ ? - THEN - UNDO, THROW NEW AssertionFailedError(SUBSTITUTE('&1 cannot be null',pcName),0). + if pcArgument eq ? + then + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName),0). // TRIM will strip off spaces etc. Since this is a character parameter, it's already in - // CPINTERNAL and so doesn't suffer from the issues described in NotEmpty(longchar, char). + // CPINTERNAL and so doesn't suffer from the issues described in NotEmpty(longchar, char). if trim(pcArgument) eq '':u then - undo, throw new AssertionFailedError(substitute('&1 cannot be empty', pcName),0). + undo, throw new AssertionFailedError(substitute('&1 cannot be empty':u, pcName),0). end method. method public static void NotNullOrEmpty(pcArgument as character): @@ -359,10 +334,9 @@ class OpenEdge.Core.Assert: end method. method public static void NotNullOrEmpty(input pcArgument as character extent, pcName as character): - define variable cLongCharArg as longchar no-undo. if extent(pcArgument) eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNullOrEmpty(input pcArgument as character extent): @@ -370,13 +344,12 @@ class OpenEdge.Core.Assert: end method. method public static void NotNull(input pcArgument as character extent, pcName as character): - define variable cLongCharArg as longchar no-undo. if extent(pcArgument) eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). if pcArgument[1] eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(input pcArgument as character extent): @@ -396,13 +369,22 @@ class OpenEdge.Core.Assert: (strong assumption) and we're OK, Jack. If the lengths match, we are ok to convert and we try to trim. */ (iRawLength eq length(pcArgument) and trim(pcArgument) eq '':u) then - undo, throw new AssertionFailedError(substitute('&1 cannot be empty', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be empty':u, pcName), 0). end method. method public static void NotEmpty(pcArgument as longchar): NotEmpty(pcArgument, "argument"). end method. + method public static void NotEmpty(pcArgument as memptr, pcName as character): + if get-size(pcArgument) = 0 then + undo, throw new AssertionFailedError(substitute('&1 cannot be empty':u, pcName), 0). + end method. + + method public static void NotEmpty(pcArgument as memptr): + NotEmpty(pcArgument, "argument"). + end method. + method public static void NotNullOrEmpty(pcArgument as longchar, pcName as character): Assert:NotNull(pcArgument, pcName). Assert:NotEmpty(pcArgument, pcName). @@ -411,10 +393,10 @@ class OpenEdge.Core.Assert: method public static void NotNullOrEmpty(pcArgument as longchar): NotNullOrEmpty(pcArgument, "argument"). end method. - + method public static void NonZero(piArgument as integer, pcName as character): if piArgument eq 0 then - undo, throw new AssertionFailedError(substitute('&1 cannot be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be zero':u, pcName), 0). end method. method public static void NonZero(piArgument as integer): @@ -423,7 +405,7 @@ class OpenEdge.Core.Assert: method public static void NonZero(piArgument as int64, pcName as character): if piArgument eq 0 then - undo, throw new AssertionFailedError(substitute('&1 cannot be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be zero':u, pcName), 0). end method. method public static void NonZero(piArgument as int64): @@ -437,7 +419,7 @@ class OpenEdge.Core.Assert: AssertArray:HasDeterminateExtent(piArgument, pcName). iMax = extent(piArgument). do iLoop = 1 to iMax: - Assert:NonZero(piArgument[iLoop], substitute('Extent &2 of &1', pcName, iLoop)). + Assert:NonZero(piArgument[iLoop], substitute('Extent &2 of &1':u, pcName, iLoop)). end. end method. @@ -452,7 +434,7 @@ class OpenEdge.Core.Assert: AssertArray:HasDeterminateExtent(piArgument, pcName). iMax = extent(piArgument). do iLoop = 1 to iMax: - Assert:NonZero(piArgument[iLoop], substitute('Extent &2 of &1', pcName, iLoop)). + Assert:NonZero(piArgument[iLoop], substitute('Extent &2 of &1':u, pcName, iLoop)). end. end method. @@ -462,7 +444,7 @@ class OpenEdge.Core.Assert: method public static void NonZero(piArgument as decimal, pcName as character): if piArgument eq 0 then - undo, throw new AssertionFailedError(substitute('&1 cannot be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be zero':u, pcName), 0). end method. method public static void NonZero(piArgument as decimal): @@ -478,19 +460,19 @@ class OpenEdge.Core.Assert: end method. method public static void IsAbstract(input poArgument as Progress.Lang.Class): - AssertObject:IsAbstract(poArgument). + AssertObject:IsAbstract(poArgument). end method. method public static void NotAbstract(input poArgument as Progress.Lang.Class): - AssertObject:NotAbstract(poArgument). + AssertObject:NotAbstract(poArgument). end method. method public static void IsFinal(input poArgument as Progress.Lang.Class): - AssertObject:IsFinal(poArgument). + AssertObject:IsFinal(poArgument). end method. method public static void NotFinal(input poArgument as Progress.Lang.Class): - AssertObject:NotFinal(poArgument). + AssertObject:NotFinal(poArgument). end method. method public static void IsType(input poArgument as Object extent, poType as Progress.Lang.Class): @@ -511,7 +493,7 @@ class OpenEdge.Core.Assert: @param character The name of the handle/variable being checked. */ method public static void NotNull(input phArgument as handle, input pcName as character): if not valid-handle(phArgument) then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(input phArgument as handle): @@ -526,7 +508,7 @@ class OpenEdge.Core.Assert: method public static void IsType(input phArgument as handle, input poCheckType as DataTypeEnum, input pcName as character): - AssertObject:IsType(phArgument, poCheckType, pcName). + AssertObject:IsType(phArgument, poCheckType, pcName). end method. method public static void IsType(input phArgument as handle, @@ -628,7 +610,7 @@ class OpenEdge.Core.Assert: Assert:NotNull(phArgument, pcName). if not phArgument:available then - undo, throw new AssertionFailedError(substitute('record in buffer &1 is not available', pcName), 0). + undo, throw new AssertionFailedError(substitute('record in buffer &1 is not available':u, pcName), 0). end method. method public static void IsAvailable(input phArgument as handle): @@ -640,7 +622,7 @@ class OpenEdge.Core.Assert: Assert:NotNull(phArgument, pcName). if phArgument:available then - undo, throw new AssertionFailedError(substitute('record in buffer &1 is available', pcName), 0). + undo, throw new AssertionFailedError(substitute('record in buffer &1 is available':u, pcName), 0). end method. method public static void NotAvailable(input phArgument as handle): @@ -649,11 +631,12 @@ class OpenEdge.Core.Assert: method public static void IsInteger(input pcArgument as character, input pcName as character): + {&_proparse_ prolint-nowarn(varusage)} define variable iCheckVal as integer no-undo. - iCheckVal = int(pcArgument) no-error. + iCheckVal = integer(pcArgument) no-error. if error-status:error then - undo, throw new AssertionFailedError(substitute('&1 is not an integer value', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is not an integer value':u, pcName), 0). end method. method public static void IsInteger(input pcArgument as character): @@ -662,11 +645,12 @@ class OpenEdge.Core.Assert: method public static void IsDecimal(input pcArgument as character, input pcName as character): + {&_proparse_ prolint-nowarn(varusage)} define variable iCheckVal as integer no-undo. iCheckVal = decimal(pcArgument) no-error. if error-status:error then - undo, throw new AssertionFailedError(substitute('&1 is not a decimal value', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is not a decimal value':u, pcName), 0). end method. method public static void IsDecimal(input pcArgument as character): @@ -675,11 +659,12 @@ class OpenEdge.Core.Assert: method public static void IsInt64(input pcArgument as character, input pcName as character): - define variable iCheckVal as integer no-undo. + {&_proparse_ prolint-nowarn(varusage)} + define variable iCheckVal as int64 no-undo. - iCheckVal = int64 (pcArgument) no-error. + iCheckVal = int64(pcArgument) no-error. if error-status:error then - undo, throw new AssertionFailedError(substitute('&1 is not an int64 value', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is not an int64 value':u, pcName), 0). end method. method public static void IsInt64(input pcArgument as character): @@ -690,21 +675,21 @@ class OpenEdge.Core.Assert: input pcName as character): /* deliberate not true */ if not (plArgument eq true) then - undo, throw new AssertionFailedError(substitute('&1 is not true', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is not true':u, pcName), 0). end method. method public static void IsFalse(input plArgument as logical, input pcName as character): /* deliberate not false */ if not (plArgument eq false) then - undo, throw new AssertionFailedError(substitute('&1 is not false', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is not false':u, pcName), 0). end method. method public static void IsUnknown(input plArgument as logical, input pcName as character): /* deliberate not ? */ if not (plArgument eq ?) then - undo, throw new AssertionFailedError(substitute('&1 is unknown', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is unknown':u, pcName), 0). end method. method public static void IsUnknown(input plArgument as logical): @@ -714,7 +699,7 @@ class OpenEdge.Core.Assert: method public static void NotTrue(input plArgument as logical, input pcName as character): if plArgument eq true then - undo, throw new AssertionFailedError(substitute('&1 is true', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is true':u, pcName), 0). end method. method public static void NotTrue(input plArgument as logical): @@ -724,7 +709,7 @@ class OpenEdge.Core.Assert: method public static void NotFalse(input plArgument as logical, input pcName as character): if plArgument eq false then - undo, throw new AssertionFailedError(substitute('&1 is false', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is false':u, pcName), 0). end method. method public static void NotFalse(input plArgument as logical): @@ -734,7 +719,7 @@ class OpenEdge.Core.Assert: method public static void NotUnknown(input plArgument as logical, input pcName as character): if plArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 is unknown', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 is unknown':u, pcName), 0). end method. method public static void NotUnknown(input plArgument as logical): @@ -743,7 +728,7 @@ class OpenEdge.Core.Assert: method public static void NotNull(piArgument as integer, pcName as character): if piArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(piArgument as integer): @@ -752,7 +737,7 @@ class OpenEdge.Core.Assert: method public static void NotNull(input ptArgument as date, pcName as character): if ptArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(input ptArgument as date): @@ -761,7 +746,7 @@ class OpenEdge.Core.Assert: method public static void NotNull(input ptArgument as datetime, pcName as character): if ptArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(input ptArgument as datetime): @@ -770,16 +755,43 @@ class OpenEdge.Core.Assert: method public static void NotNull(input ptArgument as datetime-tz, pcName as character): if ptArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(input ptArgument as datetime-tz): NotNull(ptArgument, "argument"). end method. + + method public static void IsNull(input ptArgument as date, pcName as character): + if not ptArgument eq ? then + undo, throw new AssertionFailedError(substitute('&1 must be null':u, pcName), 0). + end method. + + method public static void IsNull(input ptArgument as date): + IsNull(ptArgument, "argument"). + end method. + + method public static void IsNull(input ptArgument as datetime, pcName as character): + if not ptArgument eq ? then + undo, throw new AssertionFailedError(substitute('&1 must be null':u, pcName), 0). + end method. + + method public static void IsNull(input ptArgument as datetime): + IsNull(ptArgument, "argument"). + end method. + method public static void IsNull(input ptArgument as datetime-tz, pcName as character): + if not ptArgument eq ? then + undo, throw new AssertionFailedError(substitute('&1 must be null':u, pcName), 0). + end method. + + method public static void IsNull(input ptArgument as datetime-tz): + IsNull(ptArgument, "argument"). + end method. + method public static void IsNull(piArgument as integer, pcName as character): if piArgument ne ? then - undo, throw new AssertionFailedError(substitute('&1 must be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be null':u, pcName), 0). end method. method public static void IsNull(piArgument as integer): @@ -788,7 +800,7 @@ class OpenEdge.Core.Assert: method public static void NotNull(piArgument as int64, pcName as character): if piArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(piArgument as int64): @@ -797,7 +809,7 @@ class OpenEdge.Core.Assert: method public static void IsNull(piArgument as int64, pcName as character): if piArgument ne ? then - undo, throw new AssertionFailedError(substitute('&1 must be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be null':u, pcName), 0). end method. method public static void IsNull(piArgument as int64): @@ -806,7 +818,7 @@ class OpenEdge.Core.Assert: method public static void NotZero(piArgument as decimal, pcName as character): if piArgument eq 0 then - undo, throw new AssertionFailedError(substitute('&1 cannot be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be zero':u, pcName), 0). end method. method public static void NotZero(piArgument as decimal): @@ -815,7 +827,7 @@ class OpenEdge.Core.Assert: method public static void NotNull(pdArgument as decimal, pcName as character): if pdArgument eq ? then - undo, throw new AssertionFailedError(substitute('&1 cannot be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be null':u, pcName), 0). end method. method public static void NotNull(pdArgument as decimal): @@ -824,7 +836,7 @@ class OpenEdge.Core.Assert: method public static void IsNull(pdArgument as decimal, pcName as character): if pdArgument ne ? then - undo, throw new AssertionFailedError(substitute('&1 must be null', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be null':u, pcName), 0). end method. method public static void IsNull(pdArgument as decimal): @@ -842,7 +854,7 @@ class OpenEdge.Core.Assert: method public static void NotZero(piArgument as integer, pcName as character): if piArgument eq 0 then - undo, throw new AssertionFailedError(substitute('&1 cannot be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be zero':u, pcName), 0). end method. method public static void NotZero(piArgument as integer): @@ -851,7 +863,7 @@ class OpenEdge.Core.Assert: method public static void IsZero(piArgument as integer, pcName as character): if piArgument ne 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero':u, pcName), 0). end method. method public static void IsZero(piArgument as integer): @@ -861,7 +873,7 @@ class OpenEdge.Core.Assert: method public static void IsNegative(piArgument as integer, pcName as character): NotNull(piArgument, pcName). if piArgument ge 0 then - undo, throw new AssertionFailedError(substitute('&1 must be negative', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be negative':u, pcName), 0). end method. method public static void IsNegative(piArgument as integer): @@ -871,7 +883,7 @@ class OpenEdge.Core.Assert: method public static void IsPositive(piArgument as integer, pcName as character): NotNull(piArgument, pcName). if piArgument le 0 then - undo, throw new AssertionFailedError(substitute('&1 must be positive', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be positive':u, pcName), 0). end method. method public static void IsPositive(piArgument as integer): @@ -881,7 +893,7 @@ class OpenEdge.Core.Assert: method public static void IsZeroOrNegative(piArgument as integer, pcName as character): NotNull(piArgument, pcName). if piArgument gt 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero or negative', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero or negative':u, pcName), 0). end method. method public static void IsZeroOrNegative(piArgument as integer): @@ -891,7 +903,7 @@ class OpenEdge.Core.Assert: method public static void IsZeroOrPositive(piArgument as integer, pcName as character): NotNull(piArgument, pcName). if piArgument lt 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero or positive', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero or positive':u, pcName), 0). end method. method public static void IsZeroOrPositive(piArgument as integer): @@ -900,7 +912,7 @@ class OpenEdge.Core.Assert: method public static void NotZero(piArgument as int64, pcName as character): if piArgument eq 0 then - undo, throw new AssertionFailedError(substitute('&1 cannot be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 cannot be zero':u, pcName), 0). end method. method public static void NotZero(piArgument as int64): @@ -910,7 +922,7 @@ class OpenEdge.Core.Assert: method public static void IsZero(piArgument as int64, pcName as character): NotNull(piArgument, pcName). if piArgument ne 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero':u, pcName), 0). end method. method public static void IsZero(piArgument as int64): @@ -920,7 +932,7 @@ class OpenEdge.Core.Assert: method public static void IsNegative(piArgument as int64, pcName as character): NotNull(piArgument, pcName). if piArgument ge 0 then - undo, throw new AssertionFailedError(substitute('&1 must be negative', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be negative':u, pcName), 0). end method. method public static void IsNegative(piArgument as int64): @@ -930,7 +942,7 @@ class OpenEdge.Core.Assert: method public static void IsPositive(piArgument as int64, pcName as character): NotNull(piArgument, pcName). if piArgument le 0 then - undo, throw new AssertionFailedError(substitute('&1 must be positive', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be positive':u, pcName), 0). end method. method public static void IsPositive(piArgument as int64): @@ -940,7 +952,7 @@ class OpenEdge.Core.Assert: method public static void IsZeroOrNegative(piArgument as int64, pcName as character): NotNull(piArgument, pcName). if piArgument gt 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero or negative', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero or negative':u, pcName), 0). end method. method public static void IsZeroOrNegative(piArgument as int64): @@ -950,16 +962,16 @@ class OpenEdge.Core.Assert: method public static void IsZeroOrPositive(piArgument as int64, pcName as character): NotNull(piArgument, pcName). if piArgument lt 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero or positive', pcName), 0). - end method. + undo, throw new AssertionFailedError(substitute('&1 must be zero or positive':u, pcName), 0). + end method. method public static void IsZeroOrPositive(piArgument as int64): IsZeroOrPositive(piArgument, "argument"). - end method. + end method. method public static void IsZero(pdArgument as decimal, pcName as character): if pdArgument ne 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero':u, pcName), 0). end method. method public static void IsZero(pdArgument as decimal): @@ -969,7 +981,7 @@ class OpenEdge.Core.Assert: method public static void IsNegative(pdArgument as decimal, pcName as character): NotNull(pdArgument, pcName). if pdArgument ge 0 then - undo, throw new AssertionFailedError(substitute('&1 must be negative', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be negative':u, pcName), 0). end method. method public static void IsNegative(pdArgument as decimal): @@ -979,7 +991,7 @@ class OpenEdge.Core.Assert: method public static void IsPositive(pdArgument as decimal, pcName as character): NotNull(pdArgument, pcName). if pdArgument le 0 then - undo, throw new AssertionFailedError(substitute('&1 must be positive', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be positive':u, pcName), 0). end method. method public static void IsPositive(pdArgument as decimal): @@ -989,7 +1001,7 @@ class OpenEdge.Core.Assert: method public static void IsZeroOrNegative(pdArgument as decimal, pcName as character): NotNull(pdArgument, pcName). if pdArgument gt 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero or negative', pcName), 0). + undo, throw new AssertionFailedError(substitute('&1 must be zero or negative':u, pcName), 0). end method. method public static void IsZeroOrNegative(pdArgument as decimal): @@ -999,22 +1011,23 @@ class OpenEdge.Core.Assert: method public static void IsZeroOrPositive(pdArgument as decimal, pcName as character): NotNull(pdArgument, pcName). if pdArgument lt 0 then - undo, throw new AssertionFailedError(substitute('&1 must be zero or positive', pcName), 0). - end method. + undo, throw new AssertionFailedError(substitute('&1 must be zero or positive':u, pcName), 0). + end method. method public static void IsZeroOrPositive(pdArgument as decimal): IsZeroOrPositive(pdArgument, "argument"). end method. - /* Asserts that the input value can be converted to a logical value + /* Asserts that the input value can be converted to a logical value @param character A character expression to evaluate - @param character The format mask for the logical value + @param character The format mask for the logical value @param character The name of the argument @throws AssertionFailedError */ - method public static void IsLogical(input pcValue as character, + method public static void IsLogical(input pcValue as character, input pcMask as character, input pcName as character): + {&_proparse_ prolint-nowarn(varusage)} define variable lValue as logical no-undo. NotNullOrEmpty(pcMask, 'Format mask'). @@ -1022,14 +1035,14 @@ class OpenEdge.Core.Assert: assign lValue = logical(pcValue, pcMask) no-error. if error-status:error then undo, throw new AssertionFailedError( - substitute('&1 does not evaluate to a logical value with mask &2', + substitute('&1 does not evaluate to a logical value with mask &2':u, pcName, pcMask), 0). end method. /* Asserts that the input value can be converted to a logical value with the - default/built-in format mask (see doc) + default/built-in format mask (see doc) @param character A character expression to evaluate @throws AssertionFailedError */ @@ -1038,21 +1051,19 @@ class OpenEdge.Core.Assert: end method. /* Asserts that the input value can be converted to a logical value with the - default/built-in format mask (see doc) + default/built-in format mask (see doc) @param character A character expression to evaluate @param character The name of the argument @throws AssertionFailedError */ - method public static void IsLogical(input pcValue as character, + method public static void IsLogical(input pcValue as character, input pcName as character): + {&_proparse_ prolint-nowarn(varusage)} define variable lValue as logical no-undo. assign lValue = logical(pcValue) no-error. if error-status:error then - undo, throw new AssertionFailedError( - substitute('&1 does not evaluate to a logical value', - pcName), - 0). + undo, throw new AssertionFailedError(substitute('&1 does not evaluate to a logical value':u, pcName), 0). end method. /** Raises an AssertionFailedError. @@ -1092,13 +1103,12 @@ class OpenEdge.Core.Assert: assign idx = lookup(pValue, pList, pDelim). if idx eq 0 // value not in list or idx eq ? // value or list are ? - then - undo, throw new AssertionFailedError( - substitute('&1 ("&2") is not in the list: &3', - pName, - pValue, - pList ), - 0). + then do: + /* Combine any character substitutions first, then longchar substitutions. */ + define variable subMessage as longchar no-undo. + assign subMessage = substitute('&1 ("&2") is not in the list: &3':u, pName, pValue, '&1':u). + undo, throw new AssertionFailedError(substitute(subMessage, pList), 0). + end. end method. /* Asserts that the input value is NOT in the provided list @@ -1109,7 +1119,7 @@ class OpenEdge.Core.Assert: method static public void NotIn(input pValue as character, input pList as longchar, input pName as character): - NotIn(pValue, pList, ',':u, pName). + NotIn(pValue, pList, ',':u, pName). end method. /* Asserts that the input value is NOT in the provided list @@ -1131,13 +1141,12 @@ class OpenEdge.Core.Assert: NotEqual(pValue, pDelim). // covers positive values (is a valid value, not in the list) and ? (unknown value) - if not lookup(pValue, pList, pDelim) eq 0 then - undo, throw new AssertionFailedError( - substitute('&1 ("&2") is in the list: &3', - pName, - pValue, - pList ), - 0). + if not lookup(pValue, pList, pDelim) eq 0 then do: + /* Combine any character substitutions first, then longchar substitutions. */ + define variable subMessage as longchar no-undo. + assign subMessage = substitute('&1 ("&2") is in the list: &3':u, pName, pValue, '&1':u). + undo, throw new AssertionFailedError(substitute(subMessage, pList), 0). + end. end method. end class. diff --git a/src/OpenEdge/Core/Json/JsonConverter.cls b/src/OpenEdge/Core/Json/JsonConverter.cls index e9daa260..3a2467d9 100644 --- a/src/OpenEdge/Core/Json/JsonConverter.cls +++ b/src/OpenEdge/Core/Json/JsonConverter.cls @@ -1,5 +1,5 @@ /* ************************************************************************************************************************* -Copyright (c) 2019-2020 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +Copyright (c) 2019-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : JsonConverter @@ -69,12 +69,14 @@ using Progress.Lang.Object. using Progress.Lang.LockConflict. using Progress.Lang.SoapFaultError. using OpenEdge.Core.Json.JsonSerializer. +using OpenEdge.Core.JsonDataTypeEnum. class OpenEdge.Core.Json.JsonConverter: /* Default constructor. This is private so that this class cannot be instantiated - it's intended to be used with static members only */ constructor private JsonConverter(): + //This is private so that this class cannot be instantiated - it's intended to be used with static members only end constructor. /* Converts an array of objects into into a JSONArray @@ -336,7 +338,6 @@ class OpenEdge.Core.Json.JsonConverter: define variable errData as JsonObject no-undo. define variable errList as JsonArray no-undo. define variable loop as integer no-undo. - define variable cRetVal as character no-undo. define variable cnt as integer no-undo. define variable errProp as Progress.Reflect.Property no-undo. define variable innerErr as Progress.Lang.Error no-undo. @@ -512,10 +513,11 @@ class OpenEdge.Core.Json.JsonConverter: input pValue as Progress.Lang.Object, input pJson as JsonArray ): define variable hdlVal as handle no-undo. +/* define variable objectArray as Object extent no-undo. define variable cnt as integer no-undo. define variable loop as integer no-undo. - + */ Assert:NotNull(pJson, 'Json array'). Assert:NotNull(pIdx, 'Array index'). @@ -594,11 +596,12 @@ class OpenEdge.Core.Json.JsonConverter: input pJson as JsonObject ): define variable methodName as character no-undo. define variable hdlVal as handle no-undo. +/* define variable objectArray as Progress.Lang.Object extent no-undo. define variable cnt as integer no-undo. define variable maxCnt as integer no-undo. define variable keyValue as OpenEdge.Core.String no-undo. - + */ Assert:NotNull(pJson, 'Json object'). Assert:NotNull(pKey, 'Tuple key'). @@ -684,6 +687,8 @@ class OpenEdge.Core.Json.JsonConverter: /* Merges (combines) two JSON objects. Properties are copied from the SOURCE into the TARGET. + Properties are copied from the SOURCE into the TARGET. Property types + must match, otherwise an error is thrown. If the property exists in the target, it is overwritten by the source property if the pOverwriteExisting flag is TRUE. @@ -698,6 +703,7 @@ class OpenEdge.Core.Json.JsonConverter: define variable names as longchar extent no-undo. define variable propName as character no-undo. define variable methodName as character no-undo. + define variable srcType as integer no-undo. Assert:NotNull(pTarget, 'Target JSON object'). @@ -707,19 +713,24 @@ class OpenEdge.Core.Json.JsonConverter: assign names = pSource:GetNames() cnt = extent(names) . + PROP-LOOP: do loop = 1 to cnt: + {&_proparse_ prolint-nowarn(overflow)} assign propName = string(names[loop]) methodName = 'Add':u + srcType = pSource:GetType(propName) . if pTarget:Has(propName) then do: + Assert:Equals(JsonDataTypeEnum:GetEnum(srcType), + JsonDataTypeEnum:GetEnum(pTarget:GetType(propName))). if pOverwriteExisting then assign methodName = 'Set':u. else - next. + next PROP-LOOP. end. - case pSource:GetType(propName): + case srcType: when JsonDataType:NULL then dynamic-invoke(pTarget, methodName + 'Null':u, propName). when JsonDataType:BOOLEAN then @@ -730,10 +741,11 @@ class OpenEdge.Core.Json.JsonConverter: dynamic-invoke(pTarget, methodName, propName, pSource:GetJsonObject(propName)). when JsonDataType:STRING then // The Add/Set methods have overloads that accept longchar, so we won't run into the AddNumber issue - dynamic-invoke(pTarget, methodName, propName, pSource:GetJsonText(propName)). + dynamic-invoke(pTarget, methodName, propName, pSource:GetLongchar(propName)). when JsonDataType:NUMBER then // Because GetJsonText() returns a LONGCHAR, the reflection fails. It is extremely unlikely // that a number would have >2GB's worth of digits, so just STRING() it. + {&_proparse_ prolint-nowarn(overflow)} dynamic-invoke(pTarget, methodName + 'Number':u, propName, string(pSource:GetJsonText(propName))). end case. end. @@ -741,19 +753,28 @@ class OpenEdge.Core.Json.JsonConverter: /* This method converts a JSON property to an instance of an IPRimitiveHolder. - If the inputs are invalid, an object represeting the unknown value is returned. - If the property value cannot be converted, an object represeting the unknown value is returned. - Thie methods will try to convert JSON Strings into date/time/tz or memptr values before returning a string - JSON objects and arrayas are returned as Json text values (ie flattened) + Input JSON data types: + NULL -> OE.Core.String(?) + OBJECT, ARRAY -> OE.Core.String(GetJsonText) + BOOLEAN -> OE.Core.LogicalValue() + NUMBER -> if the value has a '.', OE.Core.Decimal(), else OE.Core.Integer() + STRING -> + If the value looks like YYYY-MM-DD, try to parse and return OE.Core.DateHolder + If the value looks like YYYY-MM-DDTHH:MM:SS.SSS, try to parse and return OE.Core.DateTimeHolder + If the value looks like YYYY-MM-DDTHH:MM:SS.SSS+HH:MM, try to parse and return OE.Core.DateTimeTzHolder + If the value looks like a base64-encoded value (last 2 chars are ==), return as OE.Core.Memptr + If the 'looks like' or parsing fail, return OE.Core.String() + + If the property value cannot be converted, an object represeting the unknown value is returned (same as NULL). @param JsonObject The object @param character The property name @return IPrimitiveHolder The representative value */ method static public IPrimitiveHolder ToScalar(input pObject as JsonObject, input pName as character): - define variable strValue as character no-undo. - define variable pos as integer no-undo. - define variable cnt as integer no-undo. + define variable strValue as longchar no-undo. + define variable pos as int64 no-undo. + define variable cnt as int64 no-undo. define variable dtzVal as datetime-tz no-undo. define variable mData as memptr no-undo. @@ -762,11 +783,11 @@ class OpenEdge.Core.Json.JsonConverter: or pName eq ? or not pObject:Has(pName) then - return new String(StringConstant:UNKNOWN). + return String:Unknown(). case pObject:GetType(pName): when JsonDataType:NULL then - return new String(StringConstant:UNKNOWN). + return String:Unknown(). when JsonDataType:BOOLEAN then return new LogicalValue(pObject:GetLogical(pName)). when JsonDataType:NUMBER then @@ -787,8 +808,12 @@ class OpenEdge.Core.Json.JsonConverter: /* This value could be a - string - iso-date / time / -tz - - memptr / base64-encoded value */ - assign strValue = pObject:GetJsonText(pName) + - memptr / base64-encoded value + + DO NOT USE GetJsonText here since any encoded values - like slashes in a URL + will be returned. Ie http://localhost is http:\/\/localhost when GetJsonText is + called, and not the correct un-encoded value. */ + assign strValue = pObject:GetLongchar(pName) cnt = length(strValue, 'raw':u) pos = index(strValue, '-':u) . @@ -797,6 +822,7 @@ class OpenEdge.Core.Json.JsonConverter: case cnt: // DATE YYYY-MM-DD when 10 then + do: if index(strValue, '-':u, pos + 1) gt 0 then do: assign dtzVal = datetime-tz(integer(substring(strValue, 6, 2)), @@ -808,11 +834,15 @@ class OpenEdge.Core.Json.JsonConverter: if not error-status:error then return new DateHolder(date(dtzVal)). else - assign error-status:error = no. + return new String(strValue). end. + else + return new String(strValue). + end. // DATETIME YYYY-MM-DDTHH:MM:SS.SSS // 12345678901234567890123 when 23 then + do: if index(strValue, '-':u, pos + 1) gt 0 and index(strValue, 'T':u, pos + 1) gt 0 then @@ -828,22 +858,33 @@ class OpenEdge.Core.Json.JsonConverter: if not error-status:error then return new DateTimeHolder(datetime(date(dtzVal), mtime(dtzVal))). else - assign error-status:error = no. + return new String(strValue). end. + else + return new String(strValue). + end. // DATETIME-TZ YYYY-MM-DDTHH:MM:SS.SSS+HH:MM when 29 then - if index(strValue, '-':u, pos + 1) gt 0 + do: + if index(strValue, '-':u, pos + 1) gt 0 and index(strValue, 'T':u, pos + 1) gt 0 then do: - assign dtzVal = TimeStamp:ToABLDateTimeTzFromISO(strValue) - no-error. + {&_proparse_ prolint-nowarn(overflow)} + assign dtzVal = TimeStamp:ToABLDateTimeTzFromISO(string(strValue)) + no-error. if not error-status:error then return new DateTimeTzHolder(dtzVal). else - assign error-status:error = no. + return new String(strValue). end. - end. + else + return new String(strValue). + end. + // not a date, just a string + otherwise + return new String(strValue). + end case. else // base64-encoded if substring(strValue, cnt - 2) eq '==':u then @@ -862,24 +903,38 @@ class OpenEdge.Core.Json.JsonConverter: end. end case. - return new String(StringConstant:UNKNOWN). + return String:Unknown(). + finally: + assign error-status:error = no. + end finally. end method. /* This method converts a JSON array element to an instance of an IPrimitiveHolder. - If the inputs are invalid, or the index is not in range, an object represeting the unknown value is returned. - If the property value cannot be converted, an object represeting the unknown value is returned. - Thie methods will try to convert JSON Strings into date/time/tz or memptr values before returning a string - JSON objects and arrayas are returned as Json text values (ie flattened) + If the inputs are invalid, or the index is not in range, a OE.Core.String object represeting the unknown value is returned. - @param JsonArray The object - @param integer The element index + Input JSON data types: + NULL -> OE.Core.String(?) + OBJECT, ARRAY -> OE.Core.String(GetJsonText) + BOOLEAN -> OE.Core.LogicalValue() + NUMBER -> if the value has a '.', OE.Core.Decimal(), else OE.Core.Integer() + STRING -> + If the value looks like YYYY-MM-DD, try to parse and return OE.Core.DateHolder + If the value looks like YYYY-MM-DDTHH:MM:SS.SSS, try to parse and return OE.Core.DateTimeHolder + If the value looks like YYYY-MM-DDTHH:MM:SS.SSS+HH:MM, try to parse and return OE.Core.DateTimeTzHolder + If the value looks like a base64-encoded value (last 2 chars are ==), return as OE.Core.Memptr + If the 'looks like' or parsing fail, return OE.Core.String() + + If the property value cannot be converted, an object represeting the unknown value is returned (same as NULL). + + @param JsonArray The object + @param integer The element index @return IPrimitiveHolder The representative value */ method static public IPrimitiveHolder ToScalar(input pArray as JsonArray, input pIdx as integer): - define variable strValue as character no-undo. - define variable pos as integer no-undo. - define variable cnt as integer no-undo. + define variable strValue as longchar no-undo. + define variable pos as int64 no-undo. + define variable cnt as int64 no-undo. define variable dtzVal as datetime-tz no-undo. define variable mData as memptr no-undo. @@ -887,11 +942,11 @@ class OpenEdge.Core.Json.JsonConverter: or pIdx eq ? or pArray:Length lt pIdx then - return new String(StringConstant:UNKNOWN). + return String:Unknown(). case pArray:GetType(pIdx): when JsonDataType:NULL then - return new String(StringConstant:UNKNOWN). + return String:Unknown(). when JsonDataType:BOOLEAN then return new LogicalValue(pArray:GetLogical(pIdx)). when JsonDataType:NUMBER then @@ -903,17 +958,20 @@ class OpenEdge.Core.Json.JsonConverter: return new OpenEdge.Core.Integer(pArray:GetInt64(pIdx)). end. when JsonDataType:ARRAY or - when JsonDataType:OBJECT then + when JsonDataType:OBJECT then return new String(pArray:GetJsonText(pIdx)). otherwise do: - /* This value could be a + /* This value could be a - string - iso-date - memptr / base64-encoded value - */ - assign strValue = pArray:GetJsonText(pIdx) + + DO NOT USE GetJsonText here since any encoded values - like slashes in a URL + will be returned. Ie http://localhost is http:\/\/localhost when GetJsonText is + called, and not the correct un-encoded value. */ + assign strValue = pArray:GetLongchar(pIdx) cnt = length(strValue, 'raw':u) pos = index(strValue, '-':u) . @@ -929,19 +987,21 @@ class OpenEdge.Core.Json.JsonConverter: integer(substring(strValue, 9, 2)), integer(substring(strValue, 1, 4)), 0, - 0 ) + 0 ) no-error. if not error-status:error then return new DateHolder(date(dtzVal)). else - assign error-status:error = no. + return new String(strValue). end. + else + return new String(strValue). end. // DATETIME YYYY-MM-DDTHH:MM:SS.SSS // 12345678901234567890123 when 23 then do: - if index(strValue, '-':u, pos + 1) gt 0 + if index(strValue, '-':u, pos + 1) gt 0 and index(strValue, 'T':u, pos + 1) gt 0 then do: @@ -956,24 +1016,32 @@ class OpenEdge.Core.Json.JsonConverter: if not error-status:error then return new DateTimeHolder(datetime(date(dtzVal), mtime(dtzVal))). else - assign error-status:error = no. + return new String(strValue). end. + else + return new String(strValue). end. // DATETIME-TZ YYYY-MM-DDTHH:MM:SS.SSS+HH:MM when 29 then do: - if index(strValue, '-':u, pos + 1) gt 0 + if index(strValue, '-':u, pos + 1) gt 0 and index(strValue, 'T':u, pos + 1) gt 0 then do: - assign dtzVal = TimeStamp:ToABLDateTimeTzFromISO(strValue) no-error. + {&_proparse_ prolint-nowarn(overflow)} + assign dtzVal = TimeStamp:ToABLDateTimeTzFromISO(string(strValue)) no-error. if not error-status:error then return new DateTimeTzHolder(dtzVal). else - assign error-status:error = no. + return new String(strValue). end. + else + return new String(strValue). end. - end. + // not a date, just a string + otherwise + return new String(strValue). + end case. else // base64-encoded if substring(strValue, cnt - 2) eq '==':u then @@ -983,7 +1051,8 @@ class OpenEdge.Core.Json.JsonConverter: if not error-status:error then return new OpenEdge.Core.Memptr(mData). else - assign error-status:error = no. + return new String(strValue). + finally: set-size(mData) = 0. end finally. @@ -993,7 +1062,10 @@ class OpenEdge.Core.Json.JsonConverter: end. end case. - return new String(StringConstant:UNKNOWN). + return String:Unknown(). + finally: + assign error-status:error = no. + end finally. end method. /* Converts a JSON array to an instance of a IObjectArrayHolder @@ -1060,11 +1132,12 @@ class OpenEdge.Core.Json.JsonConverter: if valid-object(pArray) then assign cnt = pArray:Length. + ITEM-LOOP: do loop = 1 to cnt: // ABL Arrays only have a single type, so we infer that from the first non-null value we find case pArray:GetType(loop): // loop over nulls - when JsonDataType:NULL then next. + when JsonDataType:NULL then next ITEM-LOOP. when JsonDataType:BOOLEAN then return new LogicalArrayHolder(pArray:GetLogical(1, cnt)). // just stringify non-scalar JSON when JsonDataType:OBJECT or @@ -1078,7 +1151,7 @@ class OpenEdge.Core.Json.JsonConverter: if type-of(ph, IDecimalHolder) then return new DecimalArrayHolder(pArray:GetDecimal(1, cnt)). - if type-of(ph, IInt64Holder) + if type-of(ph, IInt64Holder) or type-of(ph, IIntegerHolder) then return new IntegerArrayHolder(pArray:GetInt64(1, cnt)). @@ -1147,7 +1220,7 @@ class OpenEdge.Core.Json.JsonConverter: return Progress.Lang.Enum:ToObject(pEnumType:TypeName, pArray:GetInt64(pIdx)). when JsonDataType:STRING then return Progress.Lang.Enum:ToObject(pEnumType:TypeName, pArray:GetCharacter(pIdx)). - end. + end case. return ?. end method. @@ -1186,7 +1259,7 @@ class OpenEdge.Core.Json.JsonConverter: return Progress.Lang.Enum:ToObject(pEnumType:TypeName, pObject:GetInt64(pName)). when JsonDataType:STRING then return Progress.Lang.Enum:ToObject(pEnumType:TypeName, pObject:GetCharacter(pName)). - end. + end case. return ?. end method. diff --git a/src/OpenEdge/Core/Json/JsonSerializer.cls b/src/OpenEdge/Core/Json/JsonSerializer.cls index 0031f985..53f9e64f 100644 --- a/src/OpenEdge/Core/Json/JsonSerializer.cls +++ b/src/OpenEdge/Core/Json/JsonSerializer.cls @@ -1,5 +1,5 @@ /* ************************************************************************************************************************* -Copyright (c) 2019-2020 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. +Copyright (c) 2019-2021 by Progress Software Corporation and/or one of its subsidiaries or affiliates. All rights reserved. ************************************************************************************************************************** */ /*------------------------------------------------------------------------ File : OpenEdge.Core.Json.JsonSerializer @@ -14,13 +14,6 @@ using OpenEdge.Core.Assert. using OpenEdge.Core.Json.IJsonSerializer. using OpenEdge.Core.Json.JsonPropertyHelper. using Progress.IO.JsonSerializer. -using Progress.Json.ObjectModel.JsonArray. -using Progress.Json.ObjectModel.JsonConstruct. -using Progress.Json.ObjectModel.JsonDataType. -using Progress.Json.ObjectModel.JsonObject. -using Progress.Json.ObjectModel.ObjectModelParser. -using Progress.Lang.ParameterList. - &IF PROVERSION BEGINS "12" &THEN using Progress.IO.MemoryInputStream. using Progress.IO.MemoryOutputStream. @@ -32,6 +25,12 @@ using OpenEdge.Core.MemptrOutputStream. &GLOBAL-DEFINE MemInputStream MemptrInputStream &GLOBAL-DEFINE MemOutputStream MemptrOutputStream &ENDIF +using Progress.Json.ObjectModel.JsonArray. +using Progress.Json.ObjectModel.JsonConstruct. +using Progress.Json.ObjectModel.JsonDataType. +using Progress.Json.ObjectModel.JsonObject. +using Progress.Json.ObjectModel.ObjectModelParser. +using Progress.Lang.ParameterList. class OpenEdge.Core.Json.JsonSerializer: // Constant names for various properties used for de/serialising @@ -48,6 +47,7 @@ class OpenEdge.Core.Json.JsonSerializer: This is private so that this class cannot be instantiated - it's intended to be used with static members only */ constructor private JsonSerializer(): + /* Default constructor */ end constructor. /* Serialises an array of objects to JSON. @@ -67,6 +67,7 @@ class OpenEdge.Core.Json.JsonSerializer: define variable loop as integer no-undo. define variable objRefs as character no-undo. define variable idx as integer no-undo. + {&_proparse_ prolint-nowarn(varusage)} define variable ok as logical no-undo. define variable delim as character no-undo. diff --git a/src/OpenEdge/Core/String.cls b/src/OpenEdge/Core/String.cls index bd23ddea..64311f2e 100644 --- a/src/OpenEdge/Core/String.cls +++ b/src/OpenEdge/Core/String.cls @@ -229,15 +229,21 @@ class OpenEdge.Core.String serializable return cArray. if iMax eq 0 then + {&_proparse_ prolint-nowarn(overflow)} assign extent(cArray) = 1 // this should only happen of value and delimiter are empty cArray[1] = pcValue . else - assign - extent(cArray) = iMax + assign extent(cArray) = iMax. + if iMax eq 0 then + {&_proparse_ prolint-nowarn(overflow)} + assign extent(cArray) = 1 + // this should only happen of value and delimiter are empty + cArray[1] = pcValue . - + else + assign extent(cArray) = iMax. do iLoop = 1 to iMax: assign cArray[iLoop] = entry(iLoop, pcValue, pcDelimiter). end. @@ -314,6 +320,7 @@ class OpenEdge.Core.String serializable if this-object:Size ge 30000 then assign cValue = substring(this-object:Value, 1, 29985) + ' <<...MORE...>>':u. else + {&_proparse_ prolint-nowarn(overflow)} assign cValue = this-object:Value. return cValue. diff --git a/src/OpenEdge/Core/Util/AnnotationWriter.cls b/src/OpenEdge/Core/Util/AnnotationWriter.cls index 29f32132..6c7cec38 100644 --- a/src/OpenEdge/Core/Util/AnnotationWriter.cls +++ b/src/OpenEdge/Core/Util/AnnotationWriter.cls @@ -1,5 +1,5 @@ /************************************************ -Copyright (c) 2020 by Progress Software Corporation. All rights reserved. +Copyright (c) 2020-2021 by Progress Software Corporation. All rights reserved. *************************************************/ /*------------------------------------------------------------------------ File : AnnotationWriter @@ -79,8 +79,8 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: define private variable mNonUserProperties as character no-undo initial 'program,data,method,constructor,destructor,property,event,procedure,function,user':u. - /* Default constructor */ constructor public AnnotationWriter(): + /* Default constructor */ end constructor. /* Constructor. @@ -150,13 +150,17 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: // look for slash-delimited files assign pAblProgram = replace(pAblProgram, StringConstant:BACKSLASH, '/':u). if can-find(first pSource where pSource.File-name eq pAblProgram) then - find first pSource where pSource.File-name eq pAblProgram. + /* Intentional lack of no-error on find. */ + {&_proparse_ prolint-nowarn(findnoerror)} + find first pSource where pSource.File-name eq pAblProgram. else // look for backslash-delimited files do: assign pAblProgram = replace(pAblProgram, '/':u, StringConstant:BACKSLASH). if can-find(first pSource where pSource.File-name eq pAblProgram) then - find first pSource where pSource.File-name eq pAblProgram. + /* Intentional lack of no-error on find. */ + {&_proparse_ prolint-nowarn(findnoerror)} + find first pSource where pSource.File-name eq pAblProgram. end. end method. @@ -172,7 +176,6 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: define variable progData as JsonObject no-undo. define variable refName as character no-undo. define buffer lbSource for Source. - define buffer lbRef for Reference. FindSourceRecord(pAblProgram, buffer lbSource). assign annoData = new JsonObject() @@ -187,17 +190,7 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: return annoData. end. - find lbRef - where lbRef.Source-guid eq lbSource.Source-guid - and lbRef.Reference-type eq 'CLASS':u - no-error. - - if available lbRef then - assign refName = lbRef.Object-identifier. - else - assign refName = replace(lbSource.File-name, StringConstant:BACKSLASH, '/':u) - refName = entry(num-entries(refName, '/':u), refName, '/':u) - . + assign refName = GetRefName(buffer lbSource). annoData:Add(refName, progData). return annoData. @@ -213,24 +206,17 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: define variable progArray as JsonArray no-undo. define variable refName as character no-undo. define buffer lbSource for Source. - define buffer lbRef for Reference. + assign annoData = new JsonObject(). + {&_proparse_ prolint-nowarn(wholeindex)} for each lbSource where lbSource.File-num eq 1 by lbSource.File-name: - find lbRef - where lbRef.Source-guid eq lbSource.Source-guid - and lbRef.Reference-type eq 'CLASS':u - no-error. - assign progData = ExtractAnnotations(buffer lbSource, pUserMap). - if available lbRef then - assign refName = lbRef.Object-identifier. - else - assign refName = replace(lbSource.File-name, StringConstant:BACKSLASH, '/':u) - refName = entry(num-entries(refName, '/':u), refName, '/':u) - . + assign progData = ExtractAnnotations(buffer lbSource, pUserMap) + refName = GetRefName(buffer lbSource) + . // in case we have dups if annoData:Has(refName) then do: @@ -253,6 +239,37 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: return annoData. end method. + /* Gets the 'ref name' - the class/interface or procedure name + for a source xref. + + The ref name has one of two forms + 1) For CLASS or INTERFACE xrefs, the type name + 2) For all others, the file base name (ie foo.p rather than bar/foo.p) + + @param buffer Source The source record + @return character The type or procedure name. Is UNKNOWN if the source buffer is not available. */ + method protected character GetRefName(buffer pSource for Source): + define variable refName as character no-undo. + define buffer lbRef for Reference. + + if not available pSource then + return ?. + + find lbRef + where lbRef.Source-guid eq pSource.Source-guid + and ( lbRef.Reference-type eq 'CLASS':u + or lbRef.Reference-type eq 'INTERFACE':u ) + no-error. + if available lbRef then + assign refName = lbRef.Object-identifier. + else + assign refName = replace(pSource.File-name, StringConstant:BACKSLASH, '/':u) + refName = entry(num-entries(refName, '/':u), refName, '/':u) + . + + return refName. + end method. + /* Extracts annotations for a single program @param buffer Source The current program whose annotations to extract @@ -265,23 +282,47 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: define variable groupData as JsonObject no-undo. define variable annotations as JsonObject no-undo. - define buffer lbSrc for Source. + define buffer lbSrc for Source. + define buffer lbRef for Reference. assign annotations = new JsonObject() groupData = new JsonObject() . annotations:Add('meta':u, groupData). - groupData:Add('version':u, '1.0.0':u). + groupData:Add('version':u, '1.1.0':u). if not available pSource then return annotations. groupData:Add('fileName':u, pSource.File-name). - annotations:AddNull('program':u). - assign pos = r-index(pSource.File-name, '.':u). - if substring(pSource.File-name, pos + 1) eq 'cls':u then + /* What kind of a program is this? */ + find lbRef + where lbRef.Source-guid eq pSource.Source-guid + and ( lbRef.Reference-type eq 'CLASS':u + or lbRef.Reference-type eq 'INTERFACE':u ) + no-error. + if available lbRef then + groupData:Add('programType':u, lbRef.Reference-type). + else + do: + // workaround for XREF not containing this info + find lbRef + where lbRef.Source-guid eq pSource.Source-guid + and lbRef.Reference-type eq 'STRING':u + and lbRef.Object-identifier eq 'Progress.Lang.Enum':u + no-error. + if available lbRef then + groupData:Add('programType':u, 'ENUM':u). + else + groupData:Add('programType':u, 'PROCEDURE':u). + end. + + annotations:AddNull('program':u). + // this is an OOABL type + if available lbRef then do: + groupData:Add('typeName':u, lbRef.Object-identifier). groupData:AddNull('inherits':u). groupData:AddNull('implements':u). @@ -335,6 +376,7 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: define variable propGroup as character no-undo. define variable annotGroup as JsonObject no-undo. define variable valueData as JsonObject no-undo. + {&_proparse_ prolint-nowarn(varusage)} define variable jsonData as JsonObject no-undo. define variable valueSet as JsonArray no-undo. define variable groupName as character extent 2 no-undo. @@ -417,7 +459,7 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: else valueSet:Add(annoValue). end. - end. /* case */ + end case. /* case */ else if pos eq 0 and annoValue eq "" then valueData:AddNull(annoKey). /* Just add a null value for this key. */ @@ -481,7 +523,7 @@ class OpenEdge.Core.Util.AnnotationWriter implements ISupportInitialize: do: find lbCls where lbCls.Source-guid eq lbReference.Source-guid - and lbCls.Ref-seq eq lbReference.Ref-seq. + and lbCls.Ref-seq eq lbReference.Ref-seq no-error. assign extent(listValue) = ? listValue = String:Split(lbCls.Inherited-list, StringConstant:SPACE) diff --git a/src/OpenEdge/Core/Util/MathUtil.cls b/src/OpenEdge/Core/Util/MathUtil.cls index 0bbaa38d..79d1d051 100644 --- a/src/OpenEdge/Core/Util/MathUtil.cls +++ b/src/OpenEdge/Core/Util/MathUtil.cls @@ -1,5 +1,5 @@ /************************************************ -Copyright (c) 2014, 2017, 2020 by Progress Software Corporation. All rights reserved. +Copyright (c) 2014, 2017, 2020-2021 by Progress Software Corporation. All rights reserved. *************************************************/ /*------------------------------------------------------------------------ File : MathUtil @@ -58,7 +58,7 @@ class OpenEdge.Core.Util.MathUtil: assign rHex = hex-decode( cHex ) iMax = length( rHex, 'raw') . - do iLoop = 1 to iMax. + do iLoop = 1 to iMax: assign iResult = iResult * 256 iResult = iResult + get-byte(rHex, iLoop) . diff --git a/src/OpenEdge/Core/Util/UTF8Encoder.cls b/src/OpenEdge/Core/Util/UTF8Encoder.cls index ac4cb457..d6da6f9f 100644 --- a/src/OpenEdge/Core/Util/UTF8Encoder.cls +++ b/src/OpenEdge/Core/Util/UTF8Encoder.cls @@ -1,6 +1,6 @@ /************************************************ -Copyright (c) 2020 by Progress Software Corporation. All rights reserved. -*************************************************/ +Copyright (c) 2020-2021 by Progress Software Corporation. All rights reserved. +*************************************************/ /*------------------------------------------------------------------------ File : UTF8Encoder Purpose : Encodes UTF-8 strings, characters and values to and from Unicode codepoints @@ -36,9 +36,7 @@ class OpenEdge.Core.Util.UTF8Encoder: define variable memData as memptr no-undo. define variable utf8Codepoint as integer no-undo. define variable unicodeCodePt as integer no-undo. - define variable numBytes as integer no-undo. define variable rawCodepoint as raw no-undo. - define variable byte as integer no-undo. define variable encString as longchar no-undo. if length(pString) eq 0 @@ -105,8 +103,8 @@ class OpenEdge.Core.Util.UTF8Encoder: - then the length of the hex value is determined using the mask. @param longchar The encoded string - @param character The escape sequence, by default U+ but for JSON may need to be \u . - May include &1 for more complex sequences (like Swift's \u{&1} ) + @param character The escape sequence, by default U+ but for JSON may need to be \u . + May include &1 for more complex sequences (like Swift's \u{&1} ) @return longchar A decoded UTF-8 string */ method static public longchar Decode(input pString as longchar, input pEscapeSeq as character): @@ -119,6 +117,7 @@ class OpenEdge.Core.Util.UTF8Encoder: define variable escLen as integer no-undo. define variable escCloseLen as integer no-undo. define variable escCloseChr as character no-undo. + define variable substrLen as integer no-undo. fix-codepage(decodedString) = 'utf-8':u. @@ -130,7 +129,7 @@ class OpenEdge.Core.Util.UTF8Encoder: else assign escCloseChr = substring(pEscapeSeq, pos + 2) escCloseLen = length(escCloseChr) - pEscapeSeq = substring(pEscapeSeq, 1, pos - 1) + pEscapeSeq = substring(pEscapeSeq, 1, pos - 1) . assign startAt = 1 @@ -146,9 +145,13 @@ class OpenEdge.Core.Util.UTF8Encoder: assign hexChar = substring(pString, pos + escLen, hexLen) utf8 = UnicodeToUtf8(hexChar) - decodedString = decodedString - + substring(pString, startAt, (pos - startAt)) - + chr(utf8, 'utf-8':u, 'utf-8':u) + substrLen = pos - startAt + . + // workaround for OCTA-36340 + if substrLen gt 0 then + assign decodedString = decodedString + substring(pString, startAt, substrLen). + + assign decodedString = decodedString + chr(utf8, 'utf-8':u, 'utf-8':u) startAt = pos + escLen + hexLen + escCloseLen pos = index(pString, pEscapeSeq, startAt) . @@ -171,7 +174,8 @@ class OpenEdge.Core.Util.UTF8Encoder: // use a distinct DO block so that we only treat bad values as 0 do on error undo, throw: assign unicode = MathUtil:HexToInt(pHexValue). - catch e as Progress.Lang.Error : + {&_proparse_ prolint-nowarn(varusage)} + catch uncaught as Progress.Lang.Error: return 0. end catch. end. @@ -188,8 +192,6 @@ class OpenEdge.Core.Util.UTF8Encoder: @param int64 The Unicode codepoint @return int64 The UTF-8 value. Will be ZERO/0 if the codepoint is unknown or negative. */ method static public int64 UnicodeToUtf8(input pUnicode as int64): - define variable loop as integer no-undo. - define variable numBytes as integer no-undo. define variable utf8 as int64 no-undo. if pUnicode lt 0 then @@ -278,7 +280,8 @@ class OpenEdge.Core.Util.UTF8Encoder: // use a distinct DO block so that we can treat bad values as 0 do on error undo, throw: assign utf8 = asc(substring(pUtf8, 1, 1), 'utf-8':u). - catch e as Progress.Lang.Error: + {&_proparse_ prolint-nowarn(varusage)} + catch uncaught as Progress.Lang.Error: return 0. end catch. end. @@ -311,6 +314,7 @@ class OpenEdge.Core.Util.UTF8Encoder: assign idx = 1. // only loop through the last 4 bytes; the first 4 others may be 0 or 255 (depending on whether the // utf8 value is negative or not). + LOOP-BLK: do loop = 8 to 5 by -1: assign byte = get-byte(memData, loop). if byte eq 0 @@ -318,7 +322,7 @@ class OpenEdge.Core.Util.UTF8Encoder: or byte eq 0xFE or byte eq 0xFF then - leave. + leave LOOP-BLK. // These bit masks can be seen in UnicodeToUtf8(int64) // 1 byte diff --git a/src/Spark/Core/Util/OSTools.cls b/src/Spark/Core/Util/OSTools.cls index ee219773..6581b09f 100644 --- a/src/Spark/Core/Util/OSTools.cls +++ b/src/Spark/Core/Util/OSTools.cls @@ -4,11 +4,15 @@ using Spark.Core.Util.OSTools from propath. block-level on error undo, throw. +{Spark/version.i} /* Allow framework version to be set as a preprocessor. */ + /** * A static class with OS helper methods */ class Spark.Core.Util.OSTools: + define public static variable CurrentVersion as character no-undo initial "{&SPARK_VERSION}". + define private static temp-table ttDirStruct no-undo field ParentDir as character field FileName as character diff --git a/src/Spark/startup.p b/src/Spark/startup.p index cedecbb6..16bb482c 100644 --- a/src/Spark/startup.p +++ b/src/Spark/startup.p @@ -17,10 +17,6 @@ block-level on error undo, throw. /* Standard input parameter as set via sessionStartupProcParam */ define input parameter startup-data as character no-undo. -/* Denote the current version of the Progress Modernization Framework. */ -{Spark/version.i} /* Allow framework version to be updated by build process. */ -define variable CurrentVersion as character no-undo initial "{&SPARK_VERSION}". - /* Set up a custom log file if not in an MSAS environment (eg. ABLUnit). */ if session:client-type eq "4GLCLIENT" then do: log-manager:logfile-name = session:temp-directory + "server.log". @@ -29,7 +25,7 @@ end. /* session:client-type */ {Spark/Core/Lib/LogMessage.i &IsClass=false &IsPublic=false} /* *************************** Main Block *************************** */ -logMessage(substitute("Starting Spark, version &1", CurrentVersion), "SPARK-STRT", 0). +logMessage(substitute("Starting Spark, version &1", Spark.Core.Util.OSTools:CurrentVersion), "SPARK-STRT", 0). logMessage(substitute("Session Startup Param [&1], num-dbs: &2", startup-data, num-dbs), "SPARK-STRT", 3). logMessage(substitute("Internal Codepage: &1", session:cpinternal), "SPARK-STRT", 2). logMessage(substitute("Stream Codepage: &1", session:cpstream), "SPARK-STRT", 2). diff --git a/src/Spark/version.i b/src/Spark/version.i index 12bf1f12..fb78a9bb 100644 --- a/src/Spark/version.i +++ b/src/Spark/version.i @@ -1 +1 @@ -&GLOBAL-DEFINE SPARK_VERSION 6.0.1-2021.08.12.015611 (12.2.6) \ No newline at end of file +&GLOBAL-DEFINE SPARK_VERSION 6.0.2-2022.02.22.073132 (12.5.0) \ No newline at end of file diff --git a/src/build.xml b/src/build.xml index 7bd34f6c..43e8c504 100644 --- a/src/build.xml +++ b/src/build.xml @@ -126,7 +126,7 @@ - + @@ -140,6 +140,22 @@ + + + + + + + + + + + + + + + + @@ -152,19 +168,28 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -184,21 +209,13 @@ + + - - - - - - - - - - - +