Die Klasse Expression mit ihren dazugehörenden Packages kann einen als String
übergebenen Ausdruck Parsen, Compilieren und Ausführen.
Sie ist als Baustein meines Fluss-Steuerungs-Frameworks CnC (Control-and-Comand)
gedacht.
Mit der Einführung der Sequenzen ist es nun möglich mehrzeilige Ausdrücke zu schreiben.
Der Zeilenumbruch ist nun ein normales weisses Zeichen.
try { Expression exp = Expression.parse("a+b"); } catch (ExpressionParseException exc) { System.err.println(exc.getMessage()); }
Der Expression-Parser führt eine vorausschauende Typ-Prüfung durch. Dadurch werden offensichtliche Fehler, wie numerische Operationen auf Strings, abgelehnt. Beim Zugriff auf Variablen kann der Typ (noch) nicht zur Übersetzungszeit festgestellt werden, so dass hier keine Typ-Prüfung erfolgt. Mit der geplanten Variablen-Typisierung ist das dann auch möglich.
Ein einmal kompilierter (geparster) Ausdruck kann beliebig oft ausgeführt (evaluiert) werden.
Object oValue = null; try { //Laufzeit-Umgebung mit Variablen AbstractRuntimeEnvironment runEnv = new StandaloneRuntimeEnvironment() ; //Setzen Anfangsvariable runEnv.setVariable("a", new Double(1)); runEnv.setVariable("b", new Double(2)); oValue = exp.eval( runEnv ); // siehe oben: "a+b" } catch (ExpressionEvaluationException exc) { System.err.println(exc.getMessage()); } System.out.println( "oValue:"+oValue);
Das RuntimeEnvironment enthält die Variablen mit ihrem Namen als Key und ihrem Value als Objekt. Denkbar sind noch Erweiterungen wie Locale und Betriebsartenschalter.
Im CnC-Framework (JSP-Fluss-Steuerung Workflow) wird hier die Umgebung des CnC, also der JSP-Adressraum, request, session und application eingesetzt.
Variablen-Namen sind case-sensitive, dass heisst, Gross-/Kleinschreibung wird unterschieden.
Im StandaloneRuntimeEnvironment wird eine HashMap für die Variablen verwendet. Später sind folgende Erweiterungen möglich:
- lokale Vars (Stack),
- Methode posteval für Funktionalität preSetLater oder postSetLater (siehe Inline-Operatoren)
- Variablen-Typisierung
Alle Java-Elementar-Typen werden durch Objekte ihrer Wrapper-Klassen gespeichert.
Die Typ-Definition ist in der Klasse de.cnc.expression.Types hinterlegt.
Es werden folgende Typen unterstützt:
-Numerisch:
BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short (java.lang.Number)
aus Genauigkeitsgründen wird intern immer mit BigDecimal gerechnet
-Boolean
Für Boolsche Werte gibt es die reservierten Worte true und false.
Intern werden stets die Boolean-Wrapper-Klassen verwendet.
-String (eventuell später noch StringBuffer unified)
Strings sind in folgender Schreibweise möglich:
"abc"
'abc'
Die Möglichkeit der Verwendung von Hochkommas in Verbindung mit den reservierten Worten apostroph oder quote sowie highcomma oder higComma erlaubt die Lösung von Problemen mit Esc-Sequenzierung/Encoding
Beispiel input-Tag:
input value="<%= Expression.eval("'abc'+apostroph+'def'")%> ...
Dadurch ist auch die Verwendung meiner kleinen Expression-Library in JSP-Tags möglich.
Zur Realisierung eines dynamischen Verhaltens kommt noch ein Typ Expression hinzu.
Als Erweiterung wäre es sinnvoll statt eines eigenen Typsystems ( siehe
de.cnc.expression.Types ) im Expression-Parser die Java-Klassen als Typen zu
verwenden. Das realisiere ich eventuell später.
-Collections, Arrays, Listen und Maps:
Über den Index-Operator var[ i ] ist das Ansprechen von Elementen in Arrays,
Implementationen von java.util.List (ArrayList usw.)
sowie Implementationen von java.util.Map (HashMap usw.) möglich.
Dabei können die drei Collection-Arten (Array, List, Map) beliebig ineinder geschachtelt werden.
arrListMap[ 0 , 0 , 'A' ]
oder
arrListMap[ 0 ][ 0 ][ 'A' ]
oder
arrListMap[ 0 ][ 0 , 'A' ] (auch beliebig Komma oder neues Bracket-Area gemischt)
Neben Variablen-Namen können noch Sequenzen {...}, ParenthesisAreas (...) und Funktionsaufrufe mit einem Index-Operator ausgestattet wrden.
newStringArray( 'str0' , 'str1' )[ 0 ]
( newStringArray( 'str0' , 'str1' ) )[ 0 ]
{ newStringArray( 'str0' , 'str1' ) }[ 0 ]
Die Zuweisung zu indizierten Elementen von Variablen ist auch möglich:
arrListMap[ 0 , 0 , 'A' ] := 'Test1'
Die Zuweisung zu Collection-Elementen die von Ausdrücken zurückgegeben werden, die keine Variablen sind, ist noch nicht möglich.
( arrListMap )[ 0 , 0 , 'A' ] := 'Test1'
-Sequenzen:
Für Sequenzen ist ein eigener Abschnitt reserviert.
Denkbare Erweiterungen:
Referenzvariable (nicht fertig)
Instanziieren von Objekten beliebiger Klassen über Class.forName() newInstance und Methodenaufruf darauf (nicht fertig)
Sequenzen ermöglichen das Einbringen algorithmischen Verhaltens in die
Expression Library.
Die Anregung zum Einbau der Sequenz-Funktionalität stammt von Stefan Mathias Aust im
Diskussionforum de.comp.lang.java
Für Sequenzen gilt folgende Schreibweise:
{ <expression> [ ; <expression> ] }
Eine Sequenz wird in geschwungene Klammern eingeschlossen und enthält Null bis
beliebig viele Expressions, die durch Semikolon getrennt sind.
Leere Expressions, dass heisst Semikolon folgt auf Semikolon beziehungsweise
Semikolon steht am Anfang oder Ende, sind erlaubt.
Die einzelnen Expressions einer Sequenz werden nacheinander ausgeführt.
Zurückgegeben wird der Rückgabewert der letzten Expression in der Sequenz.
Alternativ kann die Sequenz auch mit der returnSequ/breakSequ-Funktion abgebrochen werden.
Dabei wird der Wert des Ausdruckes, welcher der return-Funktion übergeben wurde zurückgeliefert.
Anwendung der Sequenzen:
Sequenzen kann man als Parameter für die if/iif-Funktion und die while/iwhile- Funktion verwenden:
if( bedingung , trueExpression/Sequenz , falseExpression/Sequenz )
while( bedingung , bodyExpression/Sequenz )
repeat( bedingung , bodyExpression/Sequenz )
for( startExpression , repeatCondition , stepExpression , bodyExpression )
for( i := 0 , i <= 3 , i++ , { print( 'for:' ) ; println( i ) } )
Das else-if-Konstrukt könnte man folgendermassen aufbauen:
if( bedingung1, thenExpression/Sequenz , if( bedingung2 , elseifExpression/Sequenz , elseExpression/Sequenz ) )
breakSequence( <objectvalue> )
breakSequ ( <objectvalue> )
Bricht die aktuelle Sequenz ab und liefert den als Parameter übergebenen Wert zurück.
continueSequence()
contSequ()
Startet die aktuelle Sequenz von vorn.
Mit Hilfe der Funktionen compile und eval können Sequenzen Variablen zugewiesen und später ausgeführt werden.
Wünschenswert sind weiterhin noch lokale Variable (nicht fertig),
Referenzen auf Sequenzen (nicht fertig),
Parameter für Sequenzen (nicht fertig) und
zusätzliche Parameter für die eval-Methode (nicht fertig).
Definition: funcRecursiv := { | x | if( x > 0 , { eval( funcRecursiv , x - 1 ) } , null ) } Aufruf: eval( funcRecursiv , 5 )Konstrukte wie Function-Pointer (Funktionen höherer Ordnung) und Closures wären damit ohne weitere Massnahmen möglich.
Ausrechnen von konstanten Ausdrücken, Sub-Ausdrücken, zwischenspeichern der konstanten Werte, Löschen der konstanten Werte in tieferen Aufrufhierarchien zur Speicherplatzeinsparung
Die Einführung der Sequenzen und der mehrzeiligen Notation erfordert die Möglichkeit der Kommentare.
// Inline-Kommentare
/* ... */ Bracket-Area-Kommentare
true
false
null
apostroph quote " (siehe hierzu Strings, Problem Encoding)
highcomma highComma ' (siehe hierzu Strings, Problem Encoding)
newline newLine \n (siehe hierzu Strings, Problem Encoding)
and
or
nand
nor
xor
pi PI
euler Euler EULER Eulersche Zahl, Basis des natürlichen Logarithmus
StringLiteral
eingeschlossen in Apostrophe "..." oder Hochkomma '...',
javaübliche Esc-Sequenzen sind erlaubt: \\ \" \b \n \r \t
NumberLiteral
negatives Vorzeichen erlaubt, Dezimalpunkt am Anfang oder zwischen zwei
Ziffern, (noch) keine Exponentialdarstellung
Name beginnt mit Buchstabe, danach Buchstaben oder Zahlen, Länge maximal 30 Zeichen, Punkte (später) erlaubt (zum Beispiel: session.user)
Array-, Listen und Map-Elemente (java.util.List) werden über einen Index angesprochen (Indexoperator)
varArr[ index ]
Solche Kombinationen sind natürlich auch möglich:
varArr[ index++ ]
varArr[ ++index ]
Objekt-Attribute (Member) und Objekt-Methoden werden über den Punkt-Operator angesprochen (Dot-Operator)
Über object.member können public-Member oder die entsprechenden get- und set-Methoden lesend und schreibend angesprochen werden.
Über object.methode( Parameterliste ) können public-Methoden aufgerufen werden.
Bei Parameterwerten null oder bei Parameter-Objekten von erweiterten
Klassen kann es passieren, dass nicht die korrekte Methode aus eventuell
mehreren überladenen Methoden mit der gewünschten Signatur aufgerufen wird.
Dafür baue ich noch einen Cast-Operator ein (nicht fertig).
Alternativ können für numerische Werte (Byte, Short, Integer, Long, Float, Double) sowie
Character anstatt String die entsprechenden Konvertierungsfunktionen (nicht fertig)
benutzt werden.
Ein weiteres Problem tritt bei Elementartypen und Objekten in Methoden signaturen auf.
Zum Beispiel gibt es bei der java.util.ArrayList
remove( Object )
und
remove( int )
Beim Aufruf list.remove( 1 ) wird die jeweils zufällig zuerst gefundene Methode
aufgerufen. Hier kann man besser mit der remove-Funktion arbeiten.
Für andere Konstellationen gibt es aber keine Lösung.
Der Zugriff auf Objekt-Attribute und der Aufruf von Methoden kann im AbstractRuntimeEnvironment
abgeschaltet werden.
Sinnvoll ist noch das Unterbinden dieser Möglichkeiten im Parser (nicht fertig).
Achtung: Operator-Priorität (Vorrang, Punktrechnung vor Strichrechnung) wird beachtet.
String-Operatoren:
+ String-Verkettung
numerische Operatoren:
+
-
*
/
** ^ Potenz (Power)
% mod
Boolean-Operatoren:
and | UND-Verknüpfung mit verkürzter Auswertung (Short-Circuit-Evaluation) |
&& | UND-Verknüpfung mit verkürzter Auswertung (Short-Circuit-Evaluation) |
& | UND-Verknüpfung ohne verkürzte Auswertung |
or | ODER-Verknüpfung mit verkürzter Auswertung (Short-Circuit-Evaluation) |
|| | ODER-Verknüpfung mit verkürzter Auswertung (Short-Circuit-Evaluation) |
| | ODER-Verknüpfung ohne verkürzte Auswertung |
nand | Negiertes UND mit verkürzter Auswertung |
nor | Negiertes ODER mit verkürzter Auswertung |
xor | Exclusiv-Oder (Entweder Oder) ohne verkürzte Auswertung |
Vergleichs-Operatoren:
==
<= =<
>= =>
!= <>
Bitweise Boolean-Operatoren habe ich nicht eingebaut, da diese sehr selten benötigt werden. Prinzipiell können diese aber zur Verfügung gestellt werden.
:= einfache Zuweisung += Addititon und Zuweisung -= Subtraktion und Zuweisung *= Multiplikation und Zuweisung /= Division und Zuweisung :>= Zuweisung wenn kleiner als (so was wollte ich schon immer) :min= :<= Zuweisung wenn grösser als (so was wollte ich schon immer) :max=Statt der Zuweisung kann man auch die setVar/setVarForName-Funktion nutzen.
! logische Negation
- numerische Negation ( value * -1 )
@ Erzeugen einer Referenz (nicht fertig)
var++
++var
var--
--var
Idee:
Warum nur Incrementieren und Dekrementieren?
Man könnte sich alle möglichen Operationen Inline vorstellen.
var++2 (erhöhen um 2)
Das könnte man aber auch über Funktionen realisieren:
preSet( var , expression ) | Variable setzen und neuen Wert zurückgeben ( ++i ) |
postSet( var , expression ) | Variable setzen, aber alten Wert zurückgeben ( i++ ) |
preSetLater( var , expression ) | neuen Wert zurückgeben, ( ++i ) |
Variable nach kompletter Evaluation des Ausdrucks setzen | |
postSetLater( var , expression ) | alten Wert zurückgeben, ( i++ ) |
Variable nach kompletter Evaluation des Ausdrucks setzen |
preSetNow( var , expression ) | Variable sofort setzen und neuen Wert zurückgeben ( ++i ) |
postSetNow( var , expression ) | Variable sofort setzen, aber alten Wert zurückgeben ( i++ ) |
Alternativ ist noch die Zuweisung im Ausdruck:
3 + ( a := a + 2 )
möglich.
Das erlaubt aber keine Steuerung des Rückspeicher-Zeitpunktes.
Ausserdem kann die Zuweisung mit Hilfe der Funktionen setVar, setVariable,
setVarForName und setVariableForName nachgebildet werden.
05: Prefix- unf Postfix-Operatoren: ++ -- -(als Vorzeichen) !
04: Multiplikation Division: * / %
03: Addition Subtraktion: + - +(String-Verkettung)
02: Vergleichsoperatoren: == <= =< >= => < > != <>
01: logische Operatoren: && || & |
00: Zuweisungs-Operatoren
Höherwertige Operatoren werden vor den niederwertigen ausgeführt.
Die in der JSP Standard Tag Lib realisierte Expression Language arbeitet mit Strings in der Art
"abc${var1}def"
wobei im Bereich ${ ... } ein zur Laufzeit auszuwertender Ausdruck stehen kann.
Dem will ich mich nicht verschliessen.
Also habe ich einen entsprechenden Konverter in meine Expression Lib eingebaut:
Methode Expression.convertExpLangToExp( String )
Zum Parsen eines Strings mit Expressions der Expression Language dient die Methode:
Expression.parseExpLang( String )
Typ-Prüfung:
Anlegen einer Klasse im Package de.cnc.expression.assignmentoperators oder
de.cnc.expression.infixoperators oder
de.cnc.expression.prefixoperators analog zur Klasse
SetOperator oder
PlusOperator oder
NotPrefixOperator
Erweitern der Parser-Methode
AbstractAssignmentOperator#parseAssignmentOperator oder
AbstractInfixOperator#parseInfixOperator oder
AbstractPrefixOperator#parsePrefixOperator
Eintragen in die Prüfroutine
AbstractInfixOperator#isOperator
Auf der Grundlage des Expression Parsers ist es nun relativ
naheliegend eine Template Engine zu bauen.
Aus Mangel an eigenen Ideen benutze ich die JSP-Notation:
<% ...scriptlet code... %> <%= ...scriptlet expression... %>Übersetzt wird ein Template in Java mit der statischen Methode
public static String compileTemplate(String);oder in der Expression-Engine mit der Built-In-Function
compileTemplate(String)Beim Übersetzen und Ausführen des Templates wird die reservierte Variable CNC_TMPL_OUT vrwendet. Solange keine Sequence- lokalen Variablen realisiert sind, ist es nicht möglich, Templates rekursiv geschachtelt auszuführen.
aaa <% for( i := 0 , i <= 3 , i++ , { %>bbb <% } ) ;%>ccc ^ ^