more and better test cases + fixed memory leak introduced by changeset e43dee5892f4

Tue, 21 Apr 2015 09:47:52 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 21 Apr 2015 09:47:52 +0200
changeset 25
f82aa7afe872
parent 24
e43dee5892f4
child 26
05c3c6842aef

more and better test cases + fixed memory leak introduced by changeset e43dee5892f4

Makefile file | annotate | diff | comparison | revisions
src/c2html.c file | annotate | diff | comparison | revisions
test/Game.java file | annotate | diff | comparison | revisions
test/bigtestfile.c file | annotate | diff | comparison | revisions
test/ctestfile.c file | annotate | diff | comparison | revisions
test/javatestfile.java file | annotate | diff | comparison | revisions
--- a/Makefile	Sun Apr 19 10:48:00 2015 +0200
+++ b/Makefile	Tue Apr 21 09:47:52 2015 +0200
@@ -1,7 +1,7 @@
 #
 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 #
-# Copyright 2014 Mike Becker. All rights reserved.
+# Copyright 2015 Mike Becker. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
 # modification, are permitted provided that the following conditions are met:
@@ -37,12 +37,12 @@
 	$(MKDIR) build
 	
 test: compile
-	./build/$(BIN) $(ARGS) src/c2html.c -o build/code.html \
+	./build/$(BIN) $(ARGS) test/ctestfile.c -o build/ctest.html \
+	-H test/header.html -F test/footer.html
+	./build/$(BIN) $(ARGS) -j test/javatestfile.java -o build/javatest.html \
+	-H test/header.html -F test/footer.html
+	./build/$(BIN) $(ARGS) test/bigtestfile.c -o build/bigtest.html \
 	-H test/header.html -F test/footer.html
 
-test-java: compile
-	./build/$(BIN) $(ARGS) -j test/Game.java -o build/code.html \
-	-H test/header.html -F test/footer.html
-	
 clean:
 	$(RM) -f -R build
--- a/src/c2html.c	Sun Apr 19 10:48:00 2015 +0200
+++ b/src/c2html.c	Tue Apr 21 09:47:52 2015 +0200
@@ -44,7 +44,8 @@
     l[width] = 0;
     if (inputfile->count >= inputfile->capacity) {
         inputfile->capacity <<= 1;
-        inputfile->lines = realloc(inputfile->lines, inputfile->capacity);
+        inputfile->lines = realloc(inputfile->lines,
+            sizeof(char*)*inputfile->capacity);
     }
     inputfile->lines[inputfile->count] = l;
     inputfile->maxlinewidth =
@@ -286,6 +287,7 @@
                     (fmt_write_func)fwrite,
                     fout,
                     settings.showlinenumbers);
+            freeinputfilebuffer(inputfile);
         } else {
             perror("Error opening input file");
             retcode = -1;
--- a/test/Game.java	Sun Apr 19 10:48:00 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-package de.uapcore.threelittlestars;
-
-import java.awt.BorderLayout;
-import java.io.IOException;
-
-import javax.swing.JFrame;
-import javax.swing.JOptionPane;
-
-import de.uapcore.threelittlestars.entities.Player;
-import de.uapcore.threelittlestars.entities.Player.PlayerCharacter;
-import de.uapcore.threelittlestars.managers.AssetManager;
-import de.uapcore.threelittlestars.managers.InputManager;
-import de.uapcore.threelittlestars.managers.WorldManager;
-import de.uapcore.threelittlestars.renderers.GameRenderer;
-
-
-public class Game implements Runnable {
-   
-   public static final int TICK_RATE = 32;
-
-   private InputManager im;
-   private AssetManager am;
-   private WorldManager wm;
-   
-   private MainFrame frame;
-   private MainPanel canvas;
-
-   public Game(MainFrame frame, PlayerCharacter playerCharacter) {
-
-      this.frame = frame;
-      canvas = new MainPanel();
-      
-      am = new AssetManager(canvas);
-      wm = new WorldManager(am);
-      im = new InputManager();
-      
-      canvas.setRenderer(new GameRenderer(wm));
-      canvas.addKeyListener(im);
-      
-      frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
-      frame.add(canvas, BorderLayout.CENTER);
-      frame.pack();
-      canvas.requestFocusInWindow();
-
-      try {
-         startGame(playerCharacter);
-      } catch (IOException e) {
-         JOptionPane.showMessageDialog(frame, "Die Weltdaten sind beschÃĪdigt.",
-               frame.getTitle(), JOptionPane.ERROR_MESSAGE);
-         System.exit(1);
-      }
-   }
-   
-   public void startGame(PlayerCharacter character) throws IOException {
-      Player.setCharacter(character);
-      
-      // TODO: asset / savegame loading
-      wm.loadAsset("testworld"); // just testing here
-      
-      new Thread(this).start();
-   }
-   
-   @Override
-   public void run() {
-      long lastTick = System.currentTimeMillis();
-      do {
-         long currentTick = System.currentTimeMillis();
-         if (currentTick - lastTick >= TICK_RATE) {
-            lastTick += TICK_RATE;
-            
-            // Deliver buffered input events
-            im.deliverEvents(wm);
-            
-            // Call updates
-            wm.update();
-            
-            // Catch other key events
-            if (im.isEscapePressed()) {
-               frame.setVisible(false);
-            }
-            
-            
-            // Repaint canvas
-            canvas.repaint();
-         }
-         Thread.yield();
-      } while (frame.isVisible());
-      
-      // Cleanup stuff
-      frame.dispose();
-      System.exit(0);
-   }
-
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/bigtestfile.c	Tue Apr 21 09:47:52 2015 +0200
@@ -0,0 +1,808 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2014 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include "rules.h"
+#include "chess.h"
+#include <string.h>
+#include <stdlib.h>
+#include <sys/time.h>
+
+static GameState gamestate_copy_sim(GameState *gamestate) {
+    GameState simulation = *gamestate;
+    if (simulation.lastmove) {
+        MoveList *lastmovecopy = malloc(sizeof(MoveList));
+        *lastmovecopy = *(simulation.lastmove);
+        simulation.movelist = simulation.lastmove = lastmovecopy;
+    }
+
+    return simulation;
+}
+
+void gamestate_init(GameState *gamestate) {
+    memset(gamestate, 0, sizeof(GameState));
+    
+    Board initboard = {
+        {WROOK, WKNIGHT, WBISHOP, WQUEEN, WKING, WBISHOP, WKNIGHT, WROOK},
+        {WPAWN, WPAWN,   WPAWN,   WPAWN,  WPAWN, WPAWN,   WPAWN,   WPAWN},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {0,     0,       0,       0,      0,     0,       0,       0},
+        {BPAWN, BPAWN,   BPAWN,   BPAWN,  BPAWN, BPAWN,   BPAWN,   BPAWN},
+        {BROOK, BKNIGHT, BBISHOP, BQUEEN, BKING, BBISHOP, BKNIGHT, BROOK}
+    };
+    memcpy(gamestate->board, initboard, sizeof(Board));
+}
+
+void gamestate_cleanup(GameState *gamestate) {
+    MoveList *elem;
+    elem = gamestate->movelist;
+    while (elem) {
+        MoveList *cur = elem;
+        elem = elem->next;
+        free(cur);
+    };
+}
+
+/* MUST be called IMMEDIATLY after applying a move to work correctly */
+static void format_move(GameState *gamestate, Move *move) {
+    char *string = move->string;
+    
+    /* at least 8 characters should be available, wipe them out */
+    memset(string, 0, 8);
+    
+    /* special formats for castling */
+    if ((move->piece&PIECE_MASK) == KING &&
+            abs(move->tofile-move->fromfile) == 2) {
+        if (move->tofile==fileidx('c')) {
+            memcpy(string, "O-O-O", 5);
+        } else {
+            memcpy(string, "O-O", 3);
+        }
+    }
+
+    /* start by notating the piece character */
+    string[0] = getpiecechr(move->piece);
+    int idx = string[0] ? 1 : 0;
+    
+    /* find out how many source information we do need */
+    uint8_t piece = move->piece & PIECE_MASK;
+    if (piece == PAWN) {
+        if (move->capture) {
+            string[idx++] = filechr(move->fromfile);
+        }
+    } else if (piece != KING) {
+        Move threats[16];
+        uint8_t threatcount;
+        get_real_threats(gamestate, move->torow, move->tofile,
+            move->piece&COLOR_MASK, threats, &threatcount);
+        if (threatcount > 1) {
+            int ambrows = 0, ambfiles = 0;
+            for (uint8_t i = 0 ; i < threatcount ; i++) {
+                if (threats[i].fromrow == move->fromrow) {
+                    ambrows++;
+                }
+                if (threats[i].fromfile == move->fromfile) {
+                    ambfiles++;
+                }
+            }
+            /* ambiguous row, name file */
+            if (ambrows > 1) {
+                string[idx++] = filechr(move->fromfile);
+            }
+            /* ambiguous file, name row */
+            if (ambfiles > 1) {
+                string[idx++] = filechr(move->fromrow);
+            }
+        }
+    }
+    
+    /* capturing? */
+    if (move->capture) {
+        string[idx++] = 'x';
+    }
+    
+    /* destination */
+    string[idx++] = filechr(move->tofile);
+    string[idx++] = rowchr(move->torow);
+    
+    /* promotion? */
+    if (move->promotion) {
+        string[idx++] = '=';
+        string[idx++] = getpiecechr(move->promotion);
+    }
+    
+    /* check? */
+    if (move->check) {
+        /* works only, if this function is called when applying the move */
+        string[idx++] = gamestate->checkmate?'#':'+';
+    }
+}
+
+static void addmove(GameState* gamestate, Move *move) {
+    MoveList *elem = malloc(sizeof(MoveList));
+    elem->next = NULL;
+    elem->move = *move;
+    
+    struct timeval curtimestamp;
+    gettimeofday(&curtimestamp, NULL);
+    elem->move.timestamp.tv_sec = curtimestamp.tv_sec;
+    elem->move.timestamp.tv_usec = curtimestamp.tv_usec;
+    
+    if (gamestate->lastmove) {
+        struct movetimeval *lasttstamp = &(gamestate->lastmove->move.timestamp);
+        uint64_t sec = curtimestamp.tv_sec - lasttstamp->tv_sec;
+        suseconds_t micros;
+        if (curtimestamp.tv_usec < lasttstamp->tv_usec) {
+            micros = 1e6L-(lasttstamp->tv_usec - curtimestamp.tv_usec);
+            sec--;
+        } else {
+            micros = curtimestamp.tv_usec - lasttstamp->tv_usec;
+        }
+        
+        elem->move.movetime.tv_sec = sec;
+        elem->move.movetime.tv_usec = micros;
+        
+        gamestate->lastmove->next = elem;
+        gamestate->lastmove = elem;
+    } else {
+        elem->move.movetime.tv_usec = 0;
+        elem->move.movetime.tv_sec = 0;
+        gamestate->movelist = gamestate->lastmove = elem;
+    }
+}
+
+char getpiecechr(uint8_t piece) {
+    switch (piece & PIECE_MASK) {
+    case ROOK: return 'R';
+    case KNIGHT: return 'N';
+    case BISHOP: return 'B';
+    case QUEEN: return 'Q';
+    case KING: return 'K';
+    default: return '\0';
+    }
+}
+
+uint8_t getpiece(char c) {
+    switch (c) {
+        case 'R': return ROOK;
+        case 'N': return KNIGHT;
+        case 'B': return BISHOP;
+        case 'Q': return QUEEN;
+        case 'K': return KING;
+        default: return 0;
+    }
+}
+
+static void apply_move_impl(GameState *gamestate, Move *move, _Bool simulate) {
+    uint8_t piece = move->piece & PIECE_MASK;
+    uint8_t color = move->piece & COLOR_MASK;
+    
+    /* en passant capture */
+    if (move->capture && piece == PAWN &&
+        mdst(gamestate->board, move) == 0) {
+        gamestate->board[move->fromrow][move->tofile] = 0;
+    }
+    
+    /* remove old en passant threats */
+    for (uint8_t file = 0 ; file < 8 ; file++) {
+        gamestate->board[3][file] &= ~ENPASSANT_THREAT;
+        gamestate->board[4][file] &= ~ENPASSANT_THREAT;
+    }
+    
+    /* add new en passant threat */
+    if (piece == PAWN && (
+        (move->fromrow == 1 && move->torow == 3) ||
+        (move->fromrow == 6 && move->torow == 4))) {
+        move->piece |= ENPASSANT_THREAT;
+    }
+    
+    /* move (and maybe capture or promote) */
+    msrc(gamestate->board, move) = 0;
+    if (move->promotion) {
+        mdst(gamestate->board, move) = move->promotion;
+    } else {
+        mdst(gamestate->board, move) = move->piece;
+    }
+    
+    /* castling */
+    if (piece == KING && move->fromfile == fileidx('e')) {
+        
+        if (move->tofile == fileidx('g')) {
+            gamestate->board[move->torow][fileidx('h')] = 0;
+            gamestate->board[move->torow][fileidx('f')] = color|ROOK;
+        } else if (move->tofile == fileidx('c')) {
+            gamestate->board[move->torow][fileidx('a')] = 0;
+            gamestate->board[move->torow][fileidx('d')] = color|ROOK;
+        }
+    }
+
+    if (!simulate) {
+        if (!move->string[0]) {
+            format_move(gamestate, move);
+        }
+    }
+    /* add move, even in simulation (checkmate test needs it) */
+    addmove(gamestate, move);
+}
+
+void apply_move(GameState *gamestate, Move *move) {
+    apply_move_impl(gamestate, move, 0);
+}
+
+static int validate_move_rules(GameState *gamestate, Move *move) {
+    /* validate indices (don't trust opponent) */
+    if (!chkidx(move)) {
+        return INVALID_POSITION;
+    }
+    
+    /* must move */
+    if (move->fromfile == move->tofile && move->fromrow == move->torow) {
+        return INVALID_MOVE_SYNTAX;
+    }
+    
+    /* does piece exist */
+    if ((msrc(gamestate->board, move)&(PIECE_MASK|COLOR_MASK))
+           != (move->piece&(PIECE_MASK|COLOR_MASK))) {
+        return INVALID_POSITION;
+    }
+    
+    /* can't capture own pieces */
+    if ((mdst(gamestate->board, move) & COLOR_MASK)
+            == (move->piece & COLOR_MASK)) {
+        return RULES_VIOLATED;
+    }
+    
+    /* must capture, if and only if destination is occupied */
+    if ((mdst(gamestate->board, move) == 0 && move->capture) ||
+            (mdst(gamestate->board, move) != 0 && !move->capture)) {
+        return INVALID_MOVE_SYNTAX;
+    }
+    
+    /* validate individual rules */
+    _Bool chkrules;
+    switch (move->piece & PIECE_MASK) {
+    case PAWN:
+        chkrules = pawn_chkrules(gamestate, move) &&
+            !pawn_isblocked(gamestate, move);
+        break;
+    case ROOK:
+        chkrules = rook_chkrules(move) &&
+            !rook_isblocked(gamestate, move);
+        break;
+    case KNIGHT:
+        chkrules = knight_chkrules(move); /* knight is never blocked */
+        break;
+    case BISHOP:
+        chkrules = bishop_chkrules(move) &&
+            !bishop_isblocked(gamestate, move);
+        break;
+    case QUEEN:
+        chkrules = queen_chkrules(move) &&
+            !queen_isblocked(gamestate, move);
+        break;
+    case KING:
+        chkrules = king_chkrules(gamestate, move) &&
+            !king_isblocked(gamestate, move);
+        break;
+    default:
+        return INVALID_MOVE_SYNTAX;
+    }
+    
+    return chkrules ? VALID_MOVE_SEMANTICS : RULES_VIOLATED;
+}
+
+int validate_move(GameState *gamestate, Move *move) {
+    
+    int result = validate_move_rules(gamestate, move);
+    
+    /* cancel processing to save resources */
+    if (result != VALID_MOVE_SEMANTICS) {
+        return result;
+    }
+    
+    /* find kings for check validation */
+    uint8_t piececolor = (move->piece & COLOR_MASK);
+    
+    uint8_t mykingfile = 0, mykingrow = 0, opkingfile = 0, opkingrow = 0;
+    for (uint8_t row = 0 ; row < 8 ; row++) {
+        for (uint8_t file = 0 ; file < 8 ; file++) {
+            if (gamestate->board[row][file] ==
+                    (piececolor == WHITE?WKING:BKING)) {
+                mykingfile = file;
+                mykingrow = row;
+            } else if (gamestate->board[row][file] ==
+                    (piececolor == WHITE?BKING:WKING)) {
+                opkingfile = file;
+                opkingrow = row;
+            }
+        }
+    }
+    
+    /* simulate move for check validation */
+    GameState simulation = gamestate_copy_sim(gamestate);
+    Move simmove = *move;
+    apply_move_impl(&simulation, &simmove, 1);
+    
+    /* don't move into or stay in check position */
+    if (is_covered(&simulation, mykingrow, mykingfile,
+        opponent_color(piececolor))) {
+        
+        gamestate_cleanup(&simulation);
+        if ((move->piece & PIECE_MASK) == KING) {
+            return KING_MOVES_INTO_CHECK;
+        } else {
+            /* last move is always not null in this case */
+            return gamestate->lastmove->move.check ?
+                KING_IN_CHECK : PIECE_PINNED;
+        }
+    }
+    
+    /* correct check and checkmate flags (move is still valid) */
+    Move threats[16];
+    uint8_t threatcount;
+    move->check = get_threats(&simulation, opkingrow, opkingfile,
+        piececolor, threats, &threatcount);
+    
+    if (move->check) {
+        /* determine possible escape fields */
+        _Bool canescape = 0;
+        for (int dr = -1 ; dr <= 1 && !canescape ; dr++) {
+            for (int df = -1 ; df <= 1 && !canescape ; df++) {
+                if (!(dr == 0 && df == 0)  &&
+                        isidx(opkingrow + dr) && isidx(opkingfile + df)) {
+                    
+                    /* escape field neither blocked nor covered */
+                    if ((simulation.board[opkingrow + dr][opkingfile + df]
+                            & COLOR_MASK) != opponent_color(piececolor)) {
+                        canescape |= !is_covered(&simulation,
+                            opkingrow + dr, opkingfile + df, piececolor);
+                    }
+                }
+            }
+        }
+        /* can't escape, can he capture? */
+        if (!canescape && threatcount == 1) {
+            canescape = is_attacked(&simulation, threats[0].fromrow,
+                threats[0].fromfile, opponent_color(piececolor));
+        }
+        
+        /* can't capture, can he block? */
+        if (!canescape && threatcount == 1) {
+            Move *threat = &(threats[0]);
+            uint8_t threatpiece = threat->piece & PIECE_MASK;
+            
+            /* knight, pawns and the king cannot be blocked */
+            if (threatpiece == BISHOP || threatpiece == ROOK
+                || threatpiece == QUEEN) {
+                if (threat->fromrow == threat->torow) {
+                    /* rook aspect (on row) */
+                    int d = threat->tofile > threat->fromfile ? 1 : -1;
+                    uint8_t file = threat->fromfile;
+                    while (!canescape && file != threat->tofile - d) {
+                        file += d;
+                        canescape |= is_protected(&simulation,
+                            threat->torow, file, opponent_color(piececolor));
+                    }
+                } else if (threat->fromfile == threat->tofile) {
+                    /* rook aspect (on file) */
+                    int d = threat->torow > threat->fromrow ? 1 : -1;
+                    uint8_t row = threat->fromrow;
+                    while (!canescape && row != threat->torow - d) {
+                        row += d;
+                        canescape |= is_protected(&simulation,
+                            row, threat->tofile, opponent_color(piececolor));
+                    }
+                } else {
+                    /* bishop aspect */
+                    int dr = threat->torow > threat->fromrow ? 1 : -1;
+                    int df = threat->tofile > threat->fromfile ? 1 : -1;
+
+                    uint8_t row = threat->fromrow;
+                    uint8_t file = threat->fromfile;
+                    while (!canescape && file != threat->tofile - df
+                        && row != threat->torow - dr) {
+                        row += dr;
+                        file += df;
+                        canescape |= is_protected(&simulation, row, file,
+                            opponent_color(piececolor));
+                    }
+                }
+            }
+        }
+            
+        if (!canescape) {
+            gamestate->checkmate = 1;
+        }
+    }
+    
+    gamestate_cleanup(&simulation);
+    
+    return VALID_MOVE_SEMANTICS;
+}
+
+_Bool get_threats(GameState *gamestate, uint8_t row, uint8_t file,
+        uint8_t color, Move *threats, uint8_t *threatcount) {
+    Move candidates[32];
+    int candidatecount = 0;
+    for (uint8_t r = 0 ; r < 8 ; r++) {
+        for (uint8_t f = 0 ; f < 8 ; f++) {
+            if ((gamestate->board[r][f] & COLOR_MASK) == color) {
+                // non-capturing move
+                memset(&(candidates[candidatecount]), 0, sizeof(Move));
+                candidates[candidatecount].piece = gamestate->board[r][f];
+                candidates[candidatecount].fromrow = r;
+                candidates[candidatecount].fromfile = f;
+                candidates[candidatecount].torow = row;
+                candidates[candidatecount].tofile = file;
+                candidatecount++;
+
+                // capturing move
+                memcpy(&(candidates[candidatecount]),
+                    &(candidates[candidatecount-1]), sizeof(Move));
+                candidates[candidatecount].capture = 1;
+                candidatecount++;
+            }
+        }
+    }
+
+    if (threatcount) {
+        *threatcount = 0;
+    }
+    
+    
+    _Bool result = 0;
+    
+    for (int i = 0 ; i < candidatecount ; i++) {
+        if (validate_move_rules(gamestate, &(candidates[i]))
+                == VALID_MOVE_SEMANTICS) {
+            result = 1;
+            if (threats && threatcount) {
+                threats[(*threatcount)++] = candidates[i];
+            }
+        }
+    }
+    
+    return result;
+}
+
+_Bool is_pinned(GameState *gamestate, Move *move) {
+    uint8_t color = move->piece & COLOR_MASK;
+
+    uint8_t kingfile = 0, kingrow = 0;
+    for (uint8_t row = 0 ; row < 8 ; row++) {
+        for (uint8_t file = 0 ; file < 8 ; file++) {
+            if (gamestate->board[row][file] == (color|KING)) {
+                kingfile = file;
+                kingrow = row;
+            }
+        }
+    }
+
+    GameState simulation = gamestate_copy_sim(gamestate);
+    Move simmove = *move;
+    apply_move(&simulation, &simmove);
+    _Bool covered = is_covered(&simulation,
+        kingrow, kingfile, opponent_color(color));
+    gamestate_cleanup(&simulation);
+    
+    return covered;
+}
+
+_Bool get_real_threats(GameState *gamestate, uint8_t row, uint8_t file,
+        uint8_t color, Move *threats, uint8_t *threatcount) {
+    
+    if (threatcount) {
+        *threatcount = 0;
+    }
+
+    Move candidates[16];
+    uint8_t candidatecount;
+    if (get_threats(gamestate, row, file, color, candidates, &candidatecount)) {
+        
+        _Bool result = 0;
+        uint8_t kingfile = 0, kingrow = 0;
+        for (uint8_t row = 0 ; row < 8 ; row++) {
+            for (uint8_t file = 0 ; file < 8 ; file++) {
+                if (gamestate->board[row][file] == (color|KING)) {
+                    kingfile = file;
+                    kingrow = row;
+                }
+            }
+        }
+
+        for (uint8_t i = 0 ; i < candidatecount ; i++) {
+            GameState simulation = gamestate_copy_sim(gamestate);
+            Move simmove = candidates[i];
+            apply_move(&simulation, &simmove);
+            if (!is_covered(&simulation, kingrow, kingfile,
+                    opponent_color(color))) {
+                result = 1;
+                if (threats && threatcount) {
+                    threats[(*threatcount)++] = candidates[i];
+                }
+            }
+        }
+        
+        return result;
+    } else {
+        return 0;
+    }
+}
+
+static int getlocation(GameState *gamestate, Move *move) {   
+
+    uint8_t color = move->piece & COLOR_MASK;
+    _Bool incheck = gamestate->lastmove?gamestate->lastmove->move.check:0;
+    
+    Move threats[16], *threat = NULL;
+    uint8_t threatcount;
+    
+    if (get_threats(gamestate, move->torow, move->tofile, color,
+            threats, &threatcount)) {
+        
+        int reason = INVALID_POSITION;
+        
+        // find threats for the specified position
+        for (uint8_t i = 0 ; i < threatcount ; i++) {
+            if ((threats[i].piece & (PIECE_MASK | COLOR_MASK))
+                    == move->piece &&
+                    (move->fromrow == POS_UNSPECIFIED ||
+                    move->fromrow == threats[i].fromrow) &&
+                    (move->fromfile == POS_UNSPECIFIED ||
+                    move->fromfile == threats[i].fromfile)) {
+
+                if (threat) {
+                    return AMBIGUOUS_MOVE;
+                } else {
+                    // found threat is no real threat
+                    if (is_pinned(gamestate, &(threats[i]))) {
+                        reason = incheck?KING_IN_CHECK:PIECE_PINNED;
+                    } else {
+                        threat = &(threats[i]);
+                    }
+                }
+            }
+        }
+        
+        // can't threaten specified position
+        if (!threat) {
+            return reason;
+        }
+
+        memcpy(move, threat, sizeof(Move));
+        return VALID_MOVE_SYNTAX;
+    } else {
+        return INVALID_POSITION;
+    }
+}
+
+int eval_move(GameState *gamestate, char *mstr, Move *move, uint8_t color) {
+    memset(move, 0, sizeof(Move));
+    move->fromfile = POS_UNSPECIFIED;
+    move->fromrow = POS_UNSPECIFIED;
+
+    size_t len = strlen(mstr);
+    if (len < 1 || len > 6) {
+        return INVALID_MOVE_SYNTAX;
+    }
+    
+    /* evaluate check/checkmate flags */
+    if (mstr[len-1] == '+') {
+        len--; mstr[len] = '\0';
+        move->check = 1;
+    } else if (mstr[len-1] == '#') {
+        len--; mstr[len] = '\0';
+        /* ignore - validation should set game state */
+    }
+    
+    /* evaluate promotion */
+    if (len > 3 && mstr[len-2] == '=') {
+        move->promotion = getpiece(mstr[len-1]);
+        if (!move->promotion) {
+            return INVALID_MOVE_SYNTAX;
+        } else {
+            move->promotion |= color;
+            len -= 2;
+            mstr[len] = 0;
+        }
+    }
+    
+    if (len == 2) {
+        /* pawn move (e.g. "e4") */
+        move->piece = PAWN;
+        move->tofile = fileidx(mstr[0]);
+        move->torow = rowidx(mstr[1]);
+    } else if (len == 3) {
+        if (strcmp(mstr, "O-O") == 0) {
+            /* king side castling */
+            move->piece = KING;
+            move->fromfile = fileidx('e');
+            move->tofile = fileidx('g');
+            move->fromrow = move->torow = color == WHITE ? 0 : 7;
+        } else {
+            /* move (e.g. "Nf3") */
+            move->piece = getpiece(mstr[0]);
+            move->tofile = fileidx(mstr[1]);
+            move->torow = rowidx(mstr[2]);
+        }
+    } else if (len == 4) {
+        move->piece = getpiece(mstr[0]);
+        if (!move->piece) {
+            move->piece = PAWN;
+            move->fromfile = fileidx(mstr[0]);
+        }
+        if (mstr[1] == 'x') {
+            /* capture (e.g. "Nxf3", "dxe5") */
+            move->capture = 1;
+        } else {
+            /* move (e.g. "Ndf3", "N2c3", "e2e4") */
+            if (isfile(mstr[1])) {
+                move->fromfile = fileidx(mstr[1]);
+                if (move->piece == PAWN) {
+                    move->piece = 0;
+                }
+            } else {
+                move->fromrow = rowidx(mstr[1]);
+            }
+        }
+        move->tofile = fileidx(mstr[2]);
+        move->torow = rowidx(mstr[3]);
+    } else if (len == 5) {
+        if (strcmp(mstr, "O-O-O") == 0) {
+            /* queen side castling "O-O-O" */
+            move->piece = KING;
+            move->fromfile = fileidx('e');
+            move->tofile = fileidx('c');
+            move->fromrow = move->torow = color == WHITE ? 0 : 7;
+        } else {
+            move->piece = getpiece(mstr[0]);
+            if (mstr[2] == 'x') {
+                move->capture = 1;
+                if (move->piece) {
+                    /* capture (e.g. "Ndxf3") */
+                    move->fromfile = fileidx(mstr[1]);
+                } else {
+                    /* long notation capture (e.g. "e5xf6") */
+                    move->piece = PAWN;
+                    move->fromfile = fileidx(mstr[0]);
+                    move->fromrow = rowidx(mstr[1]);
+                }
+            } else {
+                /* long notation move (e.g. "Nc5a4") */
+                move->fromfile = fileidx(mstr[1]);
+                move->fromrow = rowidx(mstr[2]);
+            }
+            move->tofile = fileidx(mstr[3]);
+            move->torow = rowidx(mstr[4]);
+        }
+    } else if (len == 6) {
+        /* long notation capture (e.g. "Nc5xf3") */
+        if (mstr[3] == 'x') {
+            move->capture = 1;
+            move->piece = getpiece(mstr[0]);
+            move->fromfile = fileidx(mstr[1]);
+            move->fromrow = rowidx(mstr[2]);
+            move->tofile = fileidx(mstr[4]);
+            move->torow = rowidx(mstr[5]);
+        }
+    }
+
+    
+    if (move->piece) {
+        if (move->piece == PAWN
+            && move->torow == (color==WHITE?7:0)
+            && !move->promotion) {
+            return NEED_PROMOTION;
+        }
+        
+        move->piece |= color;
+        if (move->fromfile == POS_UNSPECIFIED
+            || move->fromrow == POS_UNSPECIFIED) {
+            return getlocation(gamestate, move);
+        } else {
+            return chkidx(move) ? VALID_MOVE_SYNTAX : INVALID_POSITION;
+        }
+    } else {
+        return INVALID_MOVE_SYNTAX;
+    }
+}
+
+_Bool is_protected(GameState *gamestate, uint8_t row, uint8_t file,
+        uint8_t color) {
+    
+    Move threats[16];
+    uint8_t threatcount;
+    if (get_real_threats(gamestate, row, file, color, threats, &threatcount)) {
+        for (int i = 0 ; i < threatcount ; i++) {
+            if (threats[i].piece != (color|KING)) {
+                return 1;
+            }
+        }
+        return 0;
+    } else {
+        return 0;
+    }
+}
+
+uint16_t remaining_movetime(GameInfo *gameinfo, GameState *gamestate,
+        uint8_t color) {
+    if (!gameinfo->timecontrol) {
+        return 0;
+    }
+    
+    if (gamestate->movelist) {
+        uint16_t time = gameinfo->time;
+        suseconds_t micros = 0;
+        
+        MoveList *movelist = color == WHITE ?
+            gamestate->movelist : gamestate->movelist->next;
+        
+        while (movelist) {
+            time += gameinfo->addtime;
+            
+            struct movetimeval *movetime = &(movelist->move.movetime);
+            if (movetime->tv_sec >= time) {
+                return 0;
+            }
+            
+            time -= movetime->tv_sec;
+            micros += movetime->tv_usec;
+            
+            movelist = movelist->next ? movelist->next->next : NULL;
+        }
+        
+        time_t sec;
+        movelist = gamestate->lastmove;
+        if ((movelist->move.piece & COLOR_MASK) != color) {
+            struct movetimeval *lastmovetstamp = &(movelist->move.timestamp);
+            struct timeval currenttstamp;
+            gettimeofday(&currenttstamp, NULL);
+            micros += currenttstamp.tv_usec - lastmovetstamp->tv_usec;
+            sec = currenttstamp.tv_sec - lastmovetstamp->tv_sec;
+            if (sec >= time) {
+                return 0;
+            }
+            
+            time -= sec;
+        }
+        
+        sec = micros / 1e6L;
+        
+        if (sec >= time) {
+            return 0;
+        }
+
+        time -= sec;
+        
+        return time;
+    } else {
+        return gameinfo->time;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/ctestfile.c	Tue Apr 21 09:47:52 2015 +0200
@@ -0,0 +1,386 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2015 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ucx/string.h>
+#include <ucx/buffer.h>
+#include <ucx/utils.h>
+#include <libxml/tree.h>
+#include <curl/curl.h>
+
+#include <openssl/sha.h>
+#include <openssl/hmac.h>
+#include <openssl/evp.h>
+#include <openssl/bio.h>
+#include <openssl/buffer.h>
+#include <openssl/rand.h>
+
+#include "utils.h"
+#include "crypto.h"
+#include "webdav.h"
+
+
+time_t util_parse_creationdate(char *str) {
+    // example: 2012-11-29T21:35:35Z
+    if(!str) {
+        return 0;
+    }
+    // TODO
+    return 0;
+}
+
+time_t util_parse_lastmodified(char *str) {
+    // example: Thu, 29 Nov 2012 21:35:35 GMT
+    if(!str) {
+        return 0;
+    } else {
+        return curl_getdate(str, NULL);
+    }
+}
+
+int util_getboolean(char *v) {
+    if(v[0] == 'T' || v[0] == 't') {
+        return 1;
+    }
+    return 0;
+}
+
+int util_strtoint(char *str, int64_t *value) {
+    char *end;
+    int64_t val = strtoll(str, &end, 0);
+    if(strlen(end) == 0) {
+        *value = val;
+        return 1;
+    } else {
+        return 0;
+    }
+}
+
+char* util_url_path(char *url) { 
+    char *path = NULL;
+    size_t len = strlen(url);
+    int slashcount = 0;
+    int slmax;
+    if(len > 7 && !strncasecmp(url, "http://", 7)) {
+        slmax = 3;
+    } else if(len > 8 && !strncasecmp(url, "https://", 8)) {
+        slmax = 3;
+    } else {
+        slmax = 1;
+    }
+    char c;
+    for(int i=0;i<len;i++) {
+        c = url[i];
+        if(c == '/') {
+            slashcount++;
+            if(slashcount == slmax) {
+                path = url + i;
+                break;
+            }
+        }
+    } 
+    return path;
+}
+
+char* util_url_decode(DavSession *sn, char *url) {
+    char *unesc = curl_easy_unescape(sn->handle, url, strlen(url), NULL);
+    char *ret = strdup(unesc);
+    curl_free(unesc);
+    return ret;
+}
+
+char* util_resource_name(char *url) {
+    int si = 0;
+    int osi = 0;
+    int i = 0;
+    int p = 0;
+    char c;
+    while((c = url[i]) != 0) {
+        if(c == '/') {
+            osi = si;
+            si = i;
+            p = 1;
+        }
+        i++;
+    }
+    
+    char *name = url + si + p;
+    if(name[0] == 0) {
+        name = url + osi + p;
+        if(name[0] == 0) {
+            return url;
+        }
+    }
+    
+    return name;
+}
+
+int util_mkdir(char *path, mode_t mode) {
+#ifdef _WIN32
+    return mkdir(path);
+#else
+    return mkdir(path, mode);
+#endif
+}
+
+char* util_concat_path(char *url_base, char *p) {
+    sstr_t base = sstr(url_base);
+    sstr_t path;
+    if(p) {
+        path = sstr(p);
+    } else {
+        path = sstrn("", 0);
+    }
+    
+    int add_separator = 0;
+    if(base.ptr[base.length-1] == '/') {
+        if(path.ptr[0] == '/') {
+            base.length--;
+        }
+    } else {
+        if(path.length == 0 || path.ptr[0] != '/') {
+            add_separator = 1;
+        }
+    }
+    
+    sstr_t url;
+    if(add_separator) {
+        url = sstrcat(3, base, sstr("/"), path);
+    } else {
+        url = sstrcat(2, base, path);
+    }
+    
+    return url.ptr;
+}
+
+void util_set_url(DavSession *sn, char *href) {
+    sstr_t base = sstr(sn->base_url);
+    sstr_t href_str = sstr(href);
+    
+    char *base_path = util_url_path(sn->base_url);
+    base.length -= strlen(base_path);
+    
+    sstr_t url = sstrcat(2, base, href_str);
+    
+    curl_easy_setopt(sn->handle, CURLOPT_URL, url.ptr);
+    free(url.ptr);
+}
+
+char* util_path_to_url(DavSession *sn, char *path) {
+    char *space = malloc(256);
+    UcxBuffer *url = ucx_buffer_new(space, 256, UCX_BUFFER_AUTOEXTEND);
+    
+    // add base url
+    ucx_buffer_write(sn->base_url, 1, strlen(sn->base_url), url);
+    // remove trailing slash
+    ucx_buffer_seek(url, -1, SEEK_CUR);
+    
+    sstr_t p = sstr(path);
+    ssize_t ntk = 0;
+    sstr_t *tks = sstrsplit(p, S("/"), &ntk);
+    
+    for(int i=0;i<ntk;i++) {
+        sstr_t node = tks[i];
+        if(node.length > 0) {
+            char *esc = curl_easy_escape(sn->handle, node.ptr, node.length);
+            ucx_buffer_putc(url, '/');
+            ucx_buffer_write(esc, 1, strlen(esc), url);
+            curl_free(esc);
+        }
+        free(node.ptr);
+    }
+    free(tks);
+    if(path[p.length-1] == '/') {
+        ucx_buffer_putc(url, '/');
+    }
+    ucx_buffer_putc(url, 0);
+    
+    space = url->space;
+    ucx_buffer_free(url);
+    
+    return space;
+}
+
+char* util_parent_path(char *path) {
+    char *name = util_resource_name(path);
+    size_t namelen = strlen(name);
+    size_t pathlen = strlen(path);
+    size_t parentlen = pathlen - namelen;
+    char *parent = malloc(parentlen + 1);
+    memcpy(parent, path, parentlen);
+    parent[parentlen] = '\0';
+    return parent;
+}
+
+
+char* util_xml_get_text(xmlNode *elm) {
+    xmlNode *node = elm->children;
+    while(node) {
+        if(node->type == XML_TEXT_NODE) {
+            return (char*)node->content;
+        }
+        node = node->next;
+    }
+    return NULL;
+}
+
+
+char* util_base64decode(char *in) {
+    int len = 0;
+    return util_base64decode_len(in, &len);
+}
+
+char* util_base64decode_len(char* in, int *outlen) {
+    size_t len = strlen(in);
+    char *out = calloc(1, len);
+    
+    BIO* b = BIO_new_mem_buf(in, len);
+    BIO *d = BIO_new(BIO_f_base64());
+    BIO_set_flags(d, BIO_FLAGS_BASE64_NO_NL);
+    b = BIO_push(d, b);
+
+    *outlen = BIO_read(b, out, len);
+    BIO_free_all(b);
+    
+    return out;
+}
+
+char* util_base64encode(char *in, size_t len) { 
+    BIO *b;
+    BIO *e;
+    BUF_MEM *mem;
+
+    e = BIO_new(BIO_f_base64());
+    b = BIO_new(BIO_s_mem());
+    
+    e = BIO_push(e, b);
+    BIO_write(e, in, len);
+    BIO_flush(e);
+    
+    BIO_get_mem_ptr(e, &mem);
+    char *out = malloc(mem->length);
+    memcpy(out, mem->data, mem->length -1);
+    out[mem->length - 1] = '\0';
+
+    BIO_free_all(e);
+
+    return out;
+}
+
+char* util_encrypt_str(DavSession *sn, char *str, char *key) {
+    DavKey *k = dav_context_get_key(sn->context, key);
+    if(!k) {
+        // TODO: session error
+        return NULL;
+    }
+    
+    char *enc_str = aes_encrypt(str, k);
+    char *ret_str = dav_session_strdup(sn, enc_str);
+    free(enc_str);
+    return ret_str;
+}
+
+char* util_decrypt_str(DavSession *sn, char *str, char *key) {
+    DavKey *k = dav_context_get_key(sn->context, key);
+    if(!k) {
+        // TODO: session error
+        return NULL;
+    }
+    
+    char *dec_str = aes_decrypt(str, k);
+    char *ret_str = dav_session_strdup(sn, dec_str);
+    free(dec_str);
+    return ret_str;
+}
+
+char* util_random_str() {
+    unsigned char *str = malloc(25);
+    str[24] = '\0';
+    
+    sstr_t t = S(
+            "01234567890"
+            "abcdefghijklmnopqrstuvwxyz"
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+    const unsigned char *table = (const unsigned char*)t.ptr;
+    
+    RAND_pseudo_bytes(str, 24);
+    for(int i=0;i<24;i++) {
+        int c = str[i] % t.length;
+        str[i] = table[c];
+    }
+    
+    return (char*)str;
+}
+
+/*
+ * gets a substring from 0 to the appearance of the token
+ * tokens are separated by space
+ * sets sub to the substring and returns the remaining string
+ */
+sstr_t util_getsubstr_until_token(sstr_t str, sstr_t token, sstr_t *sub) {  
+    int i;
+    int token_start = -1;
+    int token_end = -1;
+    for(i=0;i<=str.length;i++) {
+        int c;
+        if(i == str.length) {
+            c = ' ';
+        } else {
+            c = str.ptr[i];
+        }
+        if(c < 33) {
+            if(token_start != -1) {
+                token_end = i;
+                size_t len = token_end - token_start;
+                sstr_t tk = sstrsubsl(str, token_start, len);
+                //printf("token: {%.*s}\n", token.length, token.ptr);
+                if(!sstrcmp(tk, token)) {
+                    *sub = sstrtrim(sstrsubsl(str, 0, token_start));
+                    break;
+                }
+                token_start = -1;
+                token_end = -1;
+            }
+        } else {
+            if(token_start == -1) {
+                token_start = i;
+            }
+        }
+    }
+    
+    if(i < str.length) {
+        return sstrtrim(sstrsubs(str, i));
+    } else {
+        str.ptr = NULL;
+        str.length = 0;
+        return str;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/javatestfile.java	Tue Apr 21 09:47:52 2015 +0200
@@ -0,0 +1,176 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2014 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package de.uapcore.sigred.doc.base;
+
+import de.uapcore.sigred.doc.Resources;
+import de.uapcore.sigrapi.impl.Digraph;
+import de.uapcore.sigrapi.impl.Graph;
+import de.uapcore.sigrapi.IGraph;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.xerces.impl.Constants;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Namespace;
+import org.dom4j.QName;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.SAXReader;
+import org.dom4j.io.XMLWriter;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+public abstract class AbstractGraphDocument<T extends IGraph>
+        extends FileBackedDocument {
+    
+    protected static final Namespace NAMESPACE = Namespace.get("sigred",
+        "http://develop.uap-core.de/sigred/");
+    
+    private static final
+        QName TAG_GRAPHDOC = QName.get("graph-document", NAMESPACE);
+    private static final
+        QName TAG_GRAPH = QName.get("graph", NAMESPACE);
+    private static final
+        QName TAG_DIGRAPH = QName.get("digraph", NAMESPACE);
+    private static final
+        QName TAG_METADATA = QName.get("metadata", NAMESPACE);
+    
+    protected final T graph;
+    
+    private final GraphDocumentMetadata metadata;
+    
+    public AbstractGraphDocument(Class<T> graphType) {
+        T g;
+        try {
+            g = graphType.newInstance();
+        } catch (ReflectiveOperationException e) {
+            assert false;
+            g = null; // for the compiler
+        }
+        graph = g;
+        metadata = new GraphDocumentMetadata();
+    }
+
+    public T getGraph() {
+        return graph;
+    }
+    
+    public GraphDocumentMetadata getMetadata() {
+        return metadata;
+    }
+
+    protected abstract void writeGraph(Element rootNode) throws IOException;
+    protected abstract void readGraph(Element rootNode) throws IOException;
+
+    @Override
+    public void writeTo(OutputStream out) throws IOException {
+        Document doc = DocumentHelper.createDocument();
+
+        Element rootNode = doc.addElement(TAG_GRAPHDOC);
+
+        Element metadataNode = rootNode.addElement(TAG_METADATA);
+
+        metadata.write(metadataNode);
+
+        if (graph instanceof Graph) {
+            writeGraph(rootNode.addElement(TAG_GRAPH));
+        } else if (graph instanceof Digraph) {
+            writeGraph(rootNode.addElement(TAG_DIGRAPH));
+        } else {
+            throw new IOException("unsupported graph type");
+        }
+
+        XMLWriter writer = new XMLWriter(out, OutputFormat.createPrettyPrint());
+        writer.write(doc);
+        writer.flush();
+    }
+
+    @Override
+    public void readFrom(InputStream in) throws IOException {
+        try {
+            SAXReader reader = new SAXReader(true);
+            reader.setStripWhitespaceText(true);
+            
+            reader.setFeature(Constants.XERCES_FEATURE_PREFIX+
+                Constants.SCHEMA_VALIDATION_FEATURE, true);
+            reader.setProperty(Constants.XERCES_PROPERTY_PREFIX +
+                Constants.SCHEMA_LOCATION, String.format("%s %s",
+                    NAMESPACE.getURI(), Resources.class.getResource(
+                        "graph-document.xsd").toExternalForm()));
+            
+            final AtomicBoolean passed = new AtomicBoolean(true);
+            final AtomicReference<SAXParseException> xmlerror = new AtomicReference<>();
+            // TODO: we should do more detailed error handling here
+            reader.setErrorHandler(new ErrorHandler() {
+                @Override
+                public void warning(SAXParseException exception) throws SAXException {
+                }
+
+                @Override
+                public void error(SAXParseException exception) throws SAXException {
+                    xmlerror.set(exception);
+                    passed.set(false);
+                }
+
+                @Override
+                public void fatalError(SAXParseException exception) throws SAXException {
+                    xmlerror.set(exception);
+                    passed.set(false);
+                }
+                
+            });
+            Document doc = reader.read(in);
+            if (!passed.get()) {
+                // TODO: provide details (maybe via separate error object?)
+                throw xmlerror.get();
+            }
+            
+            doc.normalize();
+            
+            Element root = doc.getRootElement();
+            metadata.read(root.element(TAG_METADATA));
+            
+            if (graph instanceof Graph) {
+                readGraph(root.element(TAG_GRAPH));
+            } else if (graph instanceof Digraph) {
+                readGraph(root.element(TAG_DIGRAPH));
+            } else {
+                throw new IOException("unsupported graph type");
+            }
+        } catch (DocumentException | SAXException ex) {
+            throw new IOException(ex);
+        }
+    }
+}

mercurial