Newer
Older
dom-persist / source / dom_persist.d
module dom_persist;

import std.file;
import std.conv;
import std.stdio;
import std.format;
import std.exception : enforce;
import std.traits;


import d2sqlite3;	// https://d2sqlite3.dpldocs.info/v1.0.0/d2sqlite3.database.Database.this.html

version(unittest){	
	//stuff only compiled for unittests
	string sqlite_filename = "dom_persist_test.db";
}

unittest {
	
	writeln( "Testing tree creation (old code)" );
	
	db_drop( sqlite_filename );
	assert( !db_exists( sqlite_filename ) );

	Database db = db_create( sqlite_filename, 1 );

	assert( db_exists( sqlite_filename ) );	
	assert(db.tableColumnMetadata("params", "ID") == TableColumnMetadata("INTEGER", "BINARY", false, true, true));

	Tree_Db_Base tdb = new Tree_Db_Base( db );
	tdb.db_create_schema( );
	
	assert(db.tableColumnMetadata("doctree", "ID") == TableColumnMetadata("INTEGER", "BINARY", false, true, true));
	
	long tree_id = tdb.create_tree("mytree");
	
	long nid = tdb.appendChild( tree_id, tree_id, TreeNodeType.docType, "html" );
	long html_nid = tdb.appendChild( tree_id, tree_id, TreeNodeType.element, "html" );
	
	long head_id = tdb.appendChild( tree_id, html_nid, TreeNodeType.element, "head" );
	tdb.appendChild( tree_id, head_id, TreeNodeType.comment, "This is my comment" );
	
	long body_id = tdb.appendChild( tree_id, html_nid, TreeNodeType.element, "body" );
	tdb.appendChild( tree_id, body_id, TreeNodeType.text, "This is some text" );
	tdb.appendChild( tree_id, body_id, TreeNodeType.text, " with more text" );
	tdb.appendChildElement( tree_id, body_id, "input" );
	
	string html_out = tdb.getTreeAsText( tree_id );	
	//writeln( html_out );
	assert( html_out == "<DOCTYPE html><html><head><!--This is my comment--></head><body>This is some text with more text<input/></body></html>");
	
	db.close();
	
}

bool db_exists( string sqlite_filename ){
	return sqlite_filename.exists;
}

void db_drop( string sqlite_filename ){
	if( sqlite_filename.exists ){
		sqlite_filename.remove();
	}	
}

Database db_create( string sqlite_filename, int db_ver ){
	
	auto db = Database( sqlite_filename );
	db.run("CREATE TABLE \"Params\"(\"ID\" INTEGER, \"Name\"	TEXT NOT NULL UNIQUE, \"Val\"	TEXT,	PRIMARY KEY(\"ID\" AUTOINCREMENT))");
	db.run("Insert into params(Name, Val) values('DB_VERSION','"~to!string(db_ver)~"')");
	return db;
}

enum TreeNodeType {
	nulltype=-2, // indicates a type read from the database which is not one of the recognised types
	tree=-1, docType, element, text, comment
}

TreeNodeType getTreeNodeType( int tip ){
	
	auto tnts = [EnumMembers!TreeNodeType];
	foreach( tnt; tnts ){
		if( tnt==tip ) return tnt;
	}
	return TreeNodeType.nulltype;
}


struct NodeData {
	
	long ID;
	string e_data;
	long pid;
	TreeNodeType type;
	
	this( long ID, string e_data, long pid, TreeNodeType type){
		this.ID = ID;
		this.e_data = e_data;
		this.pid = pid;
		this.type = type;
	}
}

/**
 * This class provides direct database access to all trees in the database table. You can also obtain from it
 * an instance of class Tree_Dd for a specific tree given its root node id.
 */
class Tree_Db_Base {
	
	protected:
		Database* db;
		static long node_count = 0;
		
	public:
	
	this( ref Database db ){
		this.db = &db;
	}
	
	/**
	 * Create a new tree in the tree table and return the ID of the tree node. Note that the tree node is
	 * the parent of the root node, doctype and maybe other node data.
	 * 
	 * The tree node is marked with '0' (zero) since it has no parent.
	 */
	long create_tree( string tree_name ){		
		return appendChild( 0, 0, TreeNodeType.tree, tree_name);
	}
	
	/**
	 * Read the DOM from this database for the given tree ID (tid) and return as
	 * an html (or xml) string.
	 * 
	 * Note that the tree node (type==0) does not have a string representation.
	 */
	string getTreeAsText( long tid ){
		//NodeData cTree = getChild( tid );	//we don't want to print the 'tree' node
		return getTreeAsText_r( tid );
	}
	
	string getTreeAsText_r( long tid ){
		
		string strRtn = "";

		NodeData[] children = getChildren( tid );
		foreach( child; children){
			strRtn ~= get_openTag_commence( child.type, child.e_data );
			// --> add attributes if required
			strRtn ~= get_openTag_end( child.type, child.e_data );
			strRtn ~= getTreeAsText_r( child.ID );
			strRtn ~= get_closeTag( child.type, child.e_data );
		}
		
		return strRtn;
	}
	
	/**
	 * Return the (ordered) child node IDs of the given parent_id.
	 */
	NodeData[] getChildren( long parent_id ){
	
		NodeData[] child_nodes;
		auto results = db.execute( format("select ID, e_data, p_id, t_id from doctree where p_id=%d", parent_id) );
		foreach (row; results){
			
			//assert(row.length == 3);
			
			child_nodes ~= NodeData( 
				row.peek!long(0),
				row.peek!string(1),
				row.peek!long(2),
				getTreeNodeType( row.peek!int(3) )
			);
			
		}
		return child_nodes;
	}

	NodeData getChild( long cid ){

		auto results = db.execute( format("select ID, e_data, p_id, t_id from doctree where id=%d", cid) );
		foreach (row; results){
			
			//assert(row.length == 1);
			
			return NodeData( 
				row.peek!long(0),
				row.peek!string(1),
				row.peek!long(2),
				getTreeNodeType( row.peek!int(3) )
			);
			
		}
		throw new Exception( format( "Child with ID(%d) not found", cid) );

	}

	/**
	 * Append a new element to the given parent id (pid)
	 */
	long appendChildElement( long tree_id, long pid, string elem_name ){
		enforce(elem_name!=null && elem_name.length>0 );
		return appendChild( tree_id, pid, TreeNodeType.element, elem_name );
	}

	/**
	 * Append new text to the given parent id (pid).
	 * Returns the ID of the text node if appended or -1 otherwise.
	 */
	long appendChildText( long tree_id, long pid, string text ){
		if(text==null || text.length==0 ) return -1;
		return appendChild( tree_id, pid, TreeNodeType.text, text );
	}

	/**
	 * Append new text to the given parent id (pid).
	 * Returns the ID of the text node if appended or -1 otherwise.
	 */
	long appendChildComment( long tree_id, long pid, string text ){
		if(text==null || text.length==0 ) return -1;
		return appendChild( tree_id, pid, TreeNodeType.comment, text );
	}

	/**
	 * Append a new node to the given parent pid.
	 * node_data is used only for doctype, element and text
	 * 
	 * The ID of the new node is returned.
	 */
	long appendChild( long tree_id, long pid, TreeNodeType nt, string node_data = "" ){
		
		if( nt == TreeNodeType.docType ){
			//we might store the extra data as an attribute but this will suffice for the moment
			node_data = "DOCTYPE "~node_data;
		}
		db.run( format("Insert into doctree(e_data, p_id, t_id, tree_id, c_odr ) values( '%s', %d, %d, %d, %d )", node_data, pid, nt, tree_id, node_count ) );
		node_count+=1;
		return db.lastInsertRowid;		
	}

	/**
	 * Close this object AND the underlying DB connection.
	 */
	void close(){
		db.close();
		db=null;
	}
	
	void db_create_schema( ){		
		db.run("CREATE TABLE IF NOT EXISTS doctree (ID INTEGER, e_data	TEXT,p_id INTEGER,t_id INTEGER NOT NULL,tree_id INTEGER NOT NULL,	c_odr INTEGER NOT NULL, PRIMARY KEY( ID AUTOINCREMENT))");
	}
	
	long getRootId(){
		return -1;
	}
	
}

string get_openTag_commence( TreeNodeType nt, string e_data ){
	
	switch( nt ){
	
	case TreeNodeType.text:
		return e_data;
		
	case TreeNodeType.comment:
		return "<!--"~e_data;
		
	default:
		return "<"~e_data;
	}
}

string get_openTag_end( TreeNodeType nt, string e_data ){

	switch( nt ){
		
	case TreeNodeType.comment:
	case TreeNodeType.text:
		return "";
			
	default:
		switch(e_data){
		case "input":
		case "br":
			return "/>";
		default:
		}
		
		return ">";
	}
	
}

string get_closeTag( TreeNodeType nt, string e_data ){

	switch( nt ){
		
	case TreeNodeType.docType:
	case TreeNodeType.text:
		return "";
		
	case TreeNodeType.comment:
		return "-->";
		
	default:
		switch(e_data){
		case "input":
		case "br":
			return "";
		default:
		}

		return format("</%s>", e_data);
	}
	
}


struct TreeNameID {
	long tree_id;
	string name;
}

struct TreeNode {
	
	NodeData node_data;
	TreeNode*[] child_nodes;

	this(NodeData node_data){
		this.node_data = node_data;
	}
	
}

/**
 * An instance of this class contains access to a single tree. Tree operations are cached in RAM and only written to
 * disk during a save operation. This can be done safely because of the single user access to Sqlite.
 * 
 * Multiple database connections should work on the same thread provided each is using a different tree.
 * 
 * Instantiation of the tree involves only one database select.
 */
class Tree_Db {

	protected:
	
		long tree_id;
		Database* db;		
		
		TreeNode[long]	all_nodes;
		
		
	this( Database db, long tid ){

		this.db = &db;
		tree_id = tid;

		//load the tree in one hit using the tree_id
		//also order by the parent_id so that we know all siblings are grouped together
		//and then by child order

		auto results = db.execute( format("select ID, e_data, p_id, t_id from doctree where tree_id=%d or id=%d order by p_id,c_odr", tree_id, tree_id) );
		foreach (row; results){
			
			long id = row.peek!long(0);
			long p_id = row.peek!long(2);
			TreeNode tn = TreeNode( NodeData(
				id,
				row.peek!string(1),
				p_id,
				getTreeNodeType( row.peek!int(3) )
			));
			all_nodes[id] = tn;
			if(p_id==0) continue;
			all_nodes[p_id].child_nodes ~= &all_nodes[id];
		}

	}
	
	public:
	
	/**
	 * Return a list of all trees in the database with their ID's and names.
	 */
	static TreeNameID[] getTreeList( ref Database db ){

		TreeNameID[] tree_list;
		auto results = db.execute( format("select ID, e_data from doctree where p_id=0") );
		foreach (row; results){
			
			tree_list ~= TreeNameID (
				row.peek!long(0),
				row.peek!string(1)
			);			
		}
		return tree_list;		
	}
	
	static Tree_Db loadTree( ref Database db, long tid ){
		return new Tree_Db( db, tid );
	}
	
	TreeNode getTreeNode(){
		return all_nodes[tree_id];
	}

	string getTreeAsText( ){
		return getTreeAsText_r( all_nodes[tree_id] );
	}


	protected:
	
	string getTreeAsText_r( ref TreeNode tn ){
		
		string strRtn = "";

		TreeNode*[] children = tn.child_nodes;
		foreach( child; children){
			NodeData nd = child.node_data;
			strRtn ~= get_openTag_commence( nd.type, nd.e_data );
			// --> add attributes if required
			strRtn ~= get_openTag_end( nd.type, nd.e_data );
			strRtn ~= getTreeAsText_r( *child );
			strRtn ~= get_closeTag( nd.type, nd.e_data );
		}
		
		return strRtn;
	}
	
}
/*
 
 Edit node:
	No attributes at this stage so only editing the e_data field.
	Add dirty field to NodeData and mark accordingly
	
 Insert new node:
	add into the correct place, assign a new id <=-1. 
	An ID is required to add to the map. Using negative IDs indicates that it is not a DB ID.
	Increment the order_id of following sibling nodes
	Mark the TreeNode parent as dirty indicating that children need adding and re-ordering

 Delete node (branch)
	If the id<=-1, then it was a new node, unsaved, can be removed entirely
	Otherwise, move the node and all children into a delete-map.	
	Mark the TreeNode parent as dirty indicating that children need removing. Re-ordering is
	not required but may be advantageous
	
 Move node
	Move the node and all children into new parent (same parent also works)
	Mark old parent TreeNode as dirty indicating a re-order is necessary, re-order ram children
	Mark new parent TreeNode as dirty indicating a re-order is necessary, re-order ram children
	update parent id of moved child
	
 */ 
 
unittest{

	writeln( "Testing tree loading" );

	auto db = Database( sqlite_filename );
		
	TreeNameID[] tree_list = Tree_Db.getTreeList( db );
	assert( tree_list.length==1 );
	
	Tree_Db tree = Tree_Db.loadTree( db, tree_list[0].tree_id );
	
	TreeNode tree_node = tree.getTreeNode();
	NodeData nd_t = tree_node.node_data;
	assert( nd_t.ID == tree_list[0].tree_id );
	assert( nd_t.e_data == tree_list[0].name );
	assert( nd_t.pid == 0 );
	assert( nd_t.type == TreeNodeType.tree );

	TreeNode* html_node;
	
	int i=0;
	foreach( node_ptr; tree_node.child_nodes ){		

		NodeData c_node = (*node_ptr).node_data;
		
		switch(i){
		case 0:
			assert( c_node.ID == 2 );
			assert( c_node.e_data == "DOCTYPE html" );
			assert( c_node.pid == tree_list[0].tree_id );
			assert( c_node.type == TreeNodeType.docType );
			break;

		case 1:
			html_node = node_ptr;
			assert( c_node.ID == 3 );
			assert( c_node.e_data == "html" );
			assert( c_node.pid == tree_list[0].tree_id );
			assert( c_node.type == TreeNodeType.element );
			break;
		
		default:
		}
	
		i+=1;
	}
	
	string html_out = tree.getTreeAsText( );
	assert( html_out == "<DOCTYPE html><html><head><!--This is my comment--></head><body>This is some text with more text<input/></body></html>");

		

}