**Plugin Manager für Polarion (ALM): Steigerung von Effizienz und Einblicken**

Von Intechcore GmbH und Garantis IT Solutions Ltd.

**Einführung**

In der schnell­le­bigen Welt der Software­ent­wicklung ist ein effizi­entes Management von Tools und Erwei­te­rungen von entschei­dender Bedeutung. Hier kommt der Plugin Manager für Polarion (ALM) ins Spiel—eine leistungs­starke Lösung, die darauf ausgelegt ist, die Verwaltung und Funktio­na­lität des Polarion Servers zu optimieren. In diesem Artikel gehen wir auf die wichtigsten Funktionen, den Techno­logie-Stack und die Vorteile dieses innova­tiven Projekts ein.

**Projekt­über­sicht**

**Projekt­ge­gen­stand**

Der Plugin Manager für Polarion (ALM) konzen­triert sich auf die Verwaltung von Erwei­te­rungen innerhalb des Polarion-Ökosystems. Ziel ist es, die Instal­lation, Überwa­chung und Wartung dieser kriti­schen Kompo­nenten zu vereinfachen.

**Projekt­dauer**

Von November bis Dezember 2023 arbeitete ein engagiertes Team von drei Personen—bestehend aus einem Product Owner und erfah­renen Entwicklern—fleißig daran, dieses Projekt zum Leben zu erwecken.

**Motivation**

Das Projekt entstand als eigen­ständige Initiative nach dem erfolg­reichen Erwei­te­rungs­projekt im Jahr 2023. In dem Bestreben, ein zentrales Verwal­tungstool zu schaffen, machte sich das Team an die Entwicklung des Plugin Managers.

**System­an­for­de­rungen und Einsatz**

Um die Vorteile des Plugin Managers nutzen zu können, sind folgende Voraus­set­zungen erforderlich:

- Polarion-Version: Kompa­tibel mit Polarion 22R2 und neuer.
– Betriebs­systeme: Unter­stützt sowohl Windows als auch Linux Plattformen.

**Techno­logie-Stack**

Der Plugin Manager nutzt einen robusten Techno­logie-Stack, der die Stärken verschie­dener Tools und Frame­works kombiniert:

- **Java:** Die Haupt­sprache für die Entwicklung des Plugin Managers. Java gewähr­leistet Zuver­läs­sigkeit, Skalier­barkeit und platt­form­über­grei­fende Kompatibilität.
- **REST API:** Der Plugin Manager kommu­ni­ziert nahtlos mit dem Polarion Server über RESTful APIs. Dies ermög­licht den Echtzeit­aus­tausch von Daten und eine effiziente Verwaltung von Erweiterungen.
- **Svelte:** Das Frontend des Plugin Managers wurde mit Svelte entwickelt—einem leichten JavaScript-Framework. Seine reaktive Natur und kompo­nen­ten­ba­sierte Archi­tektur verbessern das Benutzererlebnis.

**Haupt­funk­tionen**

1. **Sicht­barkeit instal­lierter Erweiterungen**

Das Herzstück des Plugin Managers liegt in seiner Fähigkeit, eine detail­lierte Übersicht über alle auf dem Polarion Server instal­lierten Erwei­te­rungen zu bieten. Adminis­tra­toren können Funktionen leicht nachver­folgen, die Kompa­ti­bi­lität bewerten und Erwei­te­rungen über ein zentrales Dashboard verwalten.

2. **Überwa­chung des Erweiterungsstatus**

Echtzeit-Updates sind für einen reibungs­losen Betrieb unerlässlich. Der Plugin Manager hält Adminis­tra­toren über den Betriebs­status jeder Erwei­terung auf dem Laufenden. Ob eine Erwei­terung aktiv ist oder Aufmerk­samkeit erfordert, das Dashboard bietet klare Einblicke.

3. **Umfas­sende System­in­for­ma­tionen (Server)**

Eine effiziente Server­ver­waltung beginnt mit einem Verständnis für dessen Zustand. Der Plugin Manager zeigt wichtige System­in­for­ma­tionen wie Speicher­ver­brauch, CPU-Auslastung und verfüg­baren Speicher­platz an. Adminis­tra­toren können die Server­leistung basierend auf diesen Metriken proaktiv optimieren.

4. **Detail­lierte Polarion-Systemeinblicke**

Für Polarion-spezi­fische Details bietet der Plugin Manager tiefere Einblicke. Von Konfi­gu­ra­ti­ons­spe­zi­fi­ka­tionen bis hin zur Speicher­nutzung innerhalb der Polarion-Umgebung erhalten Adminis­tra­toren wertvolle Daten. Darüber hinaus erleichtert die Überwa­chung der Proto­koll­da­tei­größen die Fehler­be­hebung und gewähr­leistet die Systemstabilität.

**Hürden und Herausforderungen**

- **Korrek­turen beim Rollen­zu­griff:** Das Team erkennt die Notwen­digkeit an, rollen­ba­sierte Zugriffs­steue­rungen zu verfeinern. Zukünftige Updates werden diesen Aspekt angehen, um Sicherheit und Benut­zer­freund­lichkeit zu verbessern.
– **Polarion-Proto­koll­da­tei­ana­ly­sator:** Während der Plugin Manager bei System­ein­blicken überzeugt, würde die Ergänzung eines Proto­koll­da­tei­ana­ly­sators Adminis­tra­toren weiter bei der Fehler­be­hebung und Optimierung unterstützen.

**Vorteile für Adminis­tra­toren und Endbenutzer**

- **Zeiter­sparnis:** Der Plugin Manager reduziert die Zeit, die für die Analyse langsamer System­leis­tungen aufge­wendet wird, erheblich. Adminis­tra­toren können Infor­ma­tionen über instal­lierte Erwei­te­rungen schnell abrufen und deren Verfüg­barkeit bewerten.
– **Sicht­barkeit und Zugäng­lichkeit:** Mit einem zentralen Dashboard erhalten Adminis­tra­toren einen umfas­senden Überblick über alle Erwei­te­rungen. Diese Trans­parenz gewähr­leistet eine effiziente Verwaltung und fundierte Entscheidungsfindung.

**Kunden­feedback**

Einer unserer geschätzten Kunden rief aus: „Was für eine erstaun­liche Erwei­terung!“ Ihre positive Reaktion bestätigt die Wirkung des Plugin Managers und motiviert uns, dessen Fähig­keiten weiter zu verfeinern und auszubauen.

**Fazit**

Der Plugin Manager für Polarion (ALM) befähigt Organi­sa­tionen, ihre Inves­tition in den Polarion Server maximal zu nutzen. Durch die Verein­fa­chung des Erwei­te­rungs­ma­nage­ments und die Bereit­stellung wichtiger System­ein­blicke trägt er zu einer effizi­en­teren und zuver­läs­si­geren ALM-Umgebung bei. Da sich Software­land­schaften weiter­ent­wi­ckeln, werden Tools wie der Plugin Manager unver­zichtbar für den Erfolg.

Dies ist der zweite Artikel in der Serie „Entwicklung unserer Program­mier­sprache in Java“. Der erste Artikel kann hier gelesen werden.

In der aktuellen Phase haben wir einen Inter­preter, der in der Lage ist, die Befehle unserer Sprache auszu­führen. Dies reicht jedoch nicht aus, wenn wir den Code auf Fehler überprüfen und diese dem Benutzer auf eine klare Weise anzeigen wollen. In diesem Artikel werden wir die Hinzu­fügung von Fehler­dia­gnosen zur Sprache betrachten. Die Durch­führung einer Fehler­analyse in Ihrer eigenen Program­mier­sprache ist ein wichtiger Schritt in der Sprach­ent­wicklung. Die Verwendung leistungs­starker Werkzeuge wie ANTLR ermög­licht es Ihnen, in kurzer Zeit recht effiziente Codeanalyse-Tools zu imple­men­tieren, die Ihnen helfen, poten­zielle Probleme in einem Programm in frühen Entwick­lungs­stadien zu erkennen, was die Software­qua­lität verbessert und die Produk­ti­vität der Entwickler steigert.

Klassi­fi­zierung von Fehlern

Es gibt verschiedene Arten von Fehlern, aber im Allge­meinen können sie in drei Kategorien unter­teilt werden: Syntax-Semantik- und Laufzeit­fehler.

Syntax­fehler treten aufgrund der Verletzung der Syntax­regeln einer bestimmten Program­mier­sprache auf. Syntax­regeln definieren, wie Anwei­sungen und Ausdrücke im Code organi­siert sein sollten.

Beispiel für einen Syntax­fehler (fehlendes abschlie­ßendes Anführungszeichen):

println("Hello, World!)

Semantische Fehler treten auf, wenn ein Programm kompi­liert und sogar ausge­führt wird, das Ergebnis jedoch anders ist als erwartet. Diese Art von Fehler ist die schwie­rigste. Seman­tische Fehler können durch das Missver­ständnis des Program­mierers bezüglich der Sprache oder der anste­henden Aufgabe verur­sacht werden. Zum Beispiel, wenn ein Program­mierer ein schlechtes Verständnis der Opera­tor­rang­folge hat, könnte er folgenden Code schreiben:

var a = 1 + 2 * 3

Er könnte erwarten, dass die Variable a gleich 9 ist, aber tatsächlich wird sie gleich 7. Dies geschieht, weil der Multi­pli­ka­ti­ons­ope­rator eine höhere Priorität als der Additi­ons­ope­rator hat. Ein seman­ti­scher Fehler kann norma­ler­weise während des Debuggens oder durch umfang­reiche Tests des Programms entdeckt werden.

Laufzeit­fehler, auch bekannt als „Excep­tions“, treten während der Programm­aus­führung auf. Solche Fehler können aufgrund falscher Daten­eingabe, dem Versuch, auf eine nicht vorhandene Datei zuzugreifen, und in vielen anderen Szenarien auftreten. Einige Laufzeit­fehler können in einem Programm gehändelt werden, aber wenn dies nicht geschieht, stürzt das Programm norma­ler­weise ab.

Neben Fehlern ist es auch wichtig, poten­zielle Probleme oder nicht offen­sicht­liche Situa­tionen zu erkennen, die im strengen Sinne keine Fehler sind, aber zu unerwünschten Konse­quenzen führen können. Zum Beispiel könnte es sich um eine ungenutzte Variable handeln, die Verwendung veral­teter Funktionen oder eine überflüssige Operation. In all diesen Fällen können dem Benutzer Warnungen angezeigt werden.

Jimple­Ba­se­Vi­sitor

Um Fehler und Warnungen zu identi­fi­zieren, benötigen wir die abstrakte Klasse JimpleBaseVisitor (erzeugt von ANTLR), die uns bereits aus dem ersten Artikel bekannt ist und standard­mäßig das JimleVisitor-Interface imple­men­tiert. Sie ermög­licht es uns, den AST-Baum (Abstract Syntax Tree) zu durch­laufen, und basierend auf der Analyse seiner Knoten wird entscheiden, ob es sich um einen Fehler, eine Warnung oder einen normalen Teil des Codes handelt. Im Wesent­lichen ist die Diagnose von Fehlern fast nicht anders als das Inter­pre­tieren von Code, außer wenn wir I/O durch­führen oder auf externe Ressourcen zugreifen müssen. Zum Beispiel, wenn ein Konso­len­aus­ga­be­befehl ausge­führt wird, dann ist es unsere Aufgabe zu überprüfen, ob der als Argument übergebene Datentyp gültig ist, ohne es direkt an die Konsole auszugeben.

Lassen Sie uns die Klasse JimpleDiagnosticTool erstellen, die JimleBaseVisitor erbt und die gesamte Logik des Findens und Speicherns von Fehlern kapselt:

class JimpleDiagnosticTool extends JimpleBaseVisitor<ValidationInfo> {
    private Set<Issue> issues = new LinkedHashSet<>();
}

record Issue(IssueType type, String message, int lineNumber, int lineOffset, String details) {}

Diese Klasse enthält eine Liste vom Typ Issue, welcher Infor­ma­tionen über einen spezi­fi­schen Fehler enthält.

Es ist bekannt, dass jede Methode einer gegebenen Klasse einen Wert eines bestimmten Typs zurück­geben muss. In unserem Fall werden wir Infor­ma­tionen über den Typ des Knotens im Baum zurück­geben – ValidationInfo. Diese Klasse enthält auch Infor­ma­tionen über den möglichen Wert, dies wird uns helfen, einige seman­tische oder Laufzeit­fehler zu identifizieren.

record ValidationInfo(ValidationType type, Object value) {}

enum ValidationType {
    /**
     * Expression returns nothing.
     */
    VOID,

    /**
     * Expression is String
     */
    STRING,

    /**
     * Expression is double
     */
    DOUBLE,

    /**
     * Expression is long
     */
    NUMBER,

    /**
     * Expression is boolean
     */
    BOOL,

    /**
     * Expression contains error and analysing in another context no makes sense.
     */
    SKIP,

    /**
     * When object can be any type. Used only in Check function definition mode.
     */
    ANY,

    /**
     * Tree part is function declaration
     */
    FUNCTION_DEFINITION
}

Sie sollten auf den Wert von ValidationType.SKIP achten. Er wird verwendet, wenn ein Fehler gefunden und bereits in einem Teil des Baums regis­triert wurde und eine weitere Analyse dieses Baumknotens nicht sinnvoll ist. Wenn zum Beispiel ein Argument in einem Summen­aus­druck einen Fehler enthält, wird das zweite Argument des Ausdrucks nicht analysiert.

ValidationInfo checkBinaryOperatorCommon(ParseTree leftExp, ParseTree rightExp, Token operator) {
    ValidationInfo left = visit(leftExp);
    if (left.isSkip()) {
        return ValidationInfo.SKIP;
    }
    ValidationInfo right = visit(rightExp);
    if (right.isSkip()) {
        return ValidationInfo.SKIP;
    }
    // code omitted
}

Listers vs Visitors

Before moving on, let’s take a look at another ANTLR-generated interface  (), which can also be used if we need traverse the AST tree. What is the diffe­rence between them? The biggest diffe­rence between these mecha­nisms is that listener methods are always called by ANTLR on a per-node basis, whereas visitor methods must bypass their child elements with explicit calls. And if the programmer does not call on child nodes, then these nodes are not visited, i.e. we have the ability to control tree traversal. For example, in our imple­men­tation, the function body is first visited once in its entirety (mode ) to detect errors in the entire function (all if and else blocks), and several times with specific argument values:

Bevor wir fortfahren, werfen wir einen Blick auf ein weiteres von ANTLR generiertes Interface, JimpleListener (pattern Observer), das ebenfalls verwendet werden kann, wenn wir den AST-Baum durch­laufen müssen. Was ist der Unter­schied zwischen ihnen? Der größte Unter­schied zwischen diesen Mecha­nismen ist, dass Listener-Methoden immer von ANTLR auf einer pro-Knoten-Basis aufge­rufen werden, während Besucher-Methoden ihre Kind-Elemente mit expli­ziten Aufrufen umgehen müssen. Und wenn der Program­mierer nicht visit() auf Kindknoten aufruft, dann werden diese Knoten nicht besucht, d.h. wir haben die Möglichkeit, die Baumdurch­querung zu steuern. In unserer Imple­men­tierung wird beispiels­weise der Funktionskörper

zunächst einmal in seiner Gesamtheit (Modus checkFuncDefinition==true) besucht, um Fehler in der gesamten Funktion (alle if- und else-Blöcke) zu erkennen, und dann mehrmals mit spezi­fi­schen Argumentwerten:

@Override
ValidationInfo visitIfStatement(IfStatementContext ctx) {
    // calc expression in "if" condition
    ValidationInfo condition = visit(ctx.expression());

    if (checkFuncDefinition) {
        visit(ctx.statement());
        // as it's just function definition check, check else statement as well
        JimpleParser.ElseStatementContext elseStatement = ctx.elseStatement();
        if (elseStatement != null) {
            visit(elseStatement);
        }
        return ValidationInfo.VOID;
    }

    // it's not check function definition, it's checking of certain function call
    if (condition.isBool() && condition.hasValue()) {
        if (condition.asBoolean()) {
            visit(ctx.statement());
        } else {
            JimpleParser.ElseStatementContext elseStatement = ctx.elseStatement();
            if (elseStatement != null) {
                visit(elseStatement);
            }
        }
    }

    return ValidationInfo.VOID;
}

Das „Visitor“ Muster eignet sich sehr gut, wenn wir für jeden Baumknoten einen bestimmten Wert zuordnen müssen. Das ist genau das, was wir brauchen.

Abfangen von syntak­ti­schen Fehlern

Um einige Syntax­fehler im Code zu finden, müssen wir die Schnitt­stelle ANTLRErrorListener imple­men­tieren. Diese Schnitt­stelle enthält vier Methoden, die (vom Parser und/oder Lexer) im Falle eines Fehlers oder undefi­nierten Verhaltens aufge­rufen werden:

interface ANTLRErrorListener {
    void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e);
    void reportAmbiguity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs);
    void reportAttemptingFullContext(Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs);
    void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs);
} 

Der Name der ersten Methode (syntaxError) spricht für sich selbst; sie wird im Falle eines Syntax­fehlers aufge­rufen. Die Imple­men­tierung ist recht einfach: Wir müssen die Fehler­infor­ma­tionen in ein Objekt des Typs Issue umwandeln und es der Liste der Fehler hinzufügen:

@Override
void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
    int offset = charPositionInLine + 1;
    issues.add(new Issue(IssueType.ERROR, msg, line, offset, makeDetails(line, offset)));
}

Die verblei­benden drei Methoden können ignoriert werden. ANTLR imple­men­tiert dieses Interface auch selbst (siehe Klasse ConsoleErrorListener) und sendet Fehler an den standard error stream (System.err). Um es und andere Standard-Handler zu deakti­vieren, müssen wir die Methode removeErrorListeners am Parser und Lexer aufrufen:

    // remove default error handlers
    lexer.removeErrorListeners();
    parser.removeErrorListeners();

Ein anderer Typ eines Syntax­fehlers basiert auf den Regeln einer bestimmten Sprache. Zum Beispiel wird in unserer Sprache eine Funktion durch ihren Namen und die Anzahl der Argumente identi­fi­ziert. Wenn der Analy­sator auf einen Funkti­ons­aufruf trifft, überprüft er, ob eine Funktion mit demselben Namen und der gleichen Anzahl an Argumenten existiert. Wenn nicht, dann wird ein Fehler ausgelöst. Um dies zu tun, müssen wir die Methode visitFunctionCall überschreiben:

@Override
ValidationInfo visitFunctionCall(FunctionCallContext ctx) {
    String funName = ctx.IDENTIFIER().getText();
    int argumentsCount = ctx.expression().size();
    var funSignature = new FunctionSignature(funName, argumentsCount, ctx.getParent());
    // find a function in the context by signature (name+number of arguments)
    var handler = context.getFunction(funSignature);

    if (handler == null) {
        addIssue(IssueType.ERROR, ctx.start, "Function with such signature not found: " + funName);
        return ValidationInfo.SKIP;
    }

    // code omitted
}

Prüfen wir die if-Konstruktion. Jimple setzt voraus, dass der Ausdruck in der if-Bedingung vom Typ boolean ist:

@Override
ValidationInfo visitIfStatement(IfStatementContext ctx) {
    // visit expression
    ValidationInfo condition = visit(ctx.expression());
    // skip if expression contains error
    if (condition.isSkip()) {
        return ValidationInfo.SKIP;
    }

    if (!condition.isBool()) {
        addIssue(IssueType.WARNING, ctx.expression().start, "The \"if\" condition must be of boolean type only. But found: " + condition.type());
    }

    // code omitted
}

Der aufmerksame Leser wird feststellen, dass wir in diesem Fall eine Warnung und keinen Fehler hinzu­gefügt haben. Dies geschieht aufgrund der Tatsache, dass unsere Sprache dynamisch ist und wir nicht immer die genauen Infor­ma­tionen über den Typ des Ausdrucks kennen.

Identi­fi­zieren seman­ti­scher Fehler

Wie bereits erwähnt, sind seman­tische Fehler schwer zu finden und können oft nur beim Debuggen oder Testen des Programms gefunden werden. Einige davon können jedoch bereits in der Kompi­lie­rungs­phase identi­fi­ziert werden. Wenn wir beispiels­weise wissen, dass die Funktion zurückgibt, können wir eine Warnung anzeigen, wenn ein Divisi­ons­aus­druck diese Funktion als Divisor verwendet. Die Division durch null wird in der Regel als seman­ti­scher Fehler betrachtet, da die Division durch null in der Mathe­matik keinen Sinn ergibt.

Ein Beispiel für die Fehler­er­kennung „Division by zero“: wird ausgelöst, wenn ein Ausdruck als Divisor verwendet wird, der immer den Wert 0 zurückgibt.

ValidationInfo checkBinaryOperatorForNumeric(ValidationInfo left, ValidationInfo right, Token operator) {
    if (operator.getType() == JimpleParser.SLASH && right.hasValue() && ((Number) right.value()).longValue() == 0) {
        // if we have value of right's part of division expression and it's zero
        addIssue(IssueType.WARNING, operator, "Division by zero");
    }

    // code omitted
}

Laufzeit­fehler

Laufzeit­fehler sind ebenfalls schwer oder sogar unmöglich in der Kompi­lie­rungs-/Inter­pre­tie­rungs­phase zu erkennen. Dennoch können einige solcher Fehler identi­fi­ziert werden. Zum Beispiel, wenn eine Funktion sich selbst aufruft (entweder direkt oder durch eine andere Funktion), kann dies zu einem Stapel­überlauf-Fehler (Stack­Overflow) führen. Das Erste, was wir tun müssen, ist eine Liste (Set) zu dekla­rieren, in der wir die Funktionen speichern, die gerade aufge­rufen werden. Die Überprüfung selbst kann (und sollte) in der Methode handle­Fun­c­In­ternal zur Verar­beitung des Funkti­ons­aufrufs platziert werden. Zu Beginn dieser Methode überprüfen wir, ob der aktuelle  Function­De­fi­ni­tionContext (Funkti­ons­de­kla­ra­ti­ons­kontext) auf der Liste der bereits aufge­ru­fenen Funktionen steht, und falls dies der Fall ist, proto­kol­lieren wir eine Warnung und unter­brechen die weitere Verar­beitung der Funktion. Wenn nicht, dann fügen wir den aktuellen Kontext zu unserer Liste hinzu, und der Rest der Logik folgt. Beim Verlassen von handle­Fun­c­In­ternal muss der aktuelle Funkti­ons­kontext aus der Liste entfernt werden. Hier sollte angemerkt werden, dass wir in diesem Fall nicht nur einen poten­zi­ellen Stack­Overflow identi­fi­ziert haben, sondern auch denselben Fehler beim Diagnos­ti­zieren von Fehlern vermieden haben, nämlich beim Schleifen der Methode handleFuncInternal.

Set<FunctionDefinitionContext> calledFuncs = new HashSet<>();

ValidationInfo handleFuncInternal(List<String> parameters, List<Object> arguments, FunctionDefinitionContext ctx) {
    if (calledFuncs.contains(ctx)) {
        addIssue(IssueType.WARNING, ctx.name, String.format("Recursive call of function '%s' can lead to StackOverflow", ctx.name.getText()));
        return ValidationInfo.SKIP;
    }
    calledFuncs.add(ctx);
    
    // other checkings

    calledFuncs.remove(ctx);

    // return resulting ValidationInfo
}

Kontroll-/Daten­fluss­analyse

Für eine tiefere Unter­su­chung des Programm­codes, Optimierung und Identi­fi­zierung komplexer Fehler werden auch die Kontroll­fluss­analyse und Daten­fluss­analyse verwendet.

Die Kontroll­fluss­analyse konzen­triert sich darauf zu verstehen, welche Teile eines Programms in Abhän­gigkeit von verschie­denen Bedin­gungen und Steuer­struk­turen wie bedingten (if-else) Anwei­sungen, Schleifen und Verzwei­gungen ausge­führt werden. Sie ermög­licht es, die Ausfüh­rungs­pfade des Programms zu identi­fi­zieren und poten­zielle Fehler zu erkennen, die mit einer fehler­haften Steuer­logik zusam­men­hängen. Zum Beispiel unerreich­barer Code oder poten­zielle Programmaufhänger.

Die Daten­fluss­analyse hingegen konzen­triert sich darauf, wie Daten innerhalb eines Programms verteilt und verwendet werden. Sie hilft, poten­zielle Daten­pro­bleme wie die Verwendung von nicht initia­li­sierten Variablen, Daten­ab­hän­gig­keiten und mögliche Speicher­lecks („memory leaks“) zu identi­fi­zieren. Die Daten­fluss­analyse kann auch Fehler erkennen, die mit falschen Daten­ope­ra­tionen zusam­men­hängen, wie die Verwendung falscher Typen oder inkor­rekter (überflüs­siger) Berechnungen.

Zusam­men­fassung

In diesem Artikel haben wir den Prozess des Hinzu­fügens von Fehler- und Warndia­gnosen zu Ihrer Program­mier­sprache unter­sucht. Wir haben gelernt, was ANTLR von Haus aus für die Proto­kol­lierung von Syntax­fehlern bietet. Außerdem haben wir eine Behandlung einiger Fehler und poten­zi­eller Probleme während der Programm­aus­führung implementiert.

Der gesamte Quellcode des Inter­preters kann einge­sehen werden unter link.

Dies ist der erste Artikel in der Reihe „Entwicklung einer eigenen Java-Program­mier­sprache“, in dem der gesamte Weg der Erstellung einer Program­mier­sprache sowie der Erstellung und Pflege von Werkzeugen für diese Sprache am Beispiel der Entwicklung einer einfachen Sprache aufge­zeigt wird. Am Ende dieses Artikels werden wir einen Inter­preter imple­men­tieren, der zur Ausführung von Programmen in unserer Sprache verwendet werden kann.

Jede Program­mier­sprache hat eine Syntax, die in eine Daten­struktur umgewandelt werden muss, die für die Validierung, Trans­for­mation und Ausführung geeignet ist. In der Regel handelt es sich bei einer solchen Daten­struktur um einen abstrakten Syntaxbaum (AST). Jeder Knoten des Baums steht für ein Konstrukt, das im Quellcode vorkommt. Der Quellcode wird von einem Parser geparst und die Ausgabe ist ein AST.

Sprachen werden schon seit langem entwickelt, so dass wir heute über eine Reihe ausge­reifter Werkzeuge verfügen, darunter auch Parser­ge­ne­ra­toren. Parser­ge­ne­ra­toren nehmen als Eingabe eine Beschreibung der Grammatik einer bestimmten Sprache, und die Ausgabe besteht aus Parsern, Inter­pretern und Compilern.

In diesem Artikel wird das Werkzeug ANTLR betrachtet. ANTLR ist ein Dienst­pro­gramm, das als Eingabe eine Grammatik in Form von RBNFs erhält und Schnittstellen/Klassen (in unserem Fall ist es Java-Code) für das Parsen von Programmen ausgibt. Die Liste der Sprachen, für die Parser generiert werden, finden Sie hier.

Beispiel­gram­matik

Bevor wir uns der eigent­lichen Grammatik zuwenden, wollen wir versuchen, einige der Regeln einer typischen Program­mier­sprache in Worte zu fassen:

  • VARIABLE – ist ein IDENTIFIER
  • DIGITAL – ist eines der Zeichen 0 1 1 2 3 4 5 6 7 8 9
  • NUMBER – ist ein oder mehrere Elemente vom Typ DIGITAL.
  • EXPRESSION – ist eine NUMBER
  • EXPRESSION – ist eine VARIABLE
  • EXPRESSION – ist ein EXPRESSION ‚+‘ EXPRESSION
  • EXPRESSION – ist ‚(‚EXPRESSION‘)‘)

Wie Sie aus dieser Liste ersehen können, ist eine Sprach­gram­matik eine Menge von Regeln, die rekursive Verknüp­fungen haben können. Jede Regel kann sich auf sich selbst oder auf eine andere Regel beziehen. ANTLR hat in seinem Arsenal viele Opera­toren, um solche Regeln zu beschreiben.

:    Regelstartbezeichnung
; Ende der Regelmarke
| Alternativer Betreiber
.. Bereichsoperator
~ Verleugnung
. Irgendein Charakter
= Aufgabe
(...)  Unterregel
(...)* Unterregel 0 Mal oder öfter wiederholen
(...)+ Unterregel mindestens einmal wiederholen
(...)? Unterregel fehlt möglicherweise
{...}  Ssemantische Aktionen (in der Sprache, die als Ausgabe verwendet wird – zum Beispiel Java)
[...] Regelparameter

Beispiele für Regeln in ANTLR

Das folgende Beispiel beschreibt die Regeln für ganze Zahlen und Fließkommazahlen:

NUMBER : [0-9]+ ;
FLOAT  : NUMBER '.' NUMBER ;

Es ist sehr wichtig zu wissen, dass die Grammatik nur die Syntax der Sprache beschreibt, aus der der Parser generiert wird. Der Parser wird einen AST erzeugen, der zur Imple­men­tierung der Semantik der Sprache verwendet werden kann. Im vorigen Beispiel haben wir eine Regel zum Parsen einer ganzen Zahl definiert, aber wir haben nicht beschrieben, wie viel Speicher­platz die Zahl belegt (8 Bit, 16, …), ob die Zahl vorzei­chen­be­haftet oder vorzei­chenlos ist. In einigen Program­mier­sprachen kann man zum Beispiel eine Variable verwenden, ohne sie zu dekla­rieren. Es ist auch möglich, den Typ einer Variablen nicht zu dekla­rieren; in diesem Fall wird der Typ zur Laufzeit automa­tisch bestimmt. Alle diese Regeln der Sprach­se­mantik werden nicht in der Grammatik beschrieben, sondern sind in einem anderen Teil der Sprache implementiert.

ANTLR-Lexeme und ‑Ausdrücke

Die ANTLR-Grammatik besteht aus zwei Arten von Regeln: Lexeme und Ausdrücke, die dazu dienen, die Struktur der Grammatik zu definieren und die Einga­be­daten zu parsen.

Lexeme (oder Token) sind Regeln, die einzelne lexika­lische Elemente der Einga­be­sprache definieren, wie z. B. Zahlen, Bezeichner, Opera­ti­ons­zeichen usw. Jedes Lexem entspricht einem bestimmten Typ von Token, der vom Parser für die weitere Verar­beitung verwendet wird. Der lexika­lische Analy­sator scannt den Einga­betext, zerlegt ihn in Token und erstellt eine Folge von Token, die dann an den Parser weiter­ge­geben werden. Token werden in Großbuch­staben geschrieben (z. B. NUMBER, IDENTIFIER).

Ausdrücke sind Regeln, die die Struktur der Grammatik der Einga­be­sprache definieren. Sie beschreiben, wie Token zuein­ander in Beziehung stehen und wie sie zu komple­xeren Konstrukten kombi­niert werden können. Ausdrücke können sowohl Verweise auf Token als auch auf andere Ausdrücke enthalten. Sie werden in camelCase-Schreib­weise geschrieben (zum Beispiel: expression, functionDefinition).

Der Unter­schied zwischen Token und Ausdrücken in ANTLR besteht also darin, dass Token die einzelnen lexika­li­schen Elemente der Einga­be­sprache definieren und sie in Token umwandeln, während Ausdrücke die Struktur der Grammatik definieren und beschreiben, wie Token zu komple­xeren Konstrukten verknüpft werden.

Sprach­liche Anforderungen

Bevor wir mit der Imple­men­tierung einer Sprache beginnen, müssen wir entscheiden, welche Funktionen sie unter­stützen soll. Für unsere Aufgabe, die wir zu Ausbil­dungs­zwecken durch­führen, werden wir eine einfache Grammatik verwenden. Die Sprache wird die folgenden Konstrukte unterstützen:

  • Variablen (Typen String, Long, Double);
  • Zuwei­sungs­ope­rator (=);
  • Arith­me­tische Operationen (+, -, *, /);
  • Vergleichs­ope­ra­toren (>, <, >=, <=, ==, !=);
  • Bedin­gungs­ope­ra­toren (if, else);
  • Funktionen;
  • Auf der Konsole drucken (integrierte println-Anweisung).

Grammatik

Schließlich eine vollständige Beschreibung der Grammatik der Sprache:

grammar Jimple;

// Grundregel der Grammatik
program: (statement)* EOF;

// Liste möglicher Aussagen
statement: variableDeclaration
| assignment
| functionDefinition
| functionCall
| println
| return
| ifStatement
| blockStatement
;

// Liste möglicher Ausdrücke
expression: '(' expression ')' #parenthesisExpr
| left=expression op=(ASTERISK | SLASH) right=expression #mulDivExpr
| left=expression op=(PLUS | MINUS) right=expression #plusMinusExpr
| left=expression compOperator right=expression #compExpr
| IDENTIFIER #idExp
| NUMBER #numExpr
| DOUBLE_NUMBER #doubleExpr
| STRING_LITERAL #stringExpr
| functionCall #funcCallExpr
;

// Beschreibungen einzelner Ausdrücke und Aussagen
variableDeclaration: 'var' IDENTIFIER '=' expression ;
assignment: IDENTIFIER '=' expression ;
compOperator: op=(LESS | LESS_OR_EQUAL | EQUAL | NOT_EQUAL | GREATER | GREATER_OR_EQUAL) ;
println: 'println' expression ;
return: 'return' expression ;
blockStatement: '{' (statement)* '}' ;
functionCall: IDENTIFIER '(' (expression (',' expression)*)? ')' ;
functionDefinition: 'fun' name=IDENTIFIER '(' (IDENTIFIER (',' IDENTIFIER)*)? ')' '{' (statement)* '}' ;
ifStatement: 'if' '(' expression ')' statement elseStatement? ;
elseStatement: 'else' statement ;

// Liste der Token
IDENTIFIER : [a-zA-Z_] [a-zA-Z_0-9]* ;
NUMBER : [0-9]+ ;
DOUBLE_NUMBER : NUMBER '.' NUMBER ;
STRING_LITERAL : '"' (~["])* '"' ;
ASTERISK : '*' ;
SLASH : '/' ;
PLUS : '+' ;
MINUS : '-' ;
ASSIGN : '=' ;
EQUAL : '==' ;
NOT_EQUAL : '!=' ;
LESS : '<' ;
LESS_OR_EQUAL : '<=' ;
GREATER : '>' ;
GREATER_OR_EQUAL : '>=' ;
SPACE : [ \r\n\t]+ -> skip;
LINE_COMMENT : '//' ~[\n\r]* -> skip;

Wie Sie vielleicht schon erraten haben, heißt unsere Sprache Jimple (abgeleitet von Jvm Simple). Vielleicht lohnt es sich, einige Punkte zu erläutern, die beim ersten Kennen­lernen von ANTLR vielleicht nicht offen­sichtlich sind.

Labels

Bei der Beschreibung der Regeln einiger Opera­tionen wurde das Label op verwendet, so dass wir dieses Label als Namen der Variablen verwenden können, die den Wert des Operators enthält. Im Prinzip könnten wir auf die Angabe von Labels verzichten, aber in diesem Fall müssen wir zusätz­lichen Code schreiben, um den Wert des Operators aus dem Parse-Baum zu erhalten.

compOperator: op=(LESS | LESS_OR_EQUAL | EQUAL | NOT_EQUAL | GREATER | GREATER_OR_EQUAL) ;

Benannte Regel­al­ter­na­tiven

Wenn in ANTLR eine Regel mit mehreren Alter­na­tiven definiert wird, kann jeder von ihnen ein Name gegeben werden und wird dann ein separater Verar­bei­tungs­knoten im Baum sein. Dies ist sehr praktisch, wenn es notwendig ist, die Verar­beitung jeder Regel­al­ter­native in eine separate Methode zu legen. Es ist wichtig, dass entweder alle Alter­na­tiven oder keine von ihnen einen Namen erhalten. Das folgende Beispiel zeigt, wie das aussehen kann:

expression: '(' expression ')' #parenthesisExpr
 | IDENTIFIER #idExp
 | NUMBER #numExpr

ANTLR erzeugt den folgenden Code:

public interface JimpleVisitor<T> {
    T visitParenthesisExpr(ParenthesisExprContext ctx);
    T visitIdExp(IdExpContext ctx);
    T visitNumExpr(NumExprContext ctx);
}

Kanäle

In ANTLR gibt es ein solches Konstrukt als Kanal (channel). Norma­ler­weise werden Kanäle verwendet, um mit Kommen­taren zu arbeiten, aber da wir in den meisten Fällen nicht prüfen müssen, ob es Kommentare gibt, sollten sie mit -> skip verworfen werden, was wir auch verwendet haben. Es gibt jedoch Fälle, in denen wir die Bedeutung von Kommen­taren oder anderen Konstrukten inter­pre­tieren müssen, dann verwenden Sie Kanäle. ANTLR hat bereits einen einge­bauten Kanal namens HIDDEN, den Sie verwenden können, oder Sie dekla­rieren Ihre eigenen Kanäle für bestimmte Zwecke. Auf diese Kanäle können Sie beim Parsen des Codes weiter zugreifen.

Beispiel für die Ankün­digung und Nutzung eines Kanals

channels { MYLINECOMMENT }
LINE_COMMENT : '//' ~[rn]+ -> channel(MYLINECOMMENT) ;

Fragmente

Zusätzlich zu den Token gibt es in ANTLR ein Konzept, das als Fragment (fragment) bezeichnet wird. Regeln mit dem Präfix fragment können nur von anderen Regeln im Lexer aufge­rufen werden. Sie sind selbst keine Token. Im folgenden Beispiel haben wir die Defini­tionen von Zahlen für verschiedene Zahlen­systeme in Fragmenten zusammengefasst.

NUMBER: DIGITS | OCTAL_DIGITS | HEX_DIGITS;
fragment DIGITS: '1'..'9' '0'..'9'*;
fragment OCTAL_DIGITS: '0' '0'..'7'+;
fragment HEX_DIGITS: '0x' ('0'..'9' | 'a'..'f' | 'A'..'F')+;

So wird eine Zahl in einem belie­bigen Zahlen­system (zum Beispiel «123», «0762» oder «0xac1») als NUMBER-Token und nicht als DIGITS, OCTAL_DIGITS oder HEX_DIGITS behandelt. Fragmente werden in Jimple nicht verwendet.

Instru­mente

Bevor wir mit der Generierung des Parsers beginnen, müssen wir Werkzeuge für die Arbeit mit ANTLR einrichten. Wie wir wissen, ist ein gutes und bequemes Werkzeug die Hälfte des Erfolgs. Zu diesem Zweck müssen wir die ANTLR-Bibliothek herun­ter­laden und Skripte schreiben, um sie auszu­führen. Es gibt auch Maven/Gradle/IntelliJ IDEA Plugins, die wir in diesem Artikel nicht verwenden werden, die aber für eine produktive Entwicklung nützlich sein können.

Wir benötigen die folgenden Skripte:

Skript antlr4.sh

java -Xmx500M -cp ".:/usr/lib/antlr-4.12.0-complete.jar" org.antlr.v4.Tool $@

Skript grun.sh

java -Xmx500M -cp ".:/usr/lib/antlr-4.12.0-complete.jar" org.antlr.v4.gui.TestRig $@

 

Parser-Generierung

Speichern Sie die Grammatik in der Datei Jimple.g4. Führen Sie dann das Skript wie folgt aus:

antlr4.sh Jimple.g4 -package org.jimple.lang -visitor

Mit dem Parameter ‑package können Sie das Java-package angeben, in dem der Code generiert werden soll. Mit dem Parameter ‑visitor können Sie die Schnitt­stelle Jimple­Vi­sitor generieren, die das Visitor-Muster imple­men­tiert.

Nach erfolg­reicher Ausführung des Skripts erscheinen mehrere Dateien im aktuellen Verzeichnis: JimpleParser.java, JimpleLexer.java, JimpleListener.java, JimpleVisitor.java.

Die ersten beiden Dateien enthalten den generierten Parser- bzw. Lexer-Code. Die beiden anderen Dateien enthalten die Schnitt­stellen für die Arbeit mit dem Parse-Baum. In diesem Artikel werden wir die Jimple­Vi­sitor-Schnitt­stelle verwenden, genauer gesagt ist Jimple­Ba­se­Vi­sitor  — ebenfalls eine generierte Klasse, die die Jimple­Vi­sitor-Schnitt­stelle imple­men­tiert und Imple­men­tie­rungen aller Methoden enthält. So können wir nur die Methoden überschreiben, die wir benötigen.

Imple­men­tierung des Interpreters

Schließlich kommen wir zum inter­es­san­testen Teil — der Imple­men­tierung des Inter­preters. Obwohl wir uns in diesem Artikel nicht mit der Überprüfung des Codes auf Fehler befassen werden, werden wir dennoch Inter­pre­ta­ti­ons­fehler imple­men­tieren. Als erstes erstellen wir eine Klasse Jimple­Inter­preter  mit der eval-Methode, deren Input ein String mit Jimple-Code sein wird. Als nächstes müssen wir den Quellcode mit JimpleLexer in Token zerlegen und dann mit Jimple­Parser einen AST-Baum erstellen.

public class JimpleInterpreter {
    public Object eval(final String input) {
        // Parsen des Quellcodes in Token
        final JimpleLexer lexer = new JimpleLexer(CharStreams.fromString(input));
        // Erstellen Sie einen AST-Baum
        final JimpleParser parser = new JimpleParser(new CommonTokenStream(lexer));
        // Erstellen Sie ein Objekt der Klasse JimpleInterpreterVisitor
        final JimpleInterpreterVisitor interpreterVisitor = new JimpleInterpreterVisitor(new JimpleContextImpl(stdout));
        // Starten Sie den Interpreter
        return interpreterVisitor.visitProgram(parser.program());
    }
}

Wir haben einen Syntaxbaum. Fügen wir nun mit Hilfe der von uns geschrie­benen Klasse Jimple­Inter­pre­ter­Vi­sitor, die den AST durch Aufruf der entspre­chenden Methoden durch­läuft, einige Seman­tiken hinzu. Da die Wurzel­regel unserer Grammatik die program Regel ist (siehe oben program: (statement)* EOF), beginnt die Baumdurch­querung dort. Dazu rufen wir die Standard­me­thode visit­Program des Jimple­Inter­pre­ter­Vi­sitor-Objekts auf, mit einem Objekt der Klasse Program­Context als Eingabe. Die ANTLR-Imple­men­tierung besteht aus dem Aufruf der visitChildren(RuleNode node-Methode, die alle Kinder eines bestimmten Baumknotens durch­läuft und für jedes von ihnen die visit-Methode aufruft.

// Von ANTLR generierter Code
public class JimpleBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements JimpleVisitor<T> {
    @Override
    public T visitProgram(JimpleParser.ProgramContext ctx) {
        return visitChildren(ctx);
    }

    // Andere Methoden werden der Kürze halber weggelassen
}

Wie Sie sehen, ist Jimple­Ba­se­Vi­sitor eine generische Klasse, für die Sie den Verar­bei­tungstyp für jeden Knoten definieren müssen. In unserem Fall ist dies die Object-Klasse, da Ausdrücke Werte unter­schied­lichen Typs zurück­geben können. Norma­ler­weise muss ein Ausdruck einen Wert zurück­geben, eine Anweisung gibt jedoch nichts zurück. Das ist ihr Unter­schied. Bei Geneh­migung können wir null zurück­geben. Um jedoch nicht verse­hentlich auf eine NullPoin­ter­Ex­ception zu stoßen, geben wir anstelle von null ein Objekt vom Typ Object zurück, das global in der Jimple­Inter­preter-Klasse definiert ist:

Wie Sie sehen können, Jimple­Ba­se­Vi­sitor — ist eine generische Klasse, für die wir die Art der Verar­beitung für jeden Knoten definieren müssen. In unserem Fall ist dies die Klasse Object, da Ausdrücke Werte unter­schied­lichen Typs zurück­geben können. Norma­ler­weise sollte ein Ausdruck (expression) einen Wert zurück­geben, während eine Anweisung ((statement) nichts zurückgibt. Das ist der Unter­schied zwischen ihnen. Im Falle einer Anweisung können wir null zurück­geben. Um jedoch eine NullPoin­ter­Ex­ception zu vermeiden, geben wir anstelle von null ein Objekt vom Typ Object zurück, das global in der Klasse Jimple­Inter­preter definiert ist:

public static final Object VOID = new Object();

Die Klasse Jimple­Inter­pre­ter­Vi­sitor erweitert die Klasse Jimple­Ba­se­Vi­sitor und überschreibt nur die Methoden, an denen wir inter­es­siert sind. Schauen wir uns die Imple­men­tierung des einge­bauten println-Operators an, der in der Grammatik als println: ‚println‘ expression; beschrieben wird. Als erstes müssen wir den Ausdruck expression berechnen, dazu müssen wir die visit-Methode aufrufen und ihr das expression-Objekt aus dem aktuellen Println­Context übergeben. In der visit­Println-Methode inter­es­sieren wir uns überhaupt nicht dafür, wie der Ausdruck ausge­wertet wird, die entspre­chende Methode ist für die Auswertung jeder Regel (Kontext) zuständig. Die Methode visitStringExpr.rpreter wird zum Beispiel verwendet, um ein String-Literal auszuwerten:

public class JimpleInterpreterVisitor extends JimpleBaseVisitor<Object> {
    @Override
    public Object visitPrintln(final JimpleParser.PrintlnContext ctx) {
        final Object result = visit(ctx.expression());
        System.out.println(result);
        return null;
    }
    
    @Override
    public Object visitStringExpr(final JimpleParser.StringExprContext ctx) {
        // Gibt ein String-Literal zurück
        return cleanStringLiteral(ctx.STRING_LITERAL().getText());
    }


    private String cleanStringLiteral(final String literal) {
        // Anführungszeichen aus der Zeichenfolge entfernen
        return literal.length() > 1 ? literal.substring(1, literal.length() - 1) : literal;
    }

    // Andere Methoden werden der Kürze halber weggelassen
}

Indem nur diese Methoden imple­men­tiert werden, unter­stützt der Inter­preter bereits println und String­li­terale, so dass wir den Code println „Hello, Jimple!“ ausführen können.

Starten des Interpreters

Um den Inter­preter zu starten, müssen wir eine Standard-Main-Methode erstellen, die nach kleinen Prüfungen unter Verwendung der Klasse Jimple­Inter­preter unseren Code ausführt:

public class MainApp {
    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("usage: jimple input.jimple");
            System.exit(1);
        }
        
        Path path = Paths.get(args[0]);
        if (!Files.exists(path)) {
            System.err.println("File not found: " + path);
            System.exit(1);
        }
        
        new JimpleInterpreter().eval(path);
    }
}

Einzel­heiten der Durchführung

Es ist nicht nötig, den gesamten Code der Inter­preter-Imple­men­tierung wieder­zu­geben, der Link zu den Quellen befindet sich am Ende des Artikels. Ich möchte jedoch auf einige inter­es­sante Details eingehen.

Wie bereits erwähnt, basiert der Inter­preter auf dem Visitor-Muster, welches die Knoten des AST-Baums besucht und die entspre­chenden Anwei­sungen ausführt. Während der Codeaus­führung erscheinen neue Bezeichner (Variablen- und/oder Funkti­ons­namen) im aktuellen Kontext, die irgendwo gespei­chert werden müssen. Zu diesem Zweck schreiben wir eine Klasse Jimple­Context, die nicht nur diese Bezeichner, sondern auch den aktuellen Ausfüh­rungs­kontext von verschach­telten Codeblöcken und Funktionen speichert, da eine lokale Variable und/oder ein Funkti­ons­pa­ra­meter nach dem Verlassen ihres Geltungs­be­reichs gelöscht werden muss.

@Override
public Object handleFunc(FunctionSignature func, List<String> parameters, List<Object> arguments, FunctionDefinitionContext ctx) {
    Map<String, Object> variables = new HashMap<>(parameters.size());
    for (int i = 0; i < parameters.size(); i++) {
        variables.put(parameters.get(i), arguments.get(i));
    }
    // Erstellen Sie einen neuen Funktionsparameterbereich und verschieben Sie ihn auf den Stapel
    context.pushCallScope(variables);
    // Ausführungsfunktionsausdrücke wurden der Kürze halber weggelassen
    // Entfernen Sie den Umfang der Funktionsparameter vom Stapel
    context.popCallScope();
    return functionResult;
}

In unserer Sprache speichert eine Variable den Wert eines Typs, der zur Laufzeit definiert wird. In den folgenden Anwei­sungen kann dieser Typ dann geändert werden. Im Grunde genommen haben wir eine Sprache mit dynami­scher Typisierung. Aller­dings gibt es immer noch eine Typüber­prüfung in Fällen, in denen es sinnlos ist, eine Operation auszu­führen. Zum Beispiel kann eine Zahl nicht durch eine Zeichen­kette dividiert werden.

Warum brauchen Sie zwei Durchgänge?

Die ursprüng­liche Version des Inter­preters sah vor, für jede Regel eine Methode zu imple­men­tieren. Findet beispiels­weise die Methode zur Funkti­ons­de­kla­ration eine Funktion mit diesem Namen (und der Anzahl der Parameter) im aktuellen Kontext, wird eine Ausnahme ausgelöst, andern­falls wird die Funktion dem aktuellen Kontext hinzu­gefügt. Die Methode des Funkti­ons­aufrufs funktio­niert auf die gleiche Weise. Wenn keine Funktion gefunden wird, wird eine Ausnahme ausgelöst, andern­falls wird die Funktion aufge­rufen. Dieser Ansatz funktio­niert, aber er erlaubt es nicht, eine Funktion aufzu­rufen, bevor sie definiert ist. Der folgende Code funktio­niert zum Beispiel nicht:

var result = 9 + 10

println "Result is " + add(result, 34)

fun add (a, b){
    return a + b
}

In diesem Fall gibt es zwei Ansätze. Der erste besteht darin, dass eine Funktion vor ihrer Verwendung definiert werden muss (was für Sprach­be­nutzer nicht sehr praktisch ist). Die zweite besteht darin, zwei Durch­gänge durch­zu­führen. Der erste Durchlauf wird benötigt, um alle Funktionen zu finden, die im Code definiert wurden. Der zweite Durchgang dient der direkten Ausführung des Codes. In meiner Imple­men­tierung habe ich den zweiten Ansatz gewählt. Man sollte die Imple­men­tierung der visit­Func­tion­De­fi­nition Methode in eine eigene Klasse verschieben, die die uns bereits bekannte generierte Klasse JimpleBaseVisitor<T> erweitert.

// Die Klasse findet alle Funktionen im Code und registriert sie im Kontext
public class FunctionDefinitionVisitor extends JimpleBaseVisitor<Object> {
    private final JimpleContext context;
    private final FunctionCallHandler handler;

    public FunctionDefinitionVisitor(final JimpleContext context, final FunctionCallHandler handler) {
        this.context = context;
        this.handler = handler;
    }

    @Override
    public Object visitFunctionDefinition(final JimpleParser.FunctionDefinitionContext ctx) {
        final String name = ctx.name.getText();
        final List<String> parameters = ctx.IDENTIFIER().stream().skip(1).map(ParseTree::getText).toList();
        final var funcSig = new FunctionSignature(name, parameters.size());
        context.registerFunction(funcSig, (func, args) -> handler.handleFunc(func, parameters, args, ctx));
        return VOID;
    }
}

Jetzt haben wir eine Klasse, die wir verwenden können, bevor wir die Inter­pre­ter­klasse direkt starten. Sie füllt unseren Kontext mit den Defini­tionen aller Funktionen, die wir in der Inter­pre­ter­klasse aufrufen werden.

Wie sieht AST aus?

Um AST zu visua­li­sieren, müssen wir das Dienst­pro­gramm grun verwenden (siehe oben). Starten Sie dazu grun mit den Parametern Jimple program ‑gui (der erste Parameter ist der Name der Grammatik, der zweite Parameter ist der Name der Regel). Dadurch wird ein Fenster mit dem AST-Baum geöffnet. Bevor Sie dieses Dienst­pro­gramm ausführen, müssen Sie den mit ANTLR erzeugten Code kompilieren.

# Parser generieren
antlr4.sh Jimple.g4

# Kompilieren Sie den generierten Code
javac -cp ".:/usr/lib/jvm/antlr-4.12.0-complete.jar" Jimple*.java

# Führen Sie das grun aus
grun.sh Jimple program -gui

# Geben Sie den Code ein: „println „Hallo, Jimple!““.
# Drücken Sie Strg+D (Linux) oder Strg+Z (Windows)

Für den Jimple-Code println „Hello, Jimple!“ wird der folgende AST erzeugt:

Zusam­men­fassung

In diesem Artikel haben Sie sich mit Konzepten wie lexika­li­schen und syntak­ti­schen Analy­sa­toren vertraut gemacht. Sie haben das ANTLR-Tool verwendet, um solche Parser zu erzeugen. Sie haben gelernt, wie man eine ANTLR-Grammatik schreibt. Schließlich waren wir in der Lage, eine einfache Sprache zu erstellen, d. h. wir haben einen Inter­preter für sie entwickelt. Als Bonus konnten wir die AST visualisieren.

Der gesamte Quellcode des Inter­preters kann hier einge­sehen werden.

Referenzen

Teilnehmer

web.sta – web basierte Appli­kation, Ziel-Webbrowser ist Internet Explorer der Version 9 und 11

TextSystem – eine eigen­ständige Desktop-Anwendung, basierend auf Java/Swing

Inter­me­dia­te­Layer – eine Client-Server Anwendung, die web.sta und TextSystem mitein­ander verbindet.

web.sta kann TextSystem über den Inter­me­dia­te­Layer starten. Inter­me­dia­te­Layer erstellt zu diesem Zweck einen HTTP-Server und web.sta sendet einfache GET- oder POST-Abfragen an den localhost. Inter­me­dia­te­Layer verar­beitet die Eingangs­pa­ra­meter und startet das TextSystem. Da keine Rückmeldung erfor­derlich ist, ist das Inter­ak­ti­ons­schema einfach und robust genug.

Citrix

In einer Citrix-Umgebung ist es nicht möglich, mehrere Listening-Sockets für dieselbe IP-Adresse und denselben Port zu erstellen. Man muss entweder den Port oder die IP-Adresse ändern. Citrix bietet jedoch einen spezi­ellen Mecha­nismus an, um diese Einschränkung zu umgehen, ohne den Algorithmus des Programms zu ändern. Dieser Mecha­nismus wird als „Virtual IP Loopback“ bezeichnet. Hier muss der Adminis­trator einfach die erfor­der­lichen Anwen­dungen im Citrix-Konfi­gu­ra­ti­ons­fenster konfi­gu­rieren. Die Anwendung, die „localhost“ für Socket-Verbin­dungen verwendet, erhält nicht 127.0.0.1, sondern eine IP-Adresse in der Form 127.0.0.<SID + 1>, wobei SID die Sitzungs-ID des Windows-Benutzers ist.

Die Proble­matik

Unter IE9 (und auch mit anderen Browsern) unter Windows Server 2008 R2 funktio­nierte dies alles einwandfrei. Dann jedoch wollte der Kunde etwas Neues und Windows Server 2012 R2 erschien mit IE11. Das ganze System hörte auf zu funktio­nieren. Unabhängig von den Citrix Einstel­lungen versucht IE11 bei Abfragen auf „localhost“ immer eine Verbindung zu 127.0.0.1 herzu­stellen, diese natürlich scheitert, weil auf der Adresse keiner Hörer (Listener) gibt. Nach ein wenig Recherche kamen wir zu dem Schluss, dass dies ein Bug in IE11 sein muss.

Routing­Service

Wenn die Virtua­li­sierung des „localhost“ in Citrix-Box für IE11 nicht funktio­niert, schreiben wir sie selbst!
Deshalb entschlossen wir uns, selbst einen Windows-Dienst zu erstellen, der ein einfacher Webserver ist und zu 127.0.0.1 eine Verbindung aufbaut und damit alle Abfragen auf der Grundlage der Sitzungs­nummer des Benutzers an den gewünschten Inter­me­dia­te­Layer umleiten. Wir haben keine einfache Lösung gefunden, um die SID zu ermitteln, aber in den Umgebungs­va­riablen haben wir sofort SESSI­ONNAME gefunden. Im Internet Explorer bekommen wir über ActiveX diese Umgebungs­va­riable und übergeben sie als Parameter in der HTTP-Abfrage weiter. Im Routing­Service erhalten wir durch den Sitzungs­namen unter wtsapi32.lib die Sitzungs­nummer. Dann leiten wir die HTTP-Abfrage um und senden die Antwort an den IE zurück.

Etwas ist schief gelaufen

Wir haben mit dem Testen und der Integration unseres Service begonnen. Aber nicht alles lief so reibungslos, wie wir es uns gewünscht hatten.
Wie sich heraus­stellte, kann der Name der Sitzung geändert werden, obwohl wir nicht verstanden haben, unter welchen Bedin­gungen dies geschieht. Es kam jedoch häufig vor, dass der Sitzungsname geändert wurde aber IE11 nur den Anfangswert der Umgebungs­va­riablen kennt und diesen Wert dauerhaft an den Routing­Service weitergibt.

Was ist in der Registry?

Es war notwendig, einen anderen Weg zu finden, um den Sitzungs­namen zu ermitteln. Wir haben nach Infor­ma­tionen zu Sitzungen in der Registry gesucht und wurden fündig: Unter HKEY_CURRENT_USER \ Volatile Environment ist es möglich eine Liste aller Sitzungen des aktuellen Benutzers abzurufen.

Wenn es nur um eine Anmeldung geht, ist alles in Ordnung, wir können sie lesen und verwenden. Wenn es für einen Benutzer jedoch viele Sitzungen gibt, müssen wir irgendwie feststellen, in welcher Sitzung wir uns befinden. Die beste Möglichkeit die uns einfiel war, den Pfad zum Ordner mit den tempo­rären Dateien abzugleichen.

Beispiel:

Im IE erhalten wir den aktuellen Pfad zu TEMP mithilfe von ActiveX Scripting.FileSystemObject. Auf diese Weise konnten wir den Namen unserer Sitzung ermitteln. Aber das ist nicht alles. Der Wert der Schlüssel in der Volatile Environment ist in der Tat die SID. Somit können wir sofort die erfor­der­liche IP-Adresse in JavaScript abrufen und eine Anfrage an diese senden.

Sollen wir es vereinfachen?

Schließlich können wir die SID abrufen und direkt eine Verbindung herstellen, ohne den Routing­Service zu verwenden. Aber der Lösungsweg sieht immer noch nicht schön aus. Die Inter­net­suche hat gezeigt, dass das Problem besteht, aber wie es behoben werden kann wird nirgendwo beschrieben. Und auch Microsoft bietet dazu bisher keine Lösung.

Wir hoffen, dass jemand mit diesem spezi­fi­schen Problem von unserer Erfahrung profi­tieren kann.

In der Software Quality Assurance werden bei der dynami­schen Quali­täts­si­cherung (Software­testen) verschiedene Methoden angewandt, um die Qualität von Produkten und Prozessen abzusi­chern. Anhand einer Dokumen­tation werden die Bestre­bungen Ihrer Qualität testfähig gemacht. Weiter­lesen

Die Imple­men­tierung einer neuen Software­lösung erfordert nicht nur die detail­lierte Auswahl und Anschaffung von Programmen. Die Heraus­for­derung stellt ebenso sehr die strate­gische Einführung der einzelnen Programm­kom­po­nenten, sowie die Sicher­stellung der Kompa­ti­bi­lität zur Erschaffung einer inein­an­der­grei­fenden IT-Infra­struktur dar. Das präzise Zusam­men­spiel techni­scher und organi­sa­to­ri­scher Abhän­gig­keiten und ein sinnvoller Handlungs­ablauf müssen gewähr­leistet werden. Zudem müssen die durch die Änderungen betrof­fenen Prozess­be­tei­ligten in die Imple­men­tierung einge­bunden werden, um die Akzeptanz der neuen Prozesse im Unter­nehmen zu garan­tieren. Nur durch Übung in einem Testumfeld kann die Sicherheit für die spätere Praxis geschaffen werden. Lediglich anwen­der­freund­liche Prozesse führen in der Praxis zum gewünschten Erfolg.

Software Development Lifecycle

Hierzu wird der Prozess des sogenannten Software Development Lifecycle angewendet. Er ist Teil des Quali­täts­ma­nage­ments eines jeden IT-Projekts. Nach Definition der zu erarbei­tenden Ziele, finden Auswahl und Einführung der hierfür notwen­digen Prozess­än­de­rungen sowie IT-Kompo­nenten statt. Daraufhin erfolgt eine Testphase, die das imple­men­tierte System auf Anwen­der­freund­lichkeit, Effek­ti­vität und Effizienz prüft. Durch erprobte Teststra­tegien und Konzepte werden Erkennt­nisse über mögliche Fehler in der Imple­men­tierung aufge­zeigt. Einzel­fall­sze­narien werden in einer Testum­gebung simuliert. Zudem können Änderungs­wünsche zur besseren Bedienung einge­bracht werden. Eine Analyse der Testergeb­nisse führt gegebe­nen­falls zu einer oder mehreren Repeti­ti­ons­phasen, inklusive notwen­diger Fehler­be­he­bungen und Anpas­sungen, bis die verein­barten Projekt­ziele erreicht sind. Zur Messung der Zieler­rei­chungs­quote können vorde­fi­nierte Kennzahlen zum Einsatz kommen, die indivi­duell, je nach Unter­nehmen, festgelegt werden.

Durch das Software Testing im Development Prozess kann kein fehler­freier Betrieb des neuen Prozesses garan­tiert werden, da während der Tests lediglich Einzel­fälle geprüft werden. Aller­dings werden erfah­rungs­gemäß aufkom­mende Fehler nach Einführung der Systeme im Lifebe­trieb erheblich reduziert.

Nicht nur für die fachliche Model­lierung von neuen IT-Prozessen sollte ein oder sollten sogar mehrere profes­sio­nelle Berater hinzu­ge­zogen werden. Durch standar­di­sierte und erprobte Erfah­rungen in der Optimierung und Automa­ti­sierung von IT-Prozessen können externe Dienst­leister ebenso wertvolle Unter­stützung während der einzelnen Entwick­lungs­zyklen der Imple­men­tierung der Prozesse bieten. Gerade beim Testing ist eine detail­lierte Dokumen­tation und Begleitung durch Program­mierer sinnvoll und notwendig, damit die richtigen Schluss­fol­ge­rungen aus den Fehlern gezogen werden können, um diese wiederum effizient lösen zu können.

Einheit­liche und verständlich geschriebene Anwen­der­hand­bücher zur Bedienung neuer Systeme, sowie Schulungen werden außerdem in der Regel durch den Dienst­leister zur Verfügung gestellt und müssen somit nicht vom Unter­nehmen selbst konzi­piert werden.

In Zeiten der stetigen Digita­li­sierung stehen Unter­nehmen verstärkt vor der Heraus­for­derung, die eigene IT-Infra­struktur nicht nur aufrecht zu erhalten, sondern außerdem bestehende Systeme zu moder­ni­sieren und ihre Poten­ziale durch die Umsetzung von techni­schen Innova­tionen zugunsten ihres Wettbe­werbs­vor­teils zu nutzen. Dabei benötigen sie aufgrund fehlender Eigen­res­sourcen profes­sio­nelle Unter­stützung. Doch welcher IT-Dienst­leister passt zum jewei­ligen Unter­nehmen? Welches Leistungs­port­folio deckt die eigenen Erwar­tungen bestmöglich ab?

Die Auswahl an IT-Dienst­leistern in Deutschland ist groß. Grund­sätzlich unter­scheidet man den Markt in drei Beratergruppen.

  • Die erste Gruppe stellen die sogenannten IT-Service-Dienst­leister dar. Sie konzen­trieren sich auf den Betrieb von Rechen­zentren sowie auf Wartung und Support der Anwen­dungs­um­gebung. Ihr Fokus liegt auf der frühzei­tigen Lokali­sierung von System­stö­rungen, Erarbeitung diesbe­züg­licher Lösungen sowie der Warnung, Vorbe­reitung und Begleitung des Kunden zur notwen­digen Anpassung der Software.
  • Die zweite Art der IT-Dienst­leister legt ihren Schwer­punkt auf die betriebs­spe­zi­fische System­ent­wicklung und –integration. Sie haben sich auf die Entwicklung von Indivi­du­al­software sowie deren Betrieb spezialisiert.
  • Die dritte Gruppe verfügt über ein umfas­sendes, zukunfts­ori­en­tiertes Leistungs­spektrum. Dieses beinhaltet eine Kombi­nation aus Management- und IT-Beratung mit der Orien­tierung an kunden- und projekt­spe­zi­fi­schen Anfor­de­rungen. Dazu gehören zum Beispiel Konzep­tio­nierung, Software­ent­wicklung, Reali­sierung innova­tiver IT-Geschäfts­mo­delle unter Einbezug von Trans­for­ma­ti­ons­ma­nagement, Business Prozess Outsourcing und Business Prozess Management.

Aktuell dominieren die IT-Service-Dienst­leister den deutschen Markt des IT-Consul­tings. Durch die steigende Weiter­ent­wicklung hinsichtlich moderner Techno­logien wächst der IT-Berater-Markt einer­seits an sich stetig. Anderer­seits entwi­ckeln sich die Kunden­an­for­de­rungen auf Basis neuer Geschäfts­mo­delle immer mehr in Richtung digitaler Trans­for­mation. Die Nachfrage nach IT-Beratern, die ihren Schwer­punkt auf den reinen Support fixer IT-Struk­turen in Betrieben gelegt haben, reduziert sich aktuell. Zunehmend werden Dienstleistungen nachge­fragt, die die Geschäfts­pro­zesse an die neuen Bedin­gungen der Wirtschaft anpassen. Die Unter­nehmen benötigen Unter­stützung bei Projekten wie etwa bei Expan­si­ons­vor­haben, Aufbau oder Erwei­te­rungen der Social Media Präsenz, der Einbindung von Cloud­lö­sungen oder Multi-Channel-Platt­formen, mit dem Ziel, den Bedin­gungen der allge­meinen Moder­ni­sierung zu genügen.

Bei der Auswahl der hierfür adäquaten, maßge­schnei­derten Software unter­stützen zukunfts­ori­en­tierte IT-Berater. In enger Zusam­men­arbeit mit dem Unter­nehmen werden Projekt­ziele und Vorge­hens­weisen für den Verän­de­rungs­prozess definiert. Struk­tu­rierte und erprobte Analy­se­schritte ermög­lichen indivi­duell auf das Unter­nehmen angepasste Projektstrategien.

Die externen Anfor­de­rungen an Unter­nehmen hinsichtlich Service­qua­lität, Flexi­bi­lität sowie Nachhal­tigkeit steigen stetig an. Hinzu kommt der erhöhte Kosten­druck des freien Marktes. Unter­nehmen sind mehr und mehr gezwungen, ihre Prozesse zu optimieren und zu automa­ti­sieren, um wettbe­werbs­fähig zu bleiben. Hierzu ist eine genaue Aufnahme und Analyse der Workflow-Landschaft des Betriebs notwendig. Weiter­lesen

Versio­nierung ist innerhalb eines Dokumen­ten­ma­nage­ment­systems (DMS) kein Fremdwort. Norma­ler­weise bedeutet es, dass Historie der Änderungen von Dokumenten gespei­chert wird. In unserem Artikel wird beschrieben, was zu tun ist, wenn die Dokumente aus den Vorlagen erstellt werden und die Vorlagen selbst fortent­wi­ckelt werden können, ohne dass die Abhän­gig­keiten vom Dokument zur Vorlage aufgelöst worden sind.

Bei größeren Unter­nehmen werden für die Erstellung von Dokumenten diverse komplexe Vorlagen umgesetzt. Z.B. auf XML-Basis, wie bei Gerichten und Staats­an­walt­schaften, oder als dot-Datei von MS Word. Die Vorlagen können vonein­ander abhängen, modula­ri­siert werden, Includs (Collec­tions) haben und vieles mehr. Die Dokumente werden mit dynami­schen Daten gefüllt und können eine interne Sprache haben, z.B. Skript­ele­mente oder Makros. Eine Vorlage wird geladen, ggf. inter­pre­tiert, oder es werden die Skripte gestartet, die eine gewisse Dynamik ins Dokument bringen. Wenn in der Skript­sprache die Bedin­gungs­ele­mente verwendet werden, kann es sein, dass einige Vorlagen erst später gelesen werden können, z.B. bei der nächsten Session oder der nächsten Inter­pre­tierung des Dokumentes. Dadurch kommen neue dynamische Daten aus Vorlagen ins Dokument.

Diesen Ablauf kann man grafisch wie folgt darstellen:

 

Jede XML-Vorlage (oder jeder XML-Baustein) kann noch bedingte Elemente haben und auf weitere XML-Vorlagen verweisen. Das Problem dabei: Wenn man versucht, die Vorlagen zu erweitern, könnten alte gespei­cherte Dokumente nicht mehr ablauf­fähig sein. Es können verschiedene Probleme von „Variable nicht gefunden“ bis „das Modul existiert nicht mehr“ auftreten. Um die aus diesen Vorlagen erstellten Dokumente immer inter­pre­tierbar zu halten und die Abhän­gigkeit von der Vorlage aufzu­lösen, gibt es zurzeit zwei Alternativen:

1. Die Vorlagen sollen so entwi­ckeln werden, dass keine Abhän­gig­keiten zwischen den geladenen Vorlagen im Dokument und erwei­terten oder neu entwi­ckelten Vorlagen, auch in abseh­barer Zeit, entstehen können.

2. Ein Versio­nie­rungs­system für die Vorlagen einführen – Die Alter­native zu Nummer 1 ist einfach, könnte aber bei größeren Projekten nicht zu reali­sieren sein, weil fast bei jeder Erwei­terung der Vorlagen alle anderen immer von jemandem darauf hin überprüft werden müssen, ob keine Abhän­gig­keiten entstehen.

Versio­nie­rungs­system

Die Idee ist nun, jede Vorlage zu versio­nieren. Alle XML-Vorlagen müssen dazu über ein Versi­ons­ver­wal­tung­s­ystem (z.B. SVN, VSS) versio­niert werden. Die Nummer der Revision sollte in der Vorlage sichtbar sein, z.B. in Form von Metadaten.

<Meta name="vss" value="{version}27{/version}" />

Die Infor­mation wird später beim Laden von alten Dokumenten verwendet. Das DMS sollte nach dem Öffnen eines alten XML-Dokumentes immer eine Zuordnung je nach Erstel­lungs­datum von allen XML-Vorlagen zum Dokument finden können. Als Schema könnte man alles in einer Tabelle darstellen:

Nummer oder symbo­li­scher Name des Releases von XML-Vorlagen

Ablauf der Textverarbeitung

release1Aktuelle_XML_Vorlage_vom_2008-02–01_XX-XX-XX.zip
Die Dokumente werden im Zeitraum von 01.02.2008 bis 07.02.2008 erstellt und gespei­chert. Alle Dokumente, wenn nichts anderes von der Verwaltung vorge­geben, sollen nur die XML-Bausteine aus der Auslie­ferung „Aktuelle_XML_Vorlage_vom_2008-02–01_XX-XX-XX.zip“ verwenden.
release2Aktuelle_XML_Vorlage_vom_2008-02–07_XX-XX-XX.zip
Die Dokumente werden im Zeitraum von 07.02.2008 bis 14.02.2008 erstellt und gespei­chert. Alle Dokumente, wenn nichts anderes von der Verwaltung vorge­geben, sollen nur die XML- Bausteine aus der Auslie­ferung „Aktuelle_XML_Vorlage_vom_2008-02–07_XX-XX-XX.zip“ verwenden.
release…Aktuelle_XML_Vorlage_vom_.…._XX-XX-XX.zip
…..
releaseNNAktuelle_XML_Vorlage_vom_2008-10–15_XX-XX-XX.zip
Die Dokumente werden im Zeitraum von 15.10.2008 bis TT.MM.JJJJ erstellt und gespei­chert. Alle Dokumente, wenn nichts anderes von der Verwaltung vorge­geben, sollen nur die XML- Bausteine aus der Auslie­ferung „Aktuelle_XML_Vorlage_vom_2008-10–15_XX-XX-XX.zip“ verwenden.

 

Die Nummer oder eine textuelle Bezeichnung der Auslie­ferung (kurz „release“ oder „schnitt“) soll als Metada­tensatz zum Dokument gespei­chert werden. Später, beim Laden des Dokumentes, gibt diese Bezeichnung vor, welche Version der noch nicht geladenen XML-Vorlagen in dieses Dokument geholt bzw. bei Bedarf aktua­li­siert werden sollte.

Allen Bezeich­nungen des Releases werden alle enthal­tenen XML-Vorlagen mit eigenen Versionen zugeordnet. Falls ein Release nur eine Delta der XML-Vorlagen enthielt, diese wird auch mit gleichem Prinzip markiert. Wenn bei der Inter­pre­tierung des Dokumentes kein XML-Baustein in der Bezeichnung des Releases gefunden wird, geht DMS automa­tisch auf eine Version der Bezeichnung runter.

Wofür benötigt man ein Dokumen­ten­ver­wal­tungs­system (DMS)?

Das DMS dient der revisi­ons­si­cheren und vor allem auch platz­spa­renden Ablage von Faxen, E‑Mails sowie der digita­li­sierten Eingangspost und auch Dokumenten, wie zum Beispiel Belegen, Rechnungen, Versi­che­rungs­un­ter­lagen und Verträgen. Das DMS sorgt ebenfalls dafür, dass man diese Unter­lagen zu einem späteren Zeitpunkt schnell und effizient wieder auffinden kann. Natürlich könnte man auch ohne ein Dokumen­ten­ver­wal­tungs­system auskommen, möchte man aber die Geschäfts­pro­zesse verein­fachen, digita­li­sieren und beschleu­nigen, ist der Schritt hin zum Dokumen­ten­ver­wal­tungs­system tatsächlich der richtige. Das Wichtigste ist, dass alle Dokumente revisions- und rechts­sicher aufbe­wahrt sind und auch schnell wieder­ge­funden werden können.

Welche Folgen hat es, wenn man dieses System nicht nutzt?

Wenn man sich für ein Dokument­ma­nage­ment­system entscheidet, dann sollte dieses auch genutzt werden. Die Dokumente müssen ins System einge­bracht werden, damit sie im Dokumen­ten­system verwaltet werden können. Wenn man dieses Dokument­ma­nage­ment­system einführt, sollte es von allen Mitar­beitern genutzt werden. Es zeigt sich aber in der Regel, dass nicht jeder Mitar­beiter eine Lizenz erhält, was nicht an den Lizenz­preisen liegt, sondern an den Folge­kosten. Es ist unumgänglich, dass jeder Mitar­beiter auf dieses System geschult wird, damit es auch optimal angewendet wird. Dabei können mitunter hohe Aufwen­dungen entstehen. Wird aber ein Dokument­ma­nage­ment­system einge­führt, aber nicht genutzt, bzw. nicht in der Form, wie es eigentlich vorge­sehen ist (also mit Dokumenten befüllt), ist das Vorhaben schnell zum Scheitern verur­teilt. Die wichtigen Daten sind damit nicht im System abgelegt. Tritt aber der Idealfall ein und es wird genutzt, dann können die Daten­server ersetzt werden. Nur wenn das der Fall ist, ist auch sicher gestellt, dass sich ein DMS umsetzen lässt.

Fortsetzung folgt.