VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "JSF" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = False '------------------------------------------------------------------------------- 'File : JSF.cls ' http://wiki.yaslaw.info/doku.php/vba/classes/jsf ' All rights reserved 'Environment : Access VBA 2007+ 'Version : 1.4.2 'Name : JSF 'Author : Stefan Erb (ERS) 'History : 15.11.2018 - ERS - Creation ' 14.10.2019 - ERS - Kleiner Fehler im toString korrigiert ' 24.12.2019 - ERS - Regex zur Erkennung der Elemente verbessert ' 16.01.2020 - ERS - Modifiers hinzugefügt ' 27.05.2020 - ERS - Excel-Modifikatioen, Modifier tbl ' 29.05.2020 - ERS - modifier tbl korrigiert ' 18.06.2020 - ERS - Feler bei modifier-Params behoben. Sollte es sich um ein Wert aus dem map handeln, muss er nicht mehr kleingeschrieben sein '------------------------------------------------------------------------------- Option Explicit '------------------------------------------------------------------------------- ' -- ! SETTINGS ! '------------------------------------------------------------------------------- 'MS Applikation in der die Klasse eingesetzt wird. Es gibt einige Spezifische Dinge 'Für Word etc noch nicht getestet #Const C_ACCESS = "ACCESS" #Const C_EXCEL = "EXCEL" 'Auswahl #Const ms_product = C_EXCEL ' /** ' * Wenn sprintf existiert, dann kann mit $format das Format analog zu sprintf() mitgegeben werden. Ansonsten wird der $-Teil ignoriert ' * @url http://wiki.yaslaw.info/doku.php/vba/functions/printf/index ' * @example ?JSF().parse("#{now $To}") -> 20181109 ' * @example ?JSF().parse("#{1.3 $.3f}") -> 1.300 ' * @example ?JSF("DUAL").parse("#{now $TO}_#{id $6d}_DATA.CSV") -> 20181109_092806_000001_DATA.CSV ' */ #Const sPrintF_exists = True '/** ' * Wenn mein json-Modul vorhanden ist, dann kann man auch json-Strings als Paramter übergeben. ' * Achtung! Nur eindimensionale Dictionary! ' * @url http://wiki.yaslaw.info/doku.php/vba/cast/json ' * @example ?JSF("{nr:1,name:Yaslaw}").parse("#{name}, du hast die Nummer #{nr} ") -> Yaslaw, du hast die Nummer 1 ' */ #Const json_exists = True 'Für das Logging kann log4vba verwendet werden. 'https://wiki.yaslaw.info/doku.php/vba/classes/log4vba #Const Log4vba = True #If Log4vba Then 'Und die Settings dazu Private Const C_ERROR_LOG_SETTINGS = eprOutConsole + eprReturnAssert + eprOutMsgBox #End If 'Mit DEBUG_MODE True wird bei Fehler der debug.assert aktiviert Private Const DEBUG_MODE = False Private Const C_ERR_HANDLER_DEFAULT = 1 'errMsgBox '/** ' * @pattern #{formel} ' * Formel: Auf die Felder der Source kann direkt zugegriffen werden: #{company} ' * Es können mehrere #{} Blöcke in einem Text stehen. In sich sind sie aber nicht schachtelbar ' * Fixwerte können in vba/sql Schreibweise innerhalb der #{} geschrieben werden ' * Alles was mit eval() ausgeführt werden, kann innerhalb der #{} stehen. Auch eigene Public Functions ' * Für den IIF() ist auch die abgekürzte Syntax möglich #{[not] [when] ? [then] : [else]} ' * Das Not ist optional ' * Der else-Teil ist optional. Standardwert ist '': #{[when] ? [then]} ' * Leerzeichen innerhalb des IIF werden ignoriert: #{[when] ? [then]} == #{[when]?[then]} ' * Anstelle von NOT ist auch der ! gültig. #{!isNull(company) ? 'Firma' & company} ' * Für Vergleiche auf Unterschiede <> kann auch != verwendet werden ' * Formatzeichen (\t, \n, \r) können irgendwo im Text stehen und werden geparst. Innerhalb der #{} müssen sie als Text vorhanden sein ' * ' * @example ?JSF("TBL_DATA_COMPANY", "CID=2").parse("Welcome #{company}") -> Welcome ERB software ' * @example ?JSF("TBL_DATA_COMPANY", "CID=2").parse("Welcome #{ucase(company)}") -> Welcome ERB SOFTWARE ' * @example ?JSF("TBL_DATA_COMPANY", "CID=2").parse("Welcome\t\t#{ucase(company)}") -> Welcome ERB SOFTWARE ' */ '------------------------------------------------------------------------------- ' -- Public Members '------------------------------------------------------------------------------- '/** ' * Paramters für die Klasse ' */ Public Enum jsfParams [_NA] = -1 jsfNothing = 0 jsfParseFormat = 2 ^ 0 'Soll \t (Tabulator) und \r (NewLine) geparst werden jsfRemoveNewLine = 2 ^ 1 'Zeilenumbrüche im Pattern werden ignoriert jsfUnixNewLine = 2 ^ 2 'Zeilenumbruch nur mit NewLine, ohne CarriageReturn jsfRemoveEmptyLine = 2 ^ 3 'Leere Zeilen entfernen jsfRichText = 2 ^ 4 'Der Pattern kommt als RichText (MS Format: vereinfachtes HTML) jsfTrimMultiSpaces = 2 ^ 5 'Mehrfach Leerzeichen reduzieren jsfTrimLines = 2 ^ 6 'Zeilen hinten und vorne von Leerzeichen befreien [_Default] = jsfParseFormat + jsfTrimMultiSpaces + jsfTrimLines + jsfRemoveEmptyLine End Enum '------------------------------------------------------------------------------- ' -- Private Members '------------------------------------------------------------------------------- Private pParams As jsfParams Private pFormatPattern As String 'Falls sprintf existiert, werden im pFormatPattern das Format gespeichert Private pModifiers As Object 'Dictionary mit den aktuellen Modifiers Private pModifier$ Private pCriteria As String #If Log4vba Then Private pLogger As Log4vba 'Logger #End If '------------------------------------------------------------------------------- ' -- Constructors '------------------------------------------------------------------------------- '/** ' * Logger ' * @param Error ' * @param String Name der Methode ' * @param Array<> Ein Arry mit den Aufrufparamtern der Method ' */ Private Function handleError(ByRef iError As Variant, ByVal iSourceName As String, ParamArray iValueArray() As Variant) As Variant Dim valueArray() As Variant: If UBound(iValueArray) > -1 Then valueArray = CVar(iValueArray) #If Log4vba Then Debug.Assert Not DEBUG_MODE And pLogger.error(iError, iSourceName, valueArray) #Else MsgBox "Error: " & iError.Number & vbCrLf & iError.Description Debug.Assert Not DEBUG_MODE #End If End Function '/** ' * Erstellt ein neues Objekt und initialisiert es ' * @param Dictionary/Recordset/TableDef/QueryDef/Iterator/Sql/TableName/QueryName/Array ' * @param String Einen Filter umd auf den richtigen Datensatz zu springen. Nur gültig für Recordset, TableDef und ähnliches ' * @param jsfParams Paramter um die Klasse zu steuern ' * @return JSF ' */ Public Function instance( _ Optional ByRef iSource As Variant = Nothing, _ Optional ByVal iCriteria As String, _ Optional ByVal iParams As jsfParams = jsfParams.[_Default] _ ) As JSF Attribute instance.VB_UserMemId = 0 'Attribute instance.VB_UserMemId = 0 Set instance = New JSF instance.construct iSource, iCriteria, iParams End Function '/** ' * Fügt eine Paramtertabelle zu. Eine Tabelle mit einer SPalte für die Keys und eine für die Values ' * @param Recordset/TableDef/QueryDef/SourceName/Sql ' * @param String Name des Key-Feldes ' * @param String Name des Value-Feldes ' * @param String Name des Feldes, dass den vbVarType Enthält ' * @param jsfParams Paramter um die Klasse zu steuern ' * @return JSF ' */ Public Function instanceParamTable( _ ByRef iSource As Variant, _ Optional ByVal iKeyName As String = "KEY", _ Optional ByVal iValueName As String = "VAL", _ Optional ByVal iVarTypeName As String = "", _ Optional ByVal iParams As jsfParams = jsfParams.[_Default] _ ) As JSF Set instanceParamTable = New JSF instanceParamTable.constructParamTable iSource, iKeyName, iValueName, iVarTypeName, iParams End Function '/** ' * Initialisiert ein JSF-Objekt ' * @param Dictionary/Recordset/TableDef/QueryDef/Iterator/Sql/TableName/QueryName/Array ' * @param String Einen Filter umd auf den richtigen Datensatz zu springen. Nur gültig für Recordset, TableDef und ähnliches ' * @param jsfParams Paramter um die Klasse zu steuern ' * @return JSF ' */ Public Function construct( _ Optional ByRef iSource As Variant = Nothing, _ Optional ByVal iCriteria As String, _ Optional ByVal iParams As jsfParams = jsfParams.[_Default] _ ) As JSF On Error GoTo Err_Handler params = iParams pCriteria = iCriteria clearMap addSource iSource, iCriteria Set construct = Me Exit_Handler: Exit Function Err_Handler: handleError Err, "construct", iSource, iCriteria, iParams Resume Exit_Handler Resume End Function '/** ' * Fügt eine Paramtertabelle zu. Eine Tabelle mit einer SPalte für die Keys und eine für die Values ' * @param Recordset/TableDef/QueryDef/SourceName/Sql ' * @param String Name des Key-Feldes ' * @param String Name des Value-Feldes ' * @param String Name des Feldes, dass den vbVarType Enthält ' * @param jsfParams Paramter um die Klasse zu steuern ' * @return JSF ' */ Public Function constructParamTable( _ ByRef iSource As Variant, _ Optional ByVal iKeyName As String = "KEY", _ Optional ByVal iValueName As String = "VAL", _ Optional ByVal iVarTypeName As String = "", _ Optional ByVal iParams As jsfParams = jsfParams.[_Default] _ ) As JSF params = iParams clearMap addParamTable iSource, iKeyName, iValueName, iVarTypeName Set constructParamTable = Me End Function '------------------------------------------------------------------------------- ' -- Public Methodes '------------------------------------------------------------------------------- '/** ' * Parst einen Text mit den vorhandenen Daten ' * @param String ' * @param jsfParams Paramter um das Parsen zu steuern. Überschreibt für den einen Parse-Vorang die Klassenparameters ' * @return String ' */ Public Function parse(ByVal iString As String, Optional ByVal iParams As jsfParams = jsfParams.[_NA]) As String On Error GoTo Err_Handler Dim origParams As jsfParams: origParams = params If Not iParams = jsfParams.[_NA] Then params = iParams Dim text As String: text = iString If andB(params, jsfRemoveNewLine) Then removeCarriageReturn text 'CarriageReturn (\r) entfernen removeEmptyTags text 'Leere Tags entfernen Dim textRev As String: textRev = StrReverse(text) removeNewLineRev textRev 'Entfernt Zeilenumbrüche ' Static rxJsfRev As Object: cRxP rxJsfRev, "\}(?!\\)(?:([^\s\}]+)\$)?([^\{]*?)\{(?!\\)#(?!\\)", , True ' Static rxJsfRev As Object: cRxP rxJsfRev, "\}(?!\\)([^\{]*?)\{(?!\\)#(?!\\)", , True Static rxJsfRev As Object: cRxP rxJsfRev, "\}(?!\\)(?:(\w+)\$)?\s*([^\{]*?)\{(?!\\)#(?!\\)", , True Do While rxJsfRev.test(textRev) Dim parts As Object: Set parts = rxJsfRev.Execute(textRev)(0).subMatches Dim itemRev As String: itemRev = parts(1) pFormatPattern = StrReverse(parts(0)) itemRev = parseJsfRev(itemRev) textRev = rxJsfRev.Replace(textRev, itemRev) Loop parseFormatRev textRev text = StrReverse(textRev) removeEmptyTags text 'Leere Tags entfernen (RichText) (ausser
) trimLines text 'Zielen trimmen removeEmptyLine text 'Leere Zeilen entfernen trimMultiSpaces text 'Mehrfach Leerzeichen reduzieren winNewLine text Exit_Handler: parse = text params = origParams Exit Function Err_Handler: params = origParams Err.Raise Err.Number, "JSF.parse", Err.Description, Err.HelpFile, Err.HelpContext Resume End Function '/** ' * Fügt ein Wert der Map zu oder überschreibt ihn ' * @param String Key ' * @param Variant Value ' * @param Boolean Flag ob exisiteirende Keys überschrieben werden sollen ' * @return JSF ' */ Public Function add(ByVal iKey As String, ByVal iValue As Variant, Optional ByVal iOverwrite As Boolean = True) As JSF Dim key1 As String, key2 As String: key1 = createKey(iKey, key2) If map.exists(key1) Then If iOverwrite Then map(key1) = iValue Else map.add key1, iValue End If If Not map.exists(key2) Then map.add key2, iValue Set add = Me End Function '/** ' * Fügt eine Source zum bestehenden Mapping hinzu ' * @param Dictionary/Recordset/TableDef/QueryDef/Iterator/Fields/String/Array ' * @param String Kriterium ' * @param Boolean True: bestehende Keys werden überschrieben ' * @return JSF ' */ Public Function addSource(ByRef iSource As Variant, Optional ByVal iCriteria As String, Optional ByVal iOverwrite As Boolean = True) As JSF Dim fld As Object Dim i As Long On Error GoTo Err_Handler If IsArray(iSource) Then For i = LBound(iSource) To UBound(iSource) add "[" & i & "]", iSource(i) Next i GoTo Exit_Handler End If Select Case TypeName(iSource) Case "Nothing" 'Dictionary: übersetzen, damit alle Keys in Lower Case sind Case "Dictionary" Dim keys As Variant: keys = iSource.keys For i = 0 To UBound(keys) add keys(i), iSource(keys(i)), iOverwrite Next i 'Recordset. Der Standard Case "Recordset2", "Recordset" 'Falls Kriterien vorhanden sind, versuchen auf den Datensatz zu springen If iCriteria <> "" Then iSource.FindFirst iCriteria If iSource.noMatch Then For Each fld In iSource.fields add fld.Name, fld.value, iOverwrite Next fld GoTo Exit_Handler End If End If addSource iSource.fields, iOverwrite Case "Fields" For Each fld In iSource add fld.Name, fld.value, iOverwrite Next fld 'Iterator Case "Iterator" Select Case TypeName(iSource.source) Case "Recordset2": Set source = iSource.source Case Else: Do While iSource.toNext add iSource.key, iSource.value, iOverwrite Loop End Select 'QueryDef und TableDef. Den Recordset öffnen und verarbeiten Case "QueryDef", "TableDef": addSource iSource.openRecordset Case "JSF": addSource iSource.map, iOverwrite Case "String" #If json_exists Then Dim var As Variant: ref var, json2obj(iSource) If TypeName(var) = "Dictionary" Then addSource var, iOverwrite GoTo Exit_Handler End If #End If #If ms_product = C_ACCESS Then If Not trim(LCase(iSource)) Like "select*" Then iSource = "select * from " & iSource addSource CurrentDb.openRecordset(iSource), iCriteria, iOverwrite #End If Case Else: Err.Raise 13 End Select Exit_Handler: Set addSource = Me Exit Function Err_Handler: Select Case Err.Number Case 3070: 'Der Filter kann nicht angewendet werden Err.Raise Err.Number, "JSF.source", Err.Description Case Else: 'Den Fehler weiterreichen Err.Raise Err.Number, "JSF.source", Err.Description End Select Resume End Function '/** ' * Fügt eine Paramtertabelle zu. Eine Tabelle mit einer Spalte für die Keys und eine für die Values ' * @param Recordset/TableDef/QueryDef/SourceName/Sql ' * @param String Name des Key-Feldes ' * @param String Name des Value-Feldes ' * @param String Name des Feldes, dass den vbVarType Enthält ' * @param Boolean True: bestehende Keys werden überschrieben ' * @return JSF ' */ Public Function addParamTable( _ ByRef iSource As Variant, _ Optional ByVal iKeyName As String = "KEY", _ Optional ByVal iValueName As String = "VAL", _ Optional ByVal iVarTypeName As String = "", _ Optional ByVal iOverwrite As Boolean = True _ ) As JSF Dim fld As Object On Error GoTo Err_Handler Select Case TypeName(iSource) Case "Nothing" 'Dictionary: übersetzen, damit alle Keys in Lower Case sind 'Recordset. Daer Standard Case "Recordset2" 'Falls Kriterien vorhanden sind, versuchen auf den Datensatz zu springen iSource.MoveFirst Do While Not iSource.EOF Dim value As Variant: value = iSource(iValueName).value If Not iVarTypeName = "" Then Select Case iSource(iVarTypeName) Case vbString: value = value Case vbBoolean: value = CBool(value) 'Case Else: value = cVal(value) Case Else: value = eval(value) End Select End If Loop #If ms_product = C_ACCESS Then 'QueryDef und TableDef. Den Recordset öffnen und verarbeiten Case "QueryDef", "TableDef" addParamTable iSource.openRecordset, iKeyName, iValueName, iVarTypeName, iOverwrite Case "String" If Not trim(LCase(iSource)) Like "select*" Then iSource = "select * from " & iSource addParamTable CurrentDb.openRecordset(iSource), iKeyName, iValueName, iVarTypeName, iOverwrite #End If Case Else: Err.Raise 13 End Select Exit_Handler: Set addParamTable = Me Exit Function Err_Handler: Err.Raise Err.Number, "JSF.source", Err.Description End Function '/** ' * Handelt den object-Cache um ein Set-String zu zerlegen ' * @return object ' */ Private Property Get rxSetString() As Object Static rxCachedSetString As Object If rxCachedSetString Is Nothing Then Set rxCachedSetString = CreateObject("VBScript.RegExp") rxCachedSetString.Global = True rxCachedSetString.pattern = "(?:(['""#])(?!\\)(.+?)\1(?!\\)|([0-9\.]+?))\s*(?:>=|[:=])\s*(?:\]([^\[]+)\[|(['""])(?!\\)(.+?)\5(?!\\)|(\w+))" End If Set rxSetString = rxCachedSetString End Property Private Function eval(ByVal iValue As String) As Variant Dim value As Variant: value = trim(iValue) Dim rx As Object: Set rx = cRx("/^(['""#])([^\1]+)\1$/") If rx.test(value) Then Dim sm As Object: Set sm = rx.Execute(value)(0).subMatches value = sm(1) Select Case sm(0) Case "#": value = dateValue(value) 'eval("#" & value & "#") Case Empty: value = CDec(value) End Select End If eval = value End Function '/** ' * leert die Map ' * @return JSF ' */ Public Function clearMap() As JSF map.RemoveAll Set clearMap = Me End Function '/** ' * ersetzt ein Paramter und gibt die Instanz zurück ' * @example: text = JSF("my_table").setParam(jsfUnixNewLine, true).parse("#{anrede} #{name}") ' * @param jsfParams ' * @param Boolean ' * @return JSF ' */ Public Function setParam(ByVal iParam As jsfParams, Optional ByVal iFlag As Boolean = True) As JSF param(iParam) = iFlag Set setParam = Me End Function '------------------------------------------------------------------------------- ' -- Public Properties '------------------------------------------------------------------------------- '/** ' * @param Sql/TableName/QueryName ' */ Public Property Let source(ByRef iSource As Variant) clearMap addSource iSource, pCriteria End Property '/** ' * @param Dictionary/Recordset/TableDef/QueryDef/Iterator ' */ Public Property Set source(ByRef iSource As Variant) clearMap addSource iSource, pCriteria End Property '/** ' * @param Dictionary