• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

    На последнюю неделю приходится экзамен, где нужно будет показать свои навыки, взломав ряд уязвимых учебных сайтов, и добыть флаги. Успешно сдавшие экзамен получат сертификат.

    Запись на курс до 25 апреля. Получить промодоступ ...

JsonParser (LS)

VladSh

начинающий
Lotus Team
11.12.2009
1 786
157
BIT
79
Visual Basic:
%REM
    Исходный код классов: https://github.com/dpastov/jsonparser-ls
    Доработано: VladSh (2020.02.14)
    Версия: 2.0
%END REM
Option Public
Option Declare

'Globalization constants
Public Const ERRc_INVALID_JSON    = 1000
Private Const ERR_INVALID_JSON     = "Invalid JSON format"

Private Const JSON_FIELD_MASK_DEFAULT = "[a-zA-Z_]"
Public Const JSON_FIELD_MASK_DOMINO = "[@$#%a-zA-Z0-9_]"

Public Const DDS_JSON_DT_PATTERN = "####-##-##T##:##:##Z"        'Domino Data Service JSON datetime format
Const DDS_JSON_UNID_TAG   = "@unid"
Const DDS_JSON_FORM_TAG   = "@form"

Class JSONArray
    Private m_items() As Variant
    Private m_size As Long
    Sub New()
    End Sub

    Public Property Get Items As Variant
        Items = Me.m_items
    End Property

    Public Property Get Size As Long
        Size = Me.m_size
    End Property

    Public Sub AddItem(itemVal As Variant)
        ReDim Preserve m_items(0 To m_size)
        If IsObject(itemVal) Then
            Set m_items(m_size) = itemVal
        Else
            m_items(m_size) = itemVal
        End If
        m_size = m_size + 1
    End Sub
End Class

Class JSONObject
    Private m_items List As Variant
    Private m_size As Long
    Sub New()
    End Sub

    Public Property Get Items As Variant
        Items = Me.m_items
    End Property

    Public Property Get Size As Long
        Size = Me.m_size
    End Property

    Public Sub AddItem(itemName As String, itemVal As Variant)
       If Not Me.HasItem(itemName) Then m_size = m_size + 1
        If IsObject(itemVal) Then
            Set Me.m_items(itemName) = itemVal
        Else
            Me.m_items(itemName) = itemVal
        End If
    End Sub

    Public Function HasItem(itemName As String) As Boolean
        HasItem = IsElement(m_items(itemName))
    End Function

    %REM
        Function getItemFromObject
        Description: возвращает значение (типы см. GetItemByPath) переданного списка, либо Null
    %END REM
    Private Function getItemFromObject(obj As Variant, itemName As String) As Variant
        If IsElement(obj(itemName)) Then
            If IsObject(obj(itemName)) Then
                getItemFromObject = obj(itemName).Items()
            Else
                getItemFromObject = obj(itemName)
            End If
        Else
            getItemFromObject = Null
        End If
    End Function

    %REM
        Возвращает возвращает содержимое элемента по переданному пути;
        Типы возвращаемых значений: String-значение | VARIANT( ) | VARIANT LIST, либо Null - в случае отсутствия элемента.
        Может быть небезопасно:
                v = jsonObj.GetItemByPath("object")("nonexistent") - вернёт ошибку!
            лучше так:
                v = jsonObj.GetItemByPath("object.nonexistent") - вернёт Null
            либо так:
                v = jsonObj.GetItemByPath("object") - и проверить на наличие: IsElement(v("nonexistent"))
    %END REM
    Public Function GetItemByPath(path As String) As Variant
        On Error GoTo ErrH
        Dim arr As Variant, i As Integer, entry As String
        arr = Split(path, ".")
        Dim data As Variant
        data = Me.m_items
        For i = 0 To UBound(arr)
            entry = arr(i)
            If Not IsElement(data(entry)) Then Exit For
            data = getItemFromObject(data, entry)
            If i = UBound(arr) Then
                GetItemByPath = data
                Exit Function
            End If
        Next
        GetItemByPath = Null
Quit:
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function

    %REM
        Function IsObjectItem
        Description: добавлено для полноценного использования оригинальной GetItem, -
            чтобы можно было понять, как присваивать результат переменной, - с помощью Set или прямым присваиванием
    %END REM
    Public Function IsObjectItem(itemName As String) As Boolean
        If HasItem(itemName) Then
            IsObjectItem = IsObject(me.Items(itemName))
        End If
    End Function

    %REM
        Оригинальный метод; оставлен для старой возможности использования
    %END REM
    Public Function GetItem(itemName As String) As Variant
        If HasItem(itemName) Then
            If IsObject(me.Items(itemName)) Then
                Set GetItem = me.Items(itemName)
            Else
                GetItem = me.Items(itemName)
            End If
        Else
            GetItem = Null
        End If
    End Function

End Class

Class JSONParser
    Private m_length As Long
    Private m_decimalSep As String
    Private m_fieldMask As String

    Sub New()
        m_fieldMask = JSON_FIELD_MASK_DEFAULT
    End Sub

    %REM
        Property Set fieldNameMask
        Description: установка маски допустимых символов в именах полей
    %END REM
    Public Property Set fieldNameMask As String
        m_fieldMask = fieldNameMask
    End Property

    Private Property Get getDecimalSep As String
        Dim session As NotesSession
        Dim international As NotesInternational
  
        If m_decimalSep = "" Then
            Set session = New NotesSession()
            Set international = session.International
            m_decimalSep = international.DecimalSep
        End If
  
        getDecimalSep = m_decimalSep
    End Property

    Private Property Get length As Long
        length = m_length
    End Property

    Function parse(jsonString As String) As Variant
        On Error GoTo ErrH
        Dim res As Variant
        Dim index1 As Long
        Dim index2 As Long
  
        m_length = Len(jsonString)
  
        index1 = InStr(jsonString, "{")
        index2 = InStr(jsonString, "[")

        If index1 > 0 And (index1 < index2 Or index2 = 0) Then
            Set res = parseObject(jsonString, index1 + 1)
        ElseIf index2 > 0 And (index2 < index1 Or index1 = 0) Then
            Set res = parseArray(jsonString, index2 + 1)
        End If
  
        Set parse = res
        Exit Function
  
ErrH:
        Error ERRc_INVALID_JSON, GetThreadInfo(1) & " (" & Erl & ") -> " + ERR_INVALID_JSON + ": " + Error$ + " (" & Err & ")"
    End Function

    Private Function parseObject(jsonString As String, index As Long) As JSONObject
        Dim res As JSONObject
        Dim propertyValue As Variant
        Dim propertyName As String
        Dim objectEnd As Long
        Dim nextPair As Long

        Set res = New JSONObject()

        nextPair = InStr(index, jsonString, ":")
        objectEnd = InStr(index, jsonString, "}")
        While nextPair < objectEnd And nextPair > 0 And objectEnd > 0
            propertyName = findPropertyName(jsonString, index)
            index = InStr(index, jsonString, ":")
            index = index + 1
      
            Call renderValue(jsonString, index, propertyValue)
            Call res.AddItem(propertyName, propertyValue)
      
            nextPair = InStr(index, jsonString, ":")
            objectEnd = InStr(index, jsonString, "}")
        Wend
  
        index = objectEnd + 1
  
        Set parseObject = res
    End Function

    Private Function parseArray(jsonString As String, index As Long) As JSONArray
        Dim res As JSONArray
        Dim propertyValue As Variant
        Dim arrEnd As Long
        Dim nextVal As Long

        Set res = New JSONArray()
        nextVal = InStr(index, jsonString, ",")
        arrEnd = InStr(index, jsonString, "]")
  
        Do
            Call renderValue(jsonString, index, propertyValue)
            If Not IsEmpty(propertyValue) Then
                Call res.AddItem(propertyValue)
            End If
      
            nextVal = InStr(index, jsonString, ",")
            arrEnd = InStr(index, jsonString, "]")
        Loop While nextVal < arrEnd And nextVal > 0 And arrEnd > 0
  
        index = arrEnd + 1
  
        Set parseArray = res
    End Function

    Private Function renderValue(jsonString As String, index As Long, propertyValue As Variant) As Variant
        Dim char As String
        Dim i As Long
  
        For i = index To length
            char = Mid(jsonString, i, 1)
      
            If char = {"} Then
                index = i
                propertyValue = findElementString(jsonString, index)
                i = length
            ElseIf char Like {#} Or char = {-} Then
                index = i
                propertyValue = findElementNumber(jsonString, index)
                i = length
            ElseIf char Like {[tfn]} Then
                index = i
                propertyValue = findElementLiteral(jsonString, index)
                i = length
            ElseIf char = "{" Then
                index = i
                Set propertyValue = parseObject(jsonString, index)
                i = length
            ElseIf char = "[" Then
                index = i + 1
                Set propertyValue = parseArray(jsonString, index)
                i = length
            End If
        Next
    End Function

    Private Function findElementNumber(jsonString As String, index As Long) As Variant
        Dim res As Variant
        Dim elementEnd As String
        Dim char As String
        Dim i As Long
  
        elementEnd = |, ]}|    'to catch: close bracket, comma, space or }
        For i = index To length
            char = Mid(jsonString, i, 1)
      
            If InStr(elementEnd, char) Then
                res = Mid(jsonString, index, i - index)
                index = i
                i = length
            End If
        Next
  
        If InStr(res, ".") And getDecimalSep()<>"." Then
            res = Replace(res, ".", getDecimalSep())
        End If
  
        findElementNumber = CDbl(res)
    End Function

    Private Function findElementLiteral(jsonString As String, index As Long) As Variant
        Dim res As String
        Dim elementEnd As String
        Dim char As String
        Dim i As Long
  
        elementEnd = |, ]}|    'to catch: close bracket, comma, space or }
        For i = index To length
            char = Mid(jsonString, i, 1)
      
            If InStr(elementEnd, char) Then
                res = Mid(jsonString, index, i - index)
                index = i
                i = length
            End If
        Next
  
        Select Case res:
            Case "null":
                findElementLiteral = Null
            Case "true":
                findElementLiteral = True
            Case "false":
                findElementLiteral = False
        End Select
    End Function

    'find element in json string
    Private Function findElementString(jsonString As String, index As Long) As String
        Dim value As String
        Dim index1 As Long, index1tmp As Long, index2 As Long
        Dim bRecover As Boolean
  
        index1 = InStr(index, jsonString, {"})
        If index1 = 0 Then Exit Function
  
        index1tmp = index1
        Do
            index2 = InStr(index1tmp + 1, jsonString, {"})
            'проверяем наличие экранирующего символа перед найденной закрывающей кавычкой
            If Mid$(jsonString, index2 - 1, 1) <> "\" Then Exit Do Else bRecover = True
            index1tmp = index2
        Loop
  
        value = Mid(jsonString, index1 + 1, index2 - index1 - 1)
        If bRecover Then
            Call recoveryString(value)
        Else
            If InStr(value, "\") <> 0 Then Call recoveryString(value)
        End If
  
        index = index2 + 1
  
        findElementString = value
    End Function

    %REM
        Sub recoveryStringValue
        Description: восстановление значения параметра из JSON (убирает все спецсимволы)
    %END REM
    Private Sub recoveryString(sValue As String)
        sValue = Replace(sValue, {\b}, Chr(8))
        sValue = Replace(sValue, "\t", Chr(9))
        sValue = Replace(sValue, "\n", Chr(10))
        sValue = Replace(sValue, "\r", Chr(13))
        sValue = Replace(sValue, {\f}, Chr(12))
        sValue = Replace(sValue, {\u0001}, Chr(1))
        sValue = Replace(sValue, {\u0002}, Chr(2))
        sValue = Replace(sValue, {\u001a}, Chr(26))
        sValue = Replace(sValue, {\"}, {"})
        sValue = Replace(sValue, {\/}, {/})
        sValue = Replace(sValue, {\\}, {\})
    End Sub

    'find property name
    Private Function findPropertyName(jsonString As String, index As Long) As String
        Dim res As String
        Dim propertyNameEnd As String
        Dim char As String
        Dim i As Long
  
        'property start with character
        For i = index To length
            char = Mid(jsonString, i, 1)
            If char Like m_fieldMask Then
                res = char
                index = i + 1
                i = length
            End If
        Next
        'rest of property could be characters and numbers etcx
        propertyNameEnd = | :"'|
        For i = index To length
            char = Mid(jsonString, i, 1)
            If InStr(propertyNameEnd, char) Then
                index = i
                i = length
            Else
                res = res + char
            End If
        Next
  
        findPropertyName = res
    End Function
End Class

%REM
    Function JSON_parseToDocument
    Description:
%END REM
Public Function JSON_parseToDocument(sJSON As String, ndbTrg As NotesDatabase) As NotesDocument
    On Error GoTo ErrH
    Dim oJson As JSONObject
    Set oJson = JSON_parse(sJSON, True)

    Dim ndNew As NotesDocument
    Set ndNew = ndbTrg.CreateDocument()

    If JSON_setToDocument(oJson, ndNew) Then
        Set JSON_parseToDocument = ndNew
    End If
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Обёртка над oJSON.GetItem() для удобства возврата "" в случае отсутствия элемента.
    Параметры:
        oJSON - JSONObject, полученный в результате разбора JSON
        sElementPath - путь к элементу в стуктуре json, разделённый "."
    Перечень возможных возвращаемых значений:
        - "" - в случае отсутствия элемента;
        - скаляр (строка/число);
        - VARIANT( );
        - VARIANT LIST.
%END REM
Public Function JSON_getItemValue(oJSON As JSONObject, sElementPath As String) As Variant
    On Error GoTo ErrH
    JSON_getItemValue = oJSON.GetItemByPath(sElementPath)
    If IsNull(JSON_getItemValue) Then JSON_getItemValue = ""
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Правильная обработка системмных свойств Domino-документа, возвращаемых в значениях полей
%END REM
Public Function JSON_setToDocument(oJson As JSONObject, ndTo As NotesDocument) As Boolean
    On Error GoTo ErrH

    If oJson.Size <> 0 Then
        Dim sItemName As String
        ForAll vItemValue In oJson.Items()
            sItemName = ListTag(vItemValue)
            If Left(sItemName, 1) = "@" Then
                'обработка системмных свойств NotesDocument'а, полученных в json'е
                Select Case sItemName
                Case DDS_JSON_UNID_TAG:
                    ndTo.UniversalID = vItemValue
                Case DDS_JSON_FORM_TAG:
                    Call ndTo.ReplaceItemValue("Form", vItemValue)
                Case Else:
                    ndTo.ReplaceItemValue(sItemName, vItemValue).SaveToDisk = False
                End Select
            Else
                If IsObject(vItemValue) Then        'JSONArray
                    Call ndTo.ReplaceItemValue(sItemName, vItemValue.Items())
                Else
                    Call ndTo.ReplaceItemValue(sItemName, vItemValue)
                End If
            End If
        End ForAll
        JSON_setToDocument = True
    End If

    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Function JSON_parse
    Description: Разбирает JSON и возвращает JSONObject либо JSONArray
    bDominoJson = True - установит маску допустимых символов в именах полей для Lotus Domino, иначе будет использована маска по умолчанию
%END REM
Public Function JSON_parse(sJSON As String, bDominoJson As Boolean) As Variant
    On Error GoTo ErrH
    Dim oJParser As New JSONParser
    If bDominoJson Then oJParser.fieldNameMask = JSON_FIELD_MASK_DOMINO
    Set JSON_parse = oJParser.Parse(sJSON)
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

Отличия от оригинальной версии:
1. JSONObject: добавлен метод GetItemByPath, позволяющий получать значение элемента по пути в виде Lotus-типов.
2. JSONObject: добавлены сервисные методы (IsObjectItem и др.) для правильного присваивания возвращаемого элемента при использовании оригинального метода GetItem, который был оставлен для обратной совместимости.
3. JSONObject: добавлено свойство Size для возможности анализа объекта на пустоту.
4. JSONParser: класс при разборе JSON теперь выдаёт ошибку "Invalid JSON format" {1000}, а не каждый раз неизвестно что.
5. JSONParser: добавлена поддержка кавычек и экранирующих символов внутри текстовых значений, - будет произведено обратное преобразование.
6. JSONParser: добавлено свойство fieldNameMask для установки маски допустимых символов, т.к. спецсимволы, разрешённые в именах полей документов Lotus, вырезались, из-за чего имена таких полей возвращались некорректно.
7. Добавлено несколько приятных функций.

Добавлено: обсуждение ведётся в теме Разбор JSON.
 

VladSh

начинающий
Lotus Team
11.12.2009
1 786
157
BIT
79
Visual Basic:
%REM
    Исходный код (1-я версия) классов: https://github.com/dpastov/jsonparser-ls
    Доработано: VladSh (2020.02.14 - 2020.04.17)
    Версия: 3.0
%END REM
Option Public
Option Declare

Const ERRc_JSON_INVALID_FORMAT  = 1000
Const ERRt_JSON_INVALID_FORMAT  = "Invalid JSON format"
Const ERRc_JSON_INVALID_OBJTYPE = 1001
Const ERRt_JSON_INVALID_OBJTYPE = "Error type of object"

Private Const JSON_FIELD_MASK_DEFAULT = "[a-zA-Z_]"
Public Const JSON_FIELD_MASK_DOMINO = "[@$#%a-zA-Z0-9_]"

Public Const DDS_JSON_DT_FORMAT  = "####-##-##T##:##:##Z"        'Domino Data Service JSON datetime format
Public Const DDS_JSON_UNID_TAG   = "@unid"
Public Const DDS_JSON_FORM_TAG   = "@form"

%REM
    Class JSONObject
    Description: базовый класс
%END REM
Class JSONObject
    Private m_size As Long
    Sub New()
    End Sub
   
    Public Property Get Items As Variant
    End Property
   
    Public Property Get Size As Long
        Size = Me.m_size
    End Property
   
    Public Function toJsonString() As String
    End Function
End Class

Class JSONArray As JSONObject
    Private m_items() As Variant
   
    Public Property Get Items As Variant
        Items = Me.m_items
    End Property
   
    Public Sub AddItem(itemVal As Variant)
        ReDim Preserve m_items(0 To m_size)
        If IsObject(itemVal) Then
            Set m_items(m_size) = itemVal
        Else
            m_items(m_size) = itemVal
        End If
        m_size = m_size + 1
    End Sub
   
    Public Function toJsonString() As String
        On Error GoTo ErrH
        Dim k As Integer
        Dim ret() As String
        Dim tname As String
        If me.m_size = 0 Then
            toJsonString = "[]"
        Else
            ReDim ret(me.m_size - 1) As String
            For k = 0 To me.m_size - 1
                tname = TypeName(m_items(k))
                Select Case tname
                Case "STRING":
                    ret(k) = {"} + JSON_EscapeString(CStr(m_items(k))) + {"}
                Case "DOUBLE", "INTEGER":
                    'дробные числа конвертируем с разделителем "."
                    ret(k) = Replace(CStr(m_items(k)), ",", ".")
                Case "BOOLEAN":
                    If m_items(k) = True Then ret(k) = "true" Else m_items(k) = "false"
                Case "NOTESDATETIME", "DATE":
                    ret(k) = Format(m_items(k), DDS_JSON_DT_FORMAT)
                Case "JSONARRAY", "JSONLIST":
                    ret(k) = m_items(k).toJsonString()
                Case Else:
                    Error ERRc_JSON_INVALID_OBJTYPE, ERRt_JSON_INVALID_OBJTYPE + ": " + tname
                End Select
            Next
            toJsonString = "[" + Join(ret, ",") + "]"
        End If
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
End Class

Class JSONList As JSONObject
    Private m_items List As Variant
   
    Public Property Get Items As Variant
        Items = Me.m_items
    End Property
   
    Public Sub AddItem(itemName As String, itemVal As Variant)
        If Not Me.HasItem(itemName) Then m_size = m_size + 1
        If IsObject(itemVal) Then
            Set Me.m_items(itemName) = itemVal
        Else
            Me.m_items(itemName) = itemVal
        End If
    End Sub
   
    Public Function HasItem(itemName As String) As Boolean
        HasItem = IsElement(m_items(itemName))
    End Function
   
    %REM
        Function getItemFromObject
        Description: возвращает значение (типы см. GetItemByPath) переданного списка, либо Null
    %END REM
    Private Function getItemFromObject(obj As Variant, itemName As String) As Variant
        If IsElement(obj(itemName)) Then
            If IsObject(obj(itemName)) Then
                getItemFromObject = obj(itemName).Items()
            Else
                getItemFromObject = obj(itemName)
            End If
        Else
            getItemFromObject = Null
        End If
    End Function
   
    %REM
        Возвращает возвращает содержимое элемента по переданному пути;
        Типы возвращаемых значений: String-значение | VARIANT( ) | VARIANT LIST, либо Null - в случае отсутствия элемента.
        Может быть небезопасно:
                v = jsonObj.GetItemByPath("object")("nonexistent") - вернёт ошибку!
            лучше так:
                v = jsonObj.GetItemByPath("object.nonexistent") - вернёт Null
            либо так:
                v = jsonObj.GetItemByPath("object") - и проверить на наличие: IsElement(v("nonexistent"))
    %END REM
    Public Function GetItemByPath(path As String) As Variant
        On Error GoTo ErrH
        Dim arr As Variant, i As Integer, entry As String
        arr = Split(path, ".")
        Dim data As Variant
        data = Me.m_items
        For i = 0 To UBound(arr)
            entry = arr(i)
            If Not IsElement(data(entry)) Then Exit For
            data = getItemFromObject(data, entry)
            If i = UBound(arr) Then
                GetItemByPath = data
                Exit Function
            End If
        Next
        GetItemByPath = Null
Quit:
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
   
    %REM
        Function IsObjectItem
        Description: добавлено для полноценного использования оригинальной GetItem,
            чтобы можно было понять, как присваивать результат переменной, - с помощью Set или прямым присваиванием
    %END REM
    Public Function IsObjectItem(itemName As String) As Boolean
        If HasItem(itemName) Then
            IsObjectItem = IsObject(me.Items(itemName))
        End If
    End Function
   
    %REM
        Оригинальный метод; оставлен для старой возможности использования
    %END REM
    Public Function GetItem(itemName As String) As Variant
        If HasItem(itemName) Then
            If IsObject(me.Items(itemName)) Then
                Set GetItem = me.Items(itemName)
            Else
                GetItem = me.Items(itemName)
            End If
        Else
            GetItem = Null
        End If
    End Function
   
    Public Function toJsonString() As String
        On Error GoTo ErrH
        Dim k As Integer
        Dim ret() As String
        Dim tname As String
        If me.m_size = 0 Then
            toJsonString = "{}"
        Else
            ReDim ret(me.m_size - 1) As String
            ForAll m In m_items
                tname = TypeName(m)
                Select Case tname
                Case "STRING":
                    ret(k) = {"} + ListTag(m) + {":"} + JSON_EscapeString(CStr(m)) + {"}
                Case "DOUBLE", "INTEGER":
                    'дробные числа конвертируем с разделителем "."
                    ret(k) = {"} + ListTag(m) + {":} + Replace(CStr(m), ",", ".")
                Case "BOOLEAN":
                    If m = True Then ret(k) = {"} + ListTag(m) + {":true} Else ret(k) = {"} + ListTag(m) + {":false}
                Case "NOTESDATETIME", "DATE":
                    ret(k) = {"} + ListTag(m) + {":} + Format(m_items(k), DDS_JSON_DT_FORMAT)
                Case "JSONARRAY", "JSONLIST":
                    ret(k) = {"} + ListTag(m) + {":} + m.toJsonString()
                Case Else:
                    Error ERRc_JSON_INVALID_OBJTYPE, ERRt_JSON_INVALID_OBJTYPE + ": " + tname
                End Select
                k = k + 1
            End ForAll
            toJsonString = "{" + Join(ret, ",") + "}"
        End If
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
End Class

Class JSONParser
    Private m_length As Long
    Private m_decimalSep As String
    Private m_fieldMask As String
   
    Sub New()
        m_fieldMask = JSON_FIELD_MASK_DEFAULT
    End Sub
   
    %REM
        Property Set fieldNameMask
        Description: установка маски допустимых символов в именах полей
    %END REM
    Public Property Set fieldNameMask As String
        m_fieldMask = fieldNameMask
    End Property
   
    Private Property Get getDecimalSep As String
        Dim session As NotesSession
        Dim international As NotesInternational
       
        If Len(m_decimalSep) = 0 Then
            Set session = New NotesSession()
            Set international = session.International
            m_decimalSep = international.DecimalSep
        End If
       
        getDecimalSep = m_decimalSep
    End Property
   
    Private Property Get length As Long
        length = m_length
    End Property
   
    Function parse(jsonString As String) As Variant
        On Error GoTo ErrH
        Dim res As Variant
        Dim index1 As Long
        Dim index2 As Long
       
        m_length = Len(jsonString)
       
        index1 = InStr(jsonString, "{")
        index2 = InStr(jsonString, "[")
   
        If index1 > 0 And (index1 < index2 Or index2 = 0) Then
            Set res = parseObject(jsonString, index1 + 1)
        ElseIf index2 > 0 And (index2 < index1 Or index1 = 0) Then
            Set res = parseArray(jsonString, index2 + 1)
        End If
       
        Set parse = res
        Exit Function
       
ErrH:
        Error ERRc_JSON_INVALID_FORMAT, GetThreadInfo(1) & " (" & Erl & ") -> " + ERRt_JSON_INVALID_FORMAT + ": " + Error$ + " (" & Err & ")"
    End Function
   
    Private Function parseObject(jsonString As String, index As Long) As JSONList
        Dim res As JSONList
        Dim propertyValue As Variant
        Dim propertyName As String
        Dim objectEnd As Long
        Dim nextPair As Long

        Set res = New JSONList()

        nextPair = InStr(index, jsonString, ":")
        objectEnd = InStr(index, jsonString, "}")
        While nextPair < objectEnd And nextPair > 0 And objectEnd > 0
            propertyName = findPropertyName(jsonString, index)
            index = InStr(index, jsonString, ":")
            index = index + 1
           
            Call renderValue(jsonString, index, propertyValue)
            Call res.AddItem(propertyName, propertyValue)
           
            nextPair = InStr(index, jsonString, ":")
            objectEnd = InStr(index, jsonString, "}")
        Wend
       
        index = objectEnd + 1
       
        Set parseObject = res
    End Function
   
    Private Function parseArray(jsonString As String, index As Long) As JSONArray
        Dim res As JSONArray
        Dim propertyValue As Variant
        Dim arrEnd As Long
        Dim nextVal As Long

        Set res = New JSONArray()
        nextVal = InStr(index, jsonString, ",")
        arrEnd = InStr(index, jsonString, "]")
       
        Do
            Call renderValue(jsonString, index, propertyValue)
            If Not IsEmpty(propertyValue) Then
                Call res.AddItem(propertyValue)
            End If
           
            nextVal = InStr(index, jsonString, ",")
            arrEnd = InStr(index, jsonString, "]")
        Loop While nextVal < arrEnd And nextVal > 0 And arrEnd > 0
       
        index = arrEnd + 1
       
        Set parseArray = res
    End Function
   
    Private Function renderValue(jsonString As String, index As Long, propertyValue As Variant) As Variant
        Dim char As String
        Dim i As Long
       
        For i = index To length
            char = Mid(jsonString, i, 1)
           
            If char = {"} Then
                index = i
                propertyValue = findElementString(jsonString, index)
                i = length
            ElseIf char Like {#} Or char = {-} Then
                index = i
                propertyValue = findElementNumber(jsonString, index)
                i = length
            ElseIf char Like {[tfn]} Then
                index = i
                propertyValue = findElementLiteral(jsonString, index)
                i = length
            ElseIf char = "{" Then
                index = i
                Set propertyValue = parseObject(jsonString, index)
                i = length
            ElseIf char = "[" Then
                index = i + 1
                Set propertyValue = parseArray(jsonString, index)
                i = length
            End If
        Next
    End Function
   
    Private Function findElementNumber(jsonString As String, index As Long) As Variant
        Dim res As Variant
        Dim elementEnd As String
        Dim char As String
        Dim i As Long
       
        elementEnd = |, ]}|    'to catch: close bracket, comma, space or }
        For i = index To length
            char = Mid(jsonString, i, 1)
           
            If InStr(elementEnd, char) Then
                res = Mid(jsonString, index, i - index)
                index = i
                i = length
            End If
        Next
       
        If InStr(res, ".") And getDecimalSep() <> "." Then
            res = Replace(res, ".", getDecimalSep())
        End If
       
        findElementNumber = CDbl(res)
    End Function
   
    Private Function findElementLiteral(jsonString As String, index As Long) As Variant
        Dim res As String
        Dim elementEnd As String
        Dim char As String
        Dim i As Long
       
        elementEnd = |, ]}|    'to catch: close bracket, comma, space or }
        For i = index To length
            char = Mid(jsonString, i, 1)
           
            If InStr(elementEnd, char) Then
                res = Mid(jsonString, index, i - index)
                index = i
                i = length
            End If
        Next
       
        Select Case res:
            Case "null":
                findElementLiteral = Null
            Case "true":
                findElementLiteral = True
            Case "false":
                findElementLiteral = False
        End Select
    End Function
   
    'find element in json string
    Private Function findElementString(jsonString As String, index As Long) As String
        Dim value As String
        Dim index1 As Long, index1tmp As Long, index2 As Long
        Dim bRecover As Boolean
       
        index1 = InStr(index, jsonString, {"})
        If index1 = 0 Then Exit Function
       
        index1tmp = index1
        Do
            index2 = InStr(index1tmp + 1, jsonString, {"})
            'проверяем наличие экранирующего символа перед найденной закрывающей кавычкой
            If Mid$(jsonString, index2 - 1, 1) <> "\" Then Exit Do Else bRecover = True
            index1tmp = index2
        Loop
       
        value = Mid(jsonString, index1 + 1, index2 - index1 - 1)
        If bRecover Then
            Call JSON_UnescapeString(value)
        Else
            If InStr(value, "\") <> 0 Then Call JSON_UnescapeString(value)
        End If
       
        index = index2 + 1
       
        findElementString = value
    End Function
   
    'find property name
    Private Function findPropertyName(jsonString As String, index As Long) As String
        Dim res As String
        Dim propertyNameEnd As String
        Dim char As String
        Dim i As Long
       
        'property start with character
        For i = index To length
            char = Mid(jsonString, i, 1)
            If char Like m_fieldMask Then
                res = char
                index = i + 1
                i = length
            End If
        Next
       
        'rest of property could be characters and numbers etcx
        propertyNameEnd = | :"'|
        For i = index To length
            char = Mid(jsonString, i, 1)
            If InStr(propertyNameEnd, char) Then
                index = i
                i = length
            Else
                res = res + char
            End If
        Next
       
        findPropertyName = res
    End Function
End Class

%REM
    Function JSON_EscapeString
    Description: sJSON - строка с JSON, которая будет обработана
    Использовать только для значений полей в json!!!
%END REM
Private Function JSON_EscapeString(sJSON As String)
    sJSON = Replace(sJSON, {\}, {\\})
    sJSON = Replace(sJSON, Chr(8), {\b})
    sJSON = Replace(sJSON, Chr(9), {\t})
    sJSON = Replace(sJSON, Chr(13), {\r})
    sJSON = Replace(sJSON, Chr(10), {\n})
    sJSON = Replace(sJSON, Chr(12), {\f})
    sJSON = Replace(sJSON, Chr(19), {})
    sJSON = Replace(sJSON, Chr(1), {\u0001})
    sJSON = Replace(sJSON, Chr(2), {\u0002})
    sJSON = Replace(sJSON, Chr(26), {\u001a})
    sJSON = Replace(sJSON, {"}, {\"})
    sJSON = Replace(sJSON, {/}, {\/})
    JSON_EscapeString = sJSON
End Function

%REM
    Function JSON_UnescapeString
    Description: восстановление значения параметра из JSON (заменяет спецсимволы на соотв. им реальные символы)
%END REM
Private Function JSON_UnescapeString(sValue As String)
    sValue = Replace(sValue, {\b}, Chr(8))
    sValue = Replace(sValue, "\t", Chr(9))
    sValue = Replace(sValue, "\n", Chr(10))
    sValue = Replace(sValue, "\r", Chr(13))
    sValue = Replace(sValue, {\f}, Chr(12))
    sValue = Replace(sValue, {\u0001}, Chr(1))
    sValue = Replace(sValue, {\u0002}, Chr(2))
    sValue = Replace(sValue, {\u001a}, Chr(26))
    sValue = Replace(sValue, {\"}, {"})
    sValue = Replace(sValue, {\/}, {/})
    sValue = Replace(sValue, {\\}, {\})
    JSON_UnescapeString = sValue
End Function

%REM
    Обёртка над oJSON.GetItem() для удобства возврата "" в случае отсутствия элемента.
    Параметры:
        oJSON - JSONList, полученный в результате разбора JSON
        sElementPath - путь к элементу в стуктуре json, разделённый "."
    Перечень возможных возвращаемых значений:
        - "" - в случае отсутствия элемента;
        - скаляр (строка/число);
        - VARIANT( );
        - VARIANT LIST.
%END REM
Public Function JSON_getItemValue(oJSON As JSONList, sElementPath As String) As Variant
    On Error GoTo ErrH
    JSON_getItemValue = oJSON.GetItemByPath(sElementPath)
    If IsNull(JSON_getItemValue) Then JSON_getItemValue = ""
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Правильная обработка системмных свойств Domino-документа, возвращаемых в значениях полей
%END REM
Public Function JSON_setToDocument(oJson As JSONList, ndTo As NotesDocument) As Boolean
    On Error GoTo ErrH
   
    If oJson.Size <> 0 Then
        Dim sItemName As String
        ForAll vItemValue In oJson.Items()
            sItemName = ListTag(vItemValue)
            If Left(sItemName, 1) = "@" Then
                'обработка системмных свойств NotesDocument'а, полученных в json'е
                Select Case sItemName
                Case DDS_JSON_UNID_TAG:
                    ndTo.UniversalID = vItemValue
                Case DDS_JSON_FORM_TAG:
                    Call ndTo.ReplaceItemValue("Form", vItemValue)
                Case Else:
                    ndTo.ReplaceItemValue(sItemName, vItemValue).SaveToDisk = False
                End Select
            Else
                If IsObject(vItemValue) Then        'JSONArray
                    Call ndTo.ReplaceItemValue(sItemName, vItemValue.Items())
                Else
                    Call ndTo.ReplaceItemValue(sItemName, vItemValue)
                End If
            End If
        End ForAll
        JSON_setToDocument = True
    End If
   
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Function JSON_parse
    Description: Разбирает JSON и возвращает JSONList либо JSONArray
    bDominoJson = True - установит маску допустимых символов в именах полей для Lotus Domino, иначе будет использована маска по умолчанию
%END REM
Public Function JSON_parse(sJSON As String, bDominoJson As Boolean) As Variant
    On Error GoTo ErrH
    Dim oJParser As New JSONParser
    If bDominoJson Then oJParser.fieldNameMask = JSON_FIELD_MASK_DOMINO
    Set JSON_parse = oJParser.Parse(sJSON)
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Function JSON_parseToDocument
    Description: создание документа и заполнение его значениями из переданного json
%END REM
Public Function JSON_parseToDocument(sJSON As String, ndbTrg As NotesDatabase) As NotesDocument
    On Error GoTo ErrH
    Dim oJson As JSONList
    Set oJson = JSON_parse(sJSON, True)
   
    Dim ndNew As NotesDocument
    Set ndNew = ndbTrg.CreateDocument()
   
    If JSON_setToDocument(oJson, ndNew) Then
        Set JSON_parseToDocument = ndNew
    End If
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

Изменения:
1. JSONParser.recoveryString вынесена во внешнюю функцию JSON_UnescapeString.
2. JSONObject переименован в JSONList.
3. Из классов JSONList и JSONArray для удобства (унификации) работы извне выделен базовый класс JSONObject.
4. Реализована возможность пользоваться JSON_EscapeString и JSON_UnescapeString как функциями (возможность пользования как процедурами осталась).
5. В классы JSONArray и JSONList добавлен метод toJsonString.
6. Унифицировано наименование констант.
 

VladSh

начинающий
Lotus Team
11.12.2009
1 786
157
BIT
79
Visual Basic:
%REM
    Исходный код (1-я версия) классов: https://github.com/dpastov/jsonparser-ls
    Доработано: VladSh (2020.02.14 - 2023.05.07)
    Версия: 4.0
%END REM
Option Public
Option Declare

'lsERR_NOTES_INVALID_DOC
Private Const ERR_NOTES_INVALID_DOC = 4187

Const ERRc_JSON_INVALID_FORMAT  = 1000
Const ERRt_JSON_INVALID_FORMAT  = "Invalid JSON format"
Const ERRc_JSON_INVALID_OBJTYPE = 1001
Const ERRt_JSON_INVALID_OBJTYPE = "Error type of object"

'Не выполнять постобработку unescape-последовательностей (отработает класс-заглушка UnEscapeEngineBase)
Const UNESCAPE_RULE_NOTHING = ""
'Выполнять простую постобработку unescape-последовательностей (отработает класс UnEscapeEngineSimple)
Const UNESCAPE_RULE_SIMPLE  = "1"
'Выполнять простую постобработку с Unicode- unescape-последовательностями (отработает класс UnEscapeEngineUnicode)
Const UNESCAPE_RULE_UNICODE = "2"
'Выполнять постобработку unescape-последовательностей для путей к файлам (отработает класс UnEscapeEnginePath)
Const UNESCAPE_RULE_PATH    = "3"

'Маска допустимых символов в именах полей - стандартная
Private Const JSON_FIELD_MASK_DEFAULT = "[a-zA-Z_]"
'Маска допустимых символов в именах полей - разрешённых в Domino
Public Const JSON_FIELD_MASK_DOMINO = "[@$#%a-zA-Z0-9_]"

Public Const DDS_JSON_DT_FORMAT  = "####-##-##T##:##:##Z"        'Domino Data Service JSON datetime format
Public Const DDS_JSON_UNID_TAG   = "@unid"
Public Const DDS_JSON_FORM_TAG   = "@form"

%REM
    Class JSONObject
    Description: базовый класс
%END REM
Class JSONObject
    Private m_type As Byte
    Private m_size As Long
 
    %REM
        Property Get Type
        Description: возвращает тип объекта:
            1 - JSONObject
            2 - JSONArray
    %END REM
    Public Property Get Type As Byte
        Me.Type = m_type
    End Property
 
    Public Property Get Items As Variant
    End Property
 
    Public Property Get Size As Long
        Me.Size = m_size
    End Property
 
    Public Function toJsonString() As String
    End Function
End Class

Class JSONList As JSONObject
    Private m_items List As Variant
 
    Sub New()
        m_type = 1
    End Sub
 
    Public Property Get Items As Variant
        Items = Me.m_items
    End Property
 
    Public Sub AddItem(itemName As String, itemVal As Variant)
        If Not Me.HasItem(itemName) Then m_size = m_size + 1
        If IsObject(itemVal) Then
            Set Me.m_items(itemName) = itemVal
        Else
            Me.m_items(itemName) = itemVal
        End If
    End Sub
 
    Public Function HasItem(itemName As String) As Boolean
        HasItem = IsElement(m_items(itemName))
    End Function
 
    %REM
        Function getItemFromObject
        Description: возвращает значение (типы см. GetItemByPath) переданного списка, либо Null
    %END REM
    Private Function getItemFromObject(obj As Variant, itemName As String) As Variant
        If IsElement(obj(itemName)) Then
            If IsObject(obj(itemName)) Then
                getItemFromObject = obj(itemName).Items()
            Else
                getItemFromObject = obj(itemName)
            End If
        Else
            getItemFromObject = Null
        End If
    End Function
 
    %REM
        Возвращает возвращает содержимое элемента по переданному пути;
        Типы возвращаемых значений: String-значение | VARIANT( ) | VARIANT LIST, либо Null - в случае отсутствия элемента.
        Может быть небезопасно:
                v = jsonObj.GetItemByPath("object")("nonexistent") - вернёт ошибку!
            лучше так:
                v = jsonObj.GetItemByPath("object.nonexistent") - вернёт Null
            либо так:
                v = jsonObj.GetItemByPath("object") - и проверить на наличие: IsElement(v("nonexistent"))
    %END REM
    Public Function GetItemByPath(path As String) As Variant
        On Error GoTo ErrH
        Dim arr As Variant, i As Integer, entry As String
        arr = Split(path, ".")
        Dim data As Variant
        data = Me.m_items
        For i = 0 To UBound(arr)
            entry = arr(i)
            If Not IsElement(data(entry)) Then Exit For
            data = getItemFromObject(data, entry)
            If i = UBound(arr) Then
                GetItemByPath = data
                Exit Function
            End If
        Next
        GetItemByPath = Null
Quit:
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
 
    %REM
        Function IsObjectItem
        Description: добавлено для полноценного использования оригинальной GetItem,
            чтобы можно было понять, как присваивать результат переменной, - с помощью Set или прямым присваиванием
    %END REM
    Public Function IsObjectItem(itemName As String) As Boolean
        If HasItem(itemName) Then
            IsObjectItem = IsObject(me.Items(itemName))
        End If
    End Function
 
    %REM
        Оригинальный метод; оставлен для старой возможности использования
    %END REM
    Public Function GetItem(itemName As String) As Variant
        If HasItem(itemName) Then
            If IsObject(me.Items(itemName)) Then
                Set GetItem = me.Items(itemName)
            Else
                GetItem = me.Items(itemName)
            End If
        Else
            GetItem = Null
        End If
    End Function
 
    Public Function toJsonString() As String
        On Error GoTo ErrH
        Dim k As Integer
        Dim ret() As String
        Dim tname As String
        If me.m_size = 0 Then
            toJsonString = "{}"
        Else
            ReDim ret(me.m_size - 1) As String
            ForAll m In m_items
                tname = TypeName(m)
                Select Case tname
                Case "STRING":
                    ret(k) = {"} + ListTag(m) + {":"} + JSON_EscapeString(CStr(m)) + {"}
                Case "DOUBLE", "INTEGER":
                    'дробные числа конвертируем с разделителем "."
                    ret(k) = {"} + ListTag(m) + {":} + Replace(CStr(m), ",", ".")
                Case "BOOLEAN":
                    If m = True Then ret(k) = {"} + ListTag(m) + {":true} Else ret(k) = {"} + ListTag(m) + {":false}
                Case "NOTESDATETIME", "DATE":
                    ret(k) = {"} + ListTag(m) + {":} + Format(m_items(k), DDS_JSON_DT_FORMAT)
                Case "JSONARRAY", "JSONLIST":
                    ret(k) = {"} + ListTag(m) + {":} + m.toJsonString()
                Case Else:
                    Error ERRc_JSON_INVALID_OBJTYPE, ERRt_JSON_INVALID_OBJTYPE + ": " + tname
                End Select
                k = k + 1
            End ForAll
            toJsonString = "{" + Join(ret, ",") + "}"
        End If
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
End Class

Class JSONArray As JSONObject
    Private m_items() As Variant
 
    Sub New()
        m_type = 2
        ReDim Preserve m_items(0)
    End Sub
 
    Public Property Get Items As Variant
        Items = Me.m_items
    End Property
 
    Public Sub AddItem(itemVal As Variant)
        ReDim Preserve m_items(0 To m_size)
        If IsObject(itemVal) Then
            Set m_items(m_size) = itemVal
        Else
            m_items(m_size) = itemVal
        End If
        m_size = m_size + 1
    End Sub
 
    Public Function toJsonString() As String
        On Error GoTo ErrH
        Dim k As Integer
        Dim ret() As String
        Dim tname As String
        If me.m_size = 0 Then
            toJsonString = "[]"
        Else
            ReDim ret(me.m_size - 1) As String
            For k = 0 To me.m_size - 1
                tname = TypeName(m_items(k))
                Select Case tname
                Case "STRING":
                    ret(k) = {"} + JSON_EscapeString(CStr(m_items(k))) + {"}
                Case "DOUBLE", "INTEGER":
                    'дробные числа конвертируем с разделителем "."
                    ret(k) = Replace(CStr(m_items(k)), ",", ".")
                Case "BOOLEAN":
                    If m_items(k) = True Then ret(k) = "true" Else m_items(k) = "false"
                Case "NOTESDATETIME", "DATE":
                    ret(k) = Format(m_items(k), DDS_JSON_DT_FORMAT)
                Case "JSONARRAY", "JSONLIST":
                    ret(k) = m_items(k).toJsonString()
                Case Else:
                    Error ERRc_JSON_INVALID_OBJTYPE, ERRt_JSON_INVALID_OBJTYPE + ": " + tname
                End Select
            Next
            toJsonString = "[" + Join(ret, ",") + "]"
        End If
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
End Class

%REM
    Class UnEscapeEngineBase
    Description: класс для реализации движков постобработки значения параметра, полученного из JSON
        (замены спецсимволов на соотв. им реальные символы)
    Используется:
    1. Как родительский класс для всех подобных классов. Для нового варинта обработки необходимо:
        - создать новый класс, отнаследовав от этого, реализовать метод process;
        - создать новую Public-константу UNESCAPE_ с уникальным значением;
        - добавить код создания объекта созданного класса в JSONParser.
    2. Как заглушка, предоставляющая возможность возвращать значения вовне как есть,
        без обработки escape-последовательностей - соответстсует константе UNESCAPE_RULE_NOTHING
%END REM
Private Class UnEscapeEngineBase
    Public Sub process(sValue As String)
    End Sub
End Class

%REM
    Class UnEscapeEngineSimple
    Description: основной движок замены unescape-последовательностей, покрывающее большую часть случаев
    Включается с помощью константы UNESCAPE_RULE_SIMPLE
%END REM
Private Class UnEscapeEngineSimple As UnEscapeEngineBase
    Public Sub process(sValue As String)
        sValue = Replace(sValue, {\b}, Chr(8))
        sValue = Replace(sValue, "\t", Chr(9))
        sValue = Replace(sValue, "\n", Chr(10))
        sValue = Replace(sValue, "\r", Chr(13))
        sValue = Replace(sValue, {\f}, Chr(12))
        sValue = Replace(sValue, {\"}, {"})
        sValue = Replace(sValue, {\/}, {/})
        sValue = Replace(sValue, {\\}, {\})
    End Sub
End Class

%REM
    Class UnEscapeEngineUnicode
    Description: движок, сочетающий замену стандартных и Unicode- unescape-последовательностей
    Включается с помощью константы UNESCAPE_RULE_UNICODE
%END REM
Private Class UnEscapeEngineUnicode As UnEscapeEngineSimple
    Public Sub process(sValue As String)
        sValue = Replace(sValue, {\u0001}, Chr(1))
        sValue = Replace(sValue, {\u0002}, Chr(2))
        sValue = Replace(sValue, {\u001a}, Chr(26))
        '...
        Call UnEscapeEngineSimple..process(sValue)
    End Sub
End Class

%REM
    Class UnEscapeEnginePath
    Description: движок обработки unescape-последовательностей для путей к файлам
    Включается с помощью константы UNESCAPE_RULE_PATH
%END REM
Private Class UnEscapeEnginePath As UnEscapeEngineBase
    Public Sub process(sValue As String)
        sValue = Replace(sValue, {\/}, {/})
        sValue = Replace(sValue, {\\}, {\})
    End Sub
End Class

%REM
    Description: класс разбора JSON
%END REM
Class JSONParser
    Private m_lLength As Long
    Private m_sDecimalSep As String
    Private m_sFieldMask As String
    Private m_sUnEscRuleDefault As String
    Private m_lstUnEscEngines List As UnEscapeEngineBase
    Private m_lstUnEscFields As Variant
 
    %REM
        Sub New
        sFieldNameMask - установка маски допустимых символов в именах полей (см. Public-константы JSON_FIELD_MASK_...)
    %END REM
    Sub New(sFieldNameMask As String)
        If Len(sFieldNameMask) <> 0 Then m_sFieldMask = sFieldNameMask Else m_sFieldMask = JSON_FIELD_MASK_DEFAULT
        Call addUnEscapeEngine(UNESCAPE_RULE_NOTHING)
    End Sub
 
    %REM
        Property Set UnEscapeRuleDefault
        Description: устанавливает константу, указывающую на правило,
            которое будет использоваться в случае отстутствия задания правил для полей
    %END REM
    Public Property Set UnEscapeRuleDefault As String
        m_sUnEscRuleDefault = UnEscapeRuleDefault
        Call addUnEscapeEngine(m_sUnEscRuleDefault)
    End Property
 
    %REM
        Property Set UnEscapeFields
        Description: правило обработки значений полей - List<String jsonFieldName, String ruleUnEscProcessing>
    %END REM
    Public Property Set UnEscapeFields As Variant
        If InStr(TypeName(UnEscapeFields), "LIST") = 0 Then Error 1111, "Invalid parameter specification - List<String jsonFieldName, String ruleUnEscProcessing> expected!"
        m_lstUnEscFields = UnEscapeFields
        ForAll UnescRule In m_lstUnEscFields
            Call addUnEscapeEngine(CStr(UnescRule))
        End ForAll
    End Property
 
    %REM
        Sub addUnEscapeEngine
        Description: инициализация объектов, соответствующих правилам обработки unescape-последовательностей
    %END REM
    Private Sub addUnEscapeEngine(sUnescRule As String)
        If Not IsElement(m_lstUnEscEngines(sUnescRule)) Then
            Select Case sUnescRule
            Case UNESCAPE_RULE_NOTHING:
                Set m_lstUnEscEngines(sUnescRule) = New UnEscapeEngineBase
            Case UNESCAPE_RULE_SIMPLE:
                Set m_lstUnEscEngines(sUnescRule) = New UnEscapeEngineSimple
            Case UNESCAPE_RULE_UNICODE:
                Set m_lstUnEscEngines(sUnescRule) = New UnEscapeEngineUnicode
            Case UNESCAPE_RULE_PATH:
                Set m_lstUnEscEngines(sUnescRule) = New UnEscapeEnginePath
            Case Else:
                Error 1111, "Rule for constant " + sUnescRule + " is not set. Define it in JSONParser.addUnEscapeEngine."
            End Select
        End If
    End Sub
 
    Private Property Get getDecimalSep As String
        Dim ns As NotesSession
        Dim international As NotesInternational
     
        If Len(m_sDecimalSep) = 0 Then
            Set ns = New NotesSession()
            Set international = ns.International
            m_sDecimalSep = international.DecimalSep
        End If
     
        getDecimalSep = m_sDecimalSep
    End Property
 
    Private Property Get length As Long
        length = m_lLength
    End Property
 
    %REM
        Function parse
        Description: основной метод; возвращает объекты классов наследников JSONObject - JSONList или JSONArray либо ошибку разбора
    %END REM
    Function parse(sJSON As String) As JSONObject
        On Error GoTo ErrH
        Dim index1 As Long
        Dim index2 As Long
     
        m_lLength = Len(sJSON)
     
        index1 = InStr(sJSON, "{")
        index2 = InStr(sJSON, "[")
 
        If index1 > 0 And (index1 < index2 Or index2 = 0) Then
            Set parse = parseList(sJSON, index1 + 1)
        ElseIf index2 > 0 And (index2 < index1 Or index1 = 0) Then
            Set parse = parseArray(sJSON, index2 + 1, "")
        Else
            Error ERRc_JSON_INVALID_FORMAT, ERRt_JSON_INVALID_FORMAT
        End If
         
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " + Error$
    End Function
 
    Private Function parseList(sJSON As String, indexStart As Long) As JSONList
        On Error GoTo ErrH
        Dim vPropertyValue As Variant
        Dim sPropertyName As String
     
        Dim oRes As New JSONList()
        Dim nObjEnd As Long
        nObjEnd = InStr(indexStart, sJSON, "}")
     
        If nObjEnd > indexStart Then
            Dim nNextPair As Long
            nNextPair = InStr(indexStart, sJSON, ":")
         
            While nNextPair < nObjEnd And nNextPair > 0 And nObjEnd > 0
                sPropertyName = findPropertyName(sJSON, indexStart)
                indexStart = InStr(indexStart, sJSON, ":")
                indexStart = indexStart + 1
             
                Call renderValue(sJSON, indexStart, vPropertyValue, sPropertyName)
                Call oRes.AddItem(sPropertyName, vPropertyValue)
             
                nNextPair = InStr(indexStart, sJSON, ":")
                nObjEnd = InStr(indexStart, sJSON, "}")
            Wend
        End If
     
        indexStart = nObjEnd + 1
     
        Set parseList = oRes
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
 
    Private Function parseArray(sJSON As String, indexStart As Long, sPropertyName As String) As JSONArray
        On Error GoTo ErrH
        Dim vPropertyValue As Variant
     
        Dim oRes As New JSONArray()
        Dim nObjEnd As Long
        nObjEnd = InStr(indexStart, sJSON, "]")
     
        If nObjEnd > indexStart Then
            Dim nNextVal As Long
            nNextVal = InStr(indexStart, sJSON, ",")
         
            Do
                Call renderValue(sJSON, indexStart, vPropertyValue, sPropertyName)
                If Not IsEmpty(vPropertyValue) Then
                    Call oRes.AddItem(vPropertyValue)
                End If
             
                nNextVal = InStr(indexStart, sJSON, ",")
                nObjEnd = InStr(indexStart, sJSON, "]")
            Loop While nNextVal < nObjEnd And nNextVal > 0 And nObjEnd > 0
        End If
     
        indexStart = nObjEnd + 1
     
        Set parseArray = oRes
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    End Function
 
    Private Function renderValue(sJSON As String, index As Long, vPropertyValue As Variant, sPropertyName As String) As Variant
        On Error GoTo ErrH
        Dim sChar As String
        Dim l As Long
     
        For l = index To length
            sChar = Mid(sJSON, l, 1)
         
            If sChar = {"} Then
                index = l
                vPropertyValue = findElementString(sJSON, index, sPropertyName)
                l = length
            ElseIf sChar = "{" Then
                index = l
                Set vPropertyValue = parseList(sJSON, index)
                l = length
            ElseIf sChar = "[" Then
                index = l + 1
                Set vPropertyValue = parseArray(sJSON, index, sPropertyName)
                l = length
            ElseIf sChar Like {#} Or sChar = {-} Then
                index = l
                vPropertyValue = findElementNumber(sJSON, index)
                l = length
            ElseIf sChar Like {[tfn]} Then
                index = l
                vPropertyValue = findElementLiteral(sJSON, index)
                l = length
            End If
        Next
        Exit Function
ErrH:
        Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " + Error$
    End Function
 
    Private Function findElementNumber(sJSON As String, index As Long) As Variant
        Dim sRes As String
        Dim sElementEnd As String
        Dim sChar As String
        Dim l As Long
     
        sElementEnd = |, ]}|        'to catch: close bracket, comma, space or }
        For l = index To length
            sChar = Mid(sJSON, l, 1)
         
            If InStr(sElementEnd, sChar) Then
                sRes = Mid(sJSON, index, l - index)
                index = l
                l = length
            End If
        Next
     
        If InStr(sRes, ".") And getDecimalSep() <> "." Then
            sRes = Replace(sRes, ".", getDecimalSep())
        End If
     
        findElementNumber = CDbl(sRes)
    End Function
 
    Private Function findElementLiteral(sJSON As String, index As Long) As Variant
        Dim sRes As String
        Dim sElementEnd As String
        Dim sChar As String
        Dim l As Long
     
        sElementEnd = |, ]}|        'to catch: close bracket, comma, space or }
        For l = index To length
            sChar = Mid(sJSON, l, 1)
         
            If InStr(sElementEnd, sChar) Then
                sRes = Mid(sJSON, index, l - index)
                index = l
                l = length
            End If
        Next
     
        Select Case sRes:
            Case "null":
                findElementLiteral = Null
            Case "true":
                findElementLiteral = True
            Case "false":
                findElementLiteral = False
        End Select
    End Function
 
    %REM
        Ищет и возвращает значение элемента, начиная поиск с позиции index
    %END REM
    Private Function findElementString(sJSON As String, index As Long, sPropertyName As String) As String
        Dim sValue As String
        Dim index1 As Long, index1tmp As Long, index2 As Long
        Dim bUnEscape As Boolean
     
        index1 = InStr(index, sJSON, {"})
        If index1 = 0 Then Exit Function
     
        index1tmp = index1
        Do
            index2 = InStr(index1tmp + 1, sJSON, {"})
            'проверяем наличие экранирующего символа перед найденной закрывающей кавычкой
            If Mid$(sJSON, index2 - 1, 1) = "\" Then
                bUnEscape = True    'если он, то это кавычка, как часть строки - продолжаем искать окончание элемента дальше
            Else
                Exit Do                'если это НЕ он, то это действительно окончание элемента - выходим
            End If
            index1tmp = index2
        Loop
     
        'значение элемента
        sValue = Mid(sJSON, index1 + 1, index2 - index1 - 1)
     
        If Not bUnEscape Then
            bUnEscape = InStr(sValue, "\") <> 0
        End If
     
        If bUnEscape Then
            'постобработка элемента
            Dim oUnEscEngine As UnEscapeEngineBase
            Set oUnEscEngine = m_lstUnEscEngines(m_sUnEscRuleDefault)
            If Len(sPropertyName) <> 0 Then
                If Not IsEmpty(m_lstUnEscFields) Then
                    If IsElement(m_lstUnEscFields(sPropertyName)) Then
                        Set oUnEscEngine = m_lstUnEscEngines(m_lstUnEscFields(sPropertyName))
                    End If
                End If
            End If
            Call oUnEscEngine.process(sValue)
        End If
     
        index = index2 + 1
     
        findElementString = sValue
    End Function
 
    %REM
        Ищет и возвращает имя элемента, начиная поиск с позиции index
    %END REM
    Private Function findPropertyName(sJSON As String, index As Long) As String
        Dim sRes As String
        Dim sPropertyNameEnd As String
        Dim sChar As String
        Dim l As Long
     
        'property start with character
        For l = index To length
            sChar = Mid(sJSON, l, 1)
            If sChar Like m_sFieldMask Then
                sRes = sChar
                index = l + 1
                l = length
            End If
        Next
     
        'rest of property could be characters and numbers etcx
        sPropertyNameEnd = | :"'|
        For l = index To length
            sChar = Mid(sJSON, l, 1)
            If InStr(sPropertyNameEnd, sChar) Then
                index = l
                l = length
            Else
                sRes = sRes + sChar
            End If
        Next
     
        findPropertyName = sRes
    End Function
End Class

%REM
    Function JSON_EscapeString
    Description: sJSON - строка с JSON, которая будет обработана
    Использовать только для значений полей в json!!!
%END REM
Private Function JSON_EscapeString(sJSON As String)
    sJSON = Replace(sJSON, {\}, {\\})
    sJSON = Replace(sJSON, Chr(8), {\b})
    sJSON = Replace(sJSON, Chr(9), {\t})
    sJSON = Replace(sJSON, Chr(13), {\r})
    sJSON = Replace(sJSON, Chr(10), {\n})
    sJSON = Replace(sJSON, Chr(12), {\f})
    sJSON = Replace(sJSON, Chr(19), {})
    sJSON = Replace(sJSON, {"}, {\"})
    sJSON = Replace(sJSON, {/}, {\/})
    JSON_EscapeString = sJSON
End Function

%REM
    Function JSON_parse
    Description: Разбирает JSON и возвращает JSONList либо JSONArray
    Parameters:
        sFieldNameMask - маска допустимых символов в именах полей (см. Public-константы JSON_FIELD_MASK_...);
            если передана пустая строка, то будет использована маска по умолчанию (стандартная)
        sUnescRuleDefault - правило обработки unescape-последовательностей, которое будет использовано по умолчанию
        lstUnescFields - List<String jsonFieldName, String ruleUnEscProcessing> либо Null
%END REM
Public Function JSON_parse(sJSON As String, sFieldNameMask As String, sUnescRuleDefault As String, lstUnescFields As Variant) As Variant
    On Error GoTo ErrH
    Dim oJsonParser As New JSONParser(sFieldNameMask)
    oJsonParser.UnEscapeRuleDefault = sUnescRuleDefault
    If Not IsNull(lstUnescFields) Then oJsonParser.UnEscapeFields = lstUnescFields
    Set JSON_parse = oJsonParser.Parse(sJSON)
    Exit Function
ErrH:
    Dim sErrText As String
    sErrText = GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    Select Case Err
        Case ERRc_JSON_INVALID_FORMAT, 13, 91, 184:
            sErrText = sErrText + ", JSON: (" + sJSON + ")"
    End Select
    Error Err, sErrText
End Function

%REM
    Запись переданного JSON в поля Domino-документа
    iRTHandling - описание см. в JSON_parseToDocument
%END REM
Public Function JSON_setToDocument(oJson As JSONList, ndTo As NotesDocument, iRTHandling As Integer) As Boolean
    On Error GoTo ErrH
    Dim v As Variant
 
    If oJson.Size <> 0 Then
        Dim sItemName As String, item As NotesItem
        Dim bTmpItem As Boolean
     
        ForAll vItemValue In oJson.Items()
            sItemName = ListTag(vItemValue)
            Do
                If Left(sItemName, 1) = "@" Then
                    'обработка системных свойств NotesDocument'а, полученных в json'е
                    Select Case sItemName
                    Case DDS_JSON_UNID_TAG:
                        ndTo.UniversalID = vItemValue
                        Exit Do
                    Case DDS_JSON_FORM_TAG:
                        Call ndTo.ReplaceItemValue("Form", vItemValue)
                        Exit Do
                    Case Else:
                        bTmpItem = True
                    End Select
                Else
                    bTmpItem = False
                End If
             
                If IsScalar(vItemValue) Then
                    Call ndTo.ReplaceItemValue(sItemName, vItemValue)
                Else
                    If TypeName(vItemValue) = "JSONARRAY" Then
                        Set item = ndTo.ReplaceItemValue(sItemName, vItemValue.Items())
                    Else
                        Select Case iRTHandling
                        Case 0:
                            Set item = ndTo.ReplaceItemValue(sItemName, "multipart")
                            item.SaveToDisk = False
                        Case 1:
                            Set item = ndTo.ReplaceItemValue(sItemName, vItemValue.toJsonString())
                        Case -1:
                            Error ERR_NOTES_INVALID_DOC, {Invalid structure of document: item '} + sItemName + {' has unsupported type}
                        Case Else:
                            Error 1111, "Unsupported value of function parameter iRTHandling: " & iRTHandling
                        End Select
                    End If
                 
                    If bTmpItem Then item.SaveToDisk = False
                End If
                Exit Do
            Loop
        End ForAll
     
        JSON_setToDocument = True
    End If
 
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Function JSON_parseToDocument
    Description: создание документа и заполнение его значениями из переданного json
    Parameters:
        iRTHandling - поведение при встрече RichText'а:
            -1 - генерируется ошибка lsERR_NOTES_INVALID_DOC (полезно для документов, не предполагающих хранение RichText'а);
            0 - создаётся item с текстом "multipart" (чтобы при необходимости м.б. проанализировать), который не сохраняется на диске;
            1 - создаётся item с json данного item'а.
        sUnescRuleDefault, lstUnescFields - описание см. в JSON_parse
%END REM
Public Function JSON_parseToDocument(sJSON As String, sUnescRuleDefault As String, lstUnescFields As Variant, _
    ndbTrg As NotesDatabase, iRTHandling As Integer) As NotesDocument
    On Error GoTo ErrH
    Dim oJson As JSONList
    Set oJson = JSON_parse(sJSON, JSON_FIELD_MASK_DOMINO, sUnescRuleDefault, lstUnescFields)
 
    Dim ndNew As NotesDocument
    Set ndNew = ndbTrg.CreateDocument()
 
    If JSON_setToDocument(oJson, ndNew, iRTHandling) Then
        Set JSON_parseToDocument = ndNew
    End If
    Exit Function
 
ErrH:
    Dim sErrText As String
    sErrText = GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    If Err = ERR_NOTES_INVALID_DOC Then sErrText = sErrText + ", JSON: " + sJSON
    Error Err, sErrText
End Function

%REM
    Обёртка над oJSON.GetItem() для удобства возврата "" в случае отсутствия элемента.
    Параметры:
        oJSON - JSONList, полученный в результате разбора JSON
        sElementPath - путь к элементу в стуктуре json, разделённый "."
    Перечень возможных возвращаемых значений:
        - "" - в случае отсутствия элемента;
        - скаляр (строка/число);
        - VARIANT( );
        - VARIANT LIST.
%END REM
Public Function JSON_getItemValue(oJSON As JSONList, sElementPath As String) As Variant
    On Error GoTo ErrH
    JSON_getItemValue = oJSON.GetItemByPath(sElementPath)
    If IsNull(JSON_getItemValue) Then JSON_getItemValue = ""
    Exit Function
ErrH:
    Error Err, GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
End Function

%REM
    Function JSON_parseToDocumentSame
    Description: создание документа и заполнение его значениями из переданного json
        Полезно использовать, если функция будет вызываться многократно в цикле, из-за того,
        что данные будут записываться в тот же самый документ не пересоздавая его, время на сервере не будет уходить вперёд.
    Внимание! Если документ после первого вызова был сохранён извне, и планирутеся сохранять его и после следующих вызовов,
        то эту функцию использовать нельзя, т.к. из-за Static предыдущий документ будет перезаписан полями из последующих вызовов!
        При необходимости сохранения документа используйте JSON_parseToDocument либо удаляйте (Delete) объект документа перед вызовом данной функции!
    iRTHandling - описание см. в JSON_parseToDocument
    sUnescRuleDefault, lstUnescFields - описание см. в JSON_parse
%END REM
Public Function JSON_parseToDocumentSame(sJSON As String, sUnescRuleDefault As String, lstUnescFields As Variant, _
    ndbTrg As NotesDatabase, iRTHandling As Integer) As NotesDocument
    On Error GoTo ErrH
    Dim oJson As JSONList
    Set oJson = JSON_parse(sJSON, True, sUnescRuleDefault, lstUnescFields)
 
    Static nd As NotesDocument
    If nd Is Nothing Then
        Set nd = ndbTrg.CreateDocument()
    Else
        'Очищаем все имеющиеся поля!
        ForAll item In nd.Items
            If Not item Is Nothing Then
                Call nd.RemoveItem(item.Name)
            End If
        End ForAll
    End If
 
    If JSON_setToDocument(oJson, nd, iRTHandling) Then
        Set JSON_parseToDocumentSame = nd
    End If
    Exit Function
ErrH:
    Dim sErrText As String
    sErrText = GetThreadInfo(1) & " (" & Erl & ") -> " & Error$
    If Err = ERR_NOTES_INVALID_DOC Then sErrText = sErrText + ", JSON: " + sJSON
    Error Err, sErrText
End Function

Изменения:
1. JSON_setToDocument:
- исправлена запись пустой строки в системные поля, содержащие массив значений;
- исправлена ошибка "Notes error: Datatype is not supported", возникающая при встрече неожидаемого объекта JSONList (для NotesRichtextItem'а) - добавлен параметр iRTHandling с разными вариантами поведения при встрече RichText'а или сложного содержимого.
2. JSON_parse, JSON_parseToDocument: унифицирована выдача JSON в текст ошибок ERRc_JSON_INVALID_FORMAT, lsERR_NOTES_INVALID_DOC соответственно.
3. Исправлено падение - парсер заваливался при получении ошибки от Domino в виде HTML - JSONParser.parse: ошибка неверной структуры JSON "Invalid JSON format: Type mismatch (13); JSON: <HTML><HEAD><TITLE>Unable to Process Request</TITLE></HEAD><BODY><P>Http Status Code: 404</P><P>Reason: File not found or unable to read file</P></BODY></HTML>".
4. JSON_parse: добавлен вывод JSON по критическим ошибкам при разборе json; JSONParser.renderValue: добавлен обработчик ошибок.
5. JSON_parse: расширена возможность задания перечня возможных символов в именах полей (ранее было только 2 варианта - стандартный и Domino) - параметр bDominoJson As Boolean заменён на sFieldNameMask As String, передаваемый в конструктор класса JSONParser для явного задания маски. Т.е. в этом смысле парсер отвязан от специфики Domino.
6. По мотивам JSON_parseToDocumentExt (@lmike) добавлена JSON_parseToDocumentSame для вызова в цикле на временном документе.
7. Исправлены ошибки, возникавшие на пустых элементах [] и {}:
- "Invalid JSON format: Out of stack space (28)" - в JSONParser:: parseArray, parseList при попытке разбора таких json (@chivononok);
- "Attempt to access uninitialized dynamic array {200}" - при попытке получения такого элемента с помощью JSONList.GetItemByPath.
8. Решена проблема с возможной утерей части путей из-за символа \t (@lmike); такие же проблемы могли быть и с другими похожими символами. Текущая реализация также позволяет возвратить оригинальные значения, полученные из json, без постобработки unescape-последовательностей.
9. JSONParser.parse вместо Variant теперь возвращает объект класса JSONObject; откорректированы типы некоторых других переменных.
10. JSONObject: добавлена возможность возврата кода, соответствующего типу объекта (JSONList или JSONArray).
11. JSONParser: выполнен косметический рефакторинг - переменные названы более удобно и однообразно.
 
  • Нравится
Реакции: rinsk
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!