Newer
Older
dom-persist / source / dom_persist.d
  1. module dom_persist;
  2.  
  3. import std.file;
  4. import std.array; //https://dlang.org/phobos/std_array.html
  5. import std.conv;
  6. import std.stdio;
  7. import std.format;
  8. import std.exception : enforce;
  9. import std.traits;
  10.  
  11.  
  12. import d2sqlite3; // https://d2sqlite3.dpldocs.info/v1.0.0/d2sqlite3.database.Database.this.html
  13.  
  14. version(unittest){
  15. //stuff only compiled for unittests
  16. string sqlite_filename = "dom_persist_test.db";
  17. }
  18.  
  19. unittest {
  20. writeln( "Testing tree creation (old code)" );
  21. db_drop( sqlite_filename );
  22. assert( !db_exists( sqlite_filename ) );
  23.  
  24. Database db = db_create( sqlite_filename, 1 );
  25.  
  26. assert( db_exists( sqlite_filename ) );
  27. assert(db.tableColumnMetadata("params", "ID") == TableColumnMetadata("INTEGER", "BINARY", false, true, true));
  28.  
  29. Tree_Db_Base tdb = new Tree_Db_Base( db );
  30. tdb.db_create_schema( );
  31. assert(db.tableColumnMetadata("doctree", "ID") == TableColumnMetadata("INTEGER", "BINARY", false, true, true));
  32. long tree_id = tdb.create_tree("mytree");
  33. long nid = tdb.appendChild( tree_id, tree_id, TreeNodeType.docType, "html" );
  34. long html_nid = tdb.appendChild( tree_id, tree_id, TreeNodeType.element, "html" );
  35. long head_id = tdb.appendChild( tree_id, html_nid, TreeNodeType.element, "head" );
  36. tdb.appendChild( tree_id, head_id, TreeNodeType.comment, "This is my comment" );
  37. long body_id = tdb.appendChild( tree_id, html_nid, TreeNodeType.element, "body" );
  38. tdb.appendChild( tree_id, body_id, TreeNodeType.text, "This is some text" );
  39. tdb.appendChild( tree_id, body_id, TreeNodeType.text, " with more text" );
  40. tdb.appendChildElement( tree_id, body_id, "input" );
  41. string html_out = tdb.getTreeAsText( tree_id );
  42. //writeln( html_out );
  43. assert( html_out == "<DOCTYPE html><html><head><!--This is my comment--></head><body>This is some text with more text<input/></body></html>");
  44. db.close();
  45. }
  46.  
  47. unittest{
  48.  
  49. writeln( "Testing DocOrderIterator" );
  50.  
  51. auto db = Database( sqlite_filename );
  52. TreeNameID[] tree_list = Tree_Db.getTreeList( db );
  53. Tree_Db tree = Tree_Db.loadTree( &db, tree_list[0].tree_id );
  54. TreeNode* tree_node = tree.getTreeNode();
  55. DocOrderIterator it = new DocOrderIterator( tree_node );
  56. int i=0;
  57. TreeNode* nxt;
  58. while( (nxt=it.nextNode) !is null ){
  59. switch(i){
  60. case 0:
  61. assert( (*nxt).node_data.type == TreeNodeType.tree );
  62. break;
  63. case 1:
  64. assert( (*nxt).node_data.type == TreeNodeType.docType );
  65. break;
  66.  
  67. case 2:
  68. case 3:
  69. case 5:
  70. case 8:
  71. assert( (*nxt).node_data.type == TreeNodeType.element );
  72. break;
  73.  
  74. case 4:
  75. assert( (*nxt).node_data.type == TreeNodeType.comment );
  76. break;
  77.  
  78. case 6:
  79. case 7:
  80. assert( (*nxt).node_data.type == TreeNodeType.text );
  81. break;
  82.  
  83. default:
  84. }
  85. i+=1;
  86. }
  87. }
  88.  
  89. bool db_exists( string sqlite_filename ){
  90. return sqlite_filename.exists;
  91. }
  92.  
  93. void db_drop( string sqlite_filename ){
  94. if( sqlite_filename.exists ){
  95. sqlite_filename.remove();
  96. }
  97. }
  98.  
  99. Database db_create( string sqlite_filename, int db_ver ){
  100. auto db = Database( sqlite_filename );
  101. db.run("CREATE TABLE \"Params\"(\"ID\" INTEGER, \"Name\" TEXT NOT NULL UNIQUE, \"Val\" TEXT, PRIMARY KEY(\"ID\" AUTOINCREMENT))");
  102. db.run("Insert into params(Name, Val) values('DB_VERSION','"~to!string(db_ver)~"')");
  103. return db;
  104. }
  105.  
  106. enum TreeNodeType {
  107. nulltype=-2, // indicates a type read from the database which is not one of the recognised types
  108. tree=-1, docType, element, text, comment
  109. }
  110.  
  111. TreeNodeType getTreeNodeType( int tip ){
  112. auto tnts = [EnumMembers!TreeNodeType];
  113. foreach( tnt; tnts ){
  114. if( tnt==tip ) return tnt;
  115. }
  116. return TreeNodeType.nulltype;
  117. }
  118.  
  119.  
  120.  
  121. /**
  122. * This class provides direct database access to all trees in the database table. You can also obtain from it
  123. * an instance of class Tree_Dd for a specific tree given its root node id.
  124. */
  125. class Tree_Db_Base {
  126. protected:
  127. Database* db;
  128. static long node_count = 0;
  129. public:
  130. this( ref Database db ){
  131. this.db = &db;
  132. }
  133. /**
  134. * Create a new tree in the tree table and return the ID of the tree node. Note that the tree node is
  135. * the parent of the root node, doctype and maybe other node data.
  136. *
  137. * The tree node is marked with '0' (zero) since it has no parent.
  138. */
  139. long create_tree( string tree_name ){
  140. return appendChild( 0, 0, TreeNodeType.tree, tree_name);
  141. }
  142. /**
  143. * Read the DOM from this database for the given tree ID (tid) and return as
  144. * an html (or xml) string.
  145. *
  146. * Note that the tree node (type==0) does not have a string representation.
  147. */
  148. string getTreeAsText( long tid ){
  149. //NodeData cTree = getChild( tid ); //we don't want to print the 'tree' node
  150. return getTreeAsText_r( tid );
  151. }
  152. string getTreeAsText_r( long tid ){
  153. string strRtn = "";
  154.  
  155. NodeData[] children = getChildren( tid );
  156. foreach( child; children){
  157. strRtn ~= get_openTag_commence( child.type, child.e_data );
  158. // --> add attributes if required
  159. strRtn ~= get_openTag_end( child.type, child.e_data );
  160. strRtn ~= getTreeAsText_r( child.ID );
  161. strRtn ~= get_closeTag( child.type, child.e_data );
  162. }
  163. return strRtn;
  164. }
  165. /**
  166. * Return the (ordered) child node IDs of the given parent_id.
  167. */
  168. NodeData[] getChildren( long parent_id ){
  169. NodeData[] child_nodes;
  170. auto results = db.execute( format("select ID, e_data, p_id, t_id from doctree where p_id=%d", parent_id) );
  171. foreach (row; results){
  172. //assert(row.length == 3);
  173. child_nodes ~= NodeData(
  174. row.peek!long(0),
  175. row.peek!string(1),
  176. row.peek!long(2),
  177. getTreeNodeType( row.peek!int(3) )
  178. );
  179. }
  180. return child_nodes;
  181. }
  182.  
  183. NodeData getChild( long cid ){
  184.  
  185. auto results = db.execute( format("select ID, e_data, p_id, t_id from doctree where id=%d", cid) );
  186. foreach (row; results){
  187. //assert(row.length == 1);
  188. return NodeData(
  189. row.peek!long(0),
  190. row.peek!string(1),
  191. row.peek!long(2),
  192. getTreeNodeType( row.peek!int(3) )
  193. );
  194. }
  195. throw new Exception( format( "Child with ID(%d) not found", cid) );
  196.  
  197. }
  198.  
  199. /**
  200. * Append a new element to the given parent id (pid)
  201. */
  202. long appendChildElement( long tree_id, long pid, string elem_name ){
  203. enforce(elem_name!=null && elem_name.length>0 );
  204. return appendChild( tree_id, pid, TreeNodeType.element, elem_name );
  205. }
  206.  
  207. /**
  208. * Append new text to the given parent id (pid).
  209. * Returns the ID of the text node if appended or -1 otherwise.
  210. */
  211. long appendChildText( long tree_id, long pid, string text ){
  212. if(text==null || text.length==0 ) return -1;
  213. return appendChild( tree_id, pid, TreeNodeType.text, text );
  214. }
  215.  
  216. /**
  217. * Append new text to the given parent id (pid).
  218. * Returns the ID of the text node if appended or -1 otherwise.
  219. */
  220. long appendChildComment( long tree_id, long pid, string text ){
  221. if(text==null || text.length==0 ) return -1;
  222. return appendChild( tree_id, pid, TreeNodeType.comment, text );
  223. }
  224.  
  225. /**
  226. * Append a new node to the given parent pid.
  227. * node_data is used only for doctype, element and text
  228. *
  229. * The ID of the new node is returned.
  230. */
  231. long appendChild( long tree_id, long pid, TreeNodeType nt, string node_data = "" ){
  232. if( nt == TreeNodeType.docType ){
  233. //we might store the extra data as an attribute but this will suffice for the moment
  234. node_data = "DOCTYPE "~node_data;
  235. }
  236. 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 ) );
  237. node_count+=1;
  238. return db.lastInsertRowid;
  239. }
  240.  
  241. /**
  242. * Close this object AND the underlying DB connection.
  243. */
  244. void close(){
  245. db.close();
  246. db=null;
  247. }
  248. void db_create_schema( ){
  249. 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))");
  250. }
  251. long getRootId(){
  252. return -1;
  253. }
  254. }
  255.  
  256. string get_openTag_commence( TreeNodeType nt, string e_data ){
  257. switch( nt ){
  258. case TreeNodeType.text:
  259. return e_data;
  260. case TreeNodeType.comment:
  261. return "<!--"~e_data;
  262. default:
  263. return "<"~e_data;
  264. }
  265. }
  266.  
  267. string get_openTag_end( TreeNodeType nt, string e_data ){
  268.  
  269. switch( nt ){
  270. case TreeNodeType.comment:
  271. case TreeNodeType.text:
  272. return "";
  273. default:
  274. switch(e_data){
  275. case "input":
  276. case "br":
  277. return "/>";
  278. default:
  279. }
  280. return ">";
  281. }
  282. }
  283.  
  284. string get_closeTag( TreeNodeType nt, string e_data ){
  285.  
  286. switch( nt ){
  287. case TreeNodeType.docType:
  288. case TreeNodeType.text:
  289. return "";
  290. case TreeNodeType.comment:
  291. return "-->";
  292. default:
  293. switch(e_data){
  294. case "input":
  295. case "br":
  296. return "";
  297. default:
  298. }
  299.  
  300. return format("</%s>", e_data);
  301. }
  302. }
  303.  
  304. struct NodeData {
  305. long ID;
  306. string e_data;
  307. long pid;
  308. TreeNodeType type;
  309. bool dirty = false;
  310. this( long ID, string e_data, long pid, TreeNodeType type){
  311. this.ID = ID;
  312. this.e_data = e_data;
  313. this.pid = pid;
  314. this.type = type;
  315. }
  316. }
  317.  
  318. struct TreeNameID {
  319. long tree_id;
  320. string name;
  321. }
  322.  
  323. struct TreeNode {
  324. private:
  325. Tree_Db owner_tree;
  326. public:
  327. NodeData node_data;
  328. TreeNode*[] child_nodes;
  329. bool dirty; // true indicates a change in child_nodes
  330.  
  331. this( Tree_Db owner_tree, NodeData node_data){
  332. this.owner_tree = owner_tree;
  333. this.node_data = node_data;
  334. }
  335. /**
  336. * Returns the parent node of this node or null if no parent exists (i.e. tree-root)
  337. */
  338. TreeNode* parentNode(){
  339. if(node_data.pid==0) return null;
  340. return owner_tree.getTreeNodeById( node_data.pid );
  341. }
  342. /**
  343. * Returns the next sibling of this node or null if none exists
  344. */
  345. TreeNode* nextSibling(){
  346. if(node_data.pid==0) return null;
  347. TreeNode* p_node = owner_tree.getTreeNodeById( node_data.pid );
  348. foreach( i, c_node; p_node.child_nodes){
  349. if(c_node==&this){
  350. if( i == p_node.child_nodes.length-1) return null;
  351. return p_node.child_nodes[i+1];
  352. }
  353. }
  354. throw new Exception("Damaged tree, possibly incorrect parent id for a child.");
  355. }
  356.  
  357. bool hasChildNodes(){
  358. return child_nodes.length>0;
  359. }
  360.  
  361. TreeNode* firstChild(){
  362. if( child_nodes.length==0 ) return null;
  363. return child_nodes[0];
  364. }
  365.  
  366. /**
  367. * Set the data for this node.
  368. *
  369. * The data is interpreted using the type of node. For example, the text content of a text node is
  370. * the data whereas for element types, the data is used to hold the element name.
  371. */
  372. void setData( string nData ){
  373. node_data.e_data = nData;
  374. node_data.dirty = true;
  375. }
  376. /**
  377. * Insert a new child node at the position indicated.
  378. */
  379. void insertChild( TreeNodeType n_type, string e_data, int pos ){
  380. owner_tree.insertChild( &this, n_type, e_data, pos );
  381. }
  382. }
  383.  
  384. /**
  385. * An instance of this class contains access to a single tree. Tree operations are cached in RAM and only written to
  386. * disk during a save operation. This can be done safely because of the single user access to Sqlite.
  387. *
  388. * Multiple database connections should work on the same thread provided each is using a different tree.
  389. *
  390. * Instantiation of the tree involves only one database select.
  391. */
  392. class Tree_Db {
  393.  
  394. protected:
  395. long tree_id;
  396. Database* db;
  397. long nnid = 0;
  398. TreeNode[long] all_nodes;
  399. this( Database* db, long tid ){
  400. this.db = db;
  401. tree_id = tid;
  402. //load the tree in one hit using the tree_id
  403. //also order by the parent_id so that we know all siblings are grouped together
  404. //and then by child order
  405. 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", tid, tid) );
  406. foreach (row; results){
  407. long id = row.peek!long(0);
  408. long p_id = row.peek!long(2);
  409. TreeNode tn = TreeNode( this, NodeData(
  410. id,
  411. row.peek!string(1),
  412. p_id,
  413. getTreeNodeType( row.peek!int(3) )
  414. ));
  415. all_nodes[id] = tn;
  416. if(p_id==0) continue;
  417. all_nodes[p_id].child_nodes ~= &all_nodes[id];
  418. }
  419. }
  420. public:
  421. /**
  422. * Return a list of all trees in the database with their ID's and names.
  423. */
  424. static TreeNameID[] getTreeList( ref Database db ){
  425.  
  426. TreeNameID[] tree_list;
  427. auto results = db.execute( format("select ID, e_data from doctree where p_id=0") );
  428. foreach (row; results){
  429. tree_list ~= TreeNameID (
  430. row.peek!long(0),
  431. row.peek!string(1)
  432. );
  433. }
  434. return tree_list;
  435. }
  436. static Tree_Db loadTree( Database* db, long tid ){
  437. return new Tree_Db( db, tid );
  438. }
  439. long getNextNodeId(){
  440. nnid -= 1;
  441. return nnid;
  442. }
  443. TreeNode* getTreeNode(){
  444. return &all_nodes[tree_id];
  445. }
  446.  
  447. TreeNode* getTreeNodeById( long id ){
  448. return &all_nodes[id];
  449. }
  450.  
  451. /**
  452. * Return the tree as html text. This is the cached tree with all changes and not from the database.
  453. */
  454. string getTreeAsText( ){
  455. return getTreeAsText_r( all_nodes[tree_id] );
  456. }
  457.  
  458.  
  459. /**
  460. * Insert a new child of parent node (p_node) at the position (pos) indicated.
  461. */
  462. void insertChild( TreeNode* p_node, TreeNodeType n_type, string e_data, int pos ){
  463. /* Insert new node:
  464. add into the correct place, assign a new id <=-1.
  465. An ID is required to add to the map. Using negative IDs indicates that it is not a DB ID.
  466. Increment the order_id of following sibling nodes
  467. Mark the TreeNode parent as dirty indicating that children need adding and re-ordering*/
  468. long id = getNextNodeId();
  469. TreeNode tn = TreeNode(
  470. this,
  471. NodeData( id, e_data, p_node.node_data.ID, n_type)
  472. );
  473. this.all_nodes[ id ] = tn;
  474. p_node.child_nodes.insertInPlace( pos, &this.all_nodes[ id ]);
  475. p_node.dirty = true;
  476. }
  477.  
  478.  
  479. /**
  480. * Save all edits to the tree to the database. After this call, the database values will be in sync
  481. * with the memory tree.
  482. */
  483. void flush(){
  484. // nodes must be written in document order so that a valid database ID is always available as a parent
  485. //ID for subsequent child writes. This is because the child update will write
  486.  
  487. DocOrderIterator it = new DocOrderIterator( &all_nodes[tree_id] );
  488. TreeNode* tnode;
  489. while( (tnode=it.nextNode) !is null ){
  490. NodeData nd = tnode.node_data;
  491. //first we check the ID<0 which implies it is a new node
  492. if( nd.ID<0 ){
  493. writeln("todo create:", nd );
  494. }else if( nd.dirty ){
  495. db.run( format("update doctree set e_data='%s' where id=%d", nd.e_data, nd.ID ) );
  496. nd.dirty = false;
  497. }
  498. }
  499.  
  500. }
  501.  
  502. protected:
  503. string getTreeAsText_r( ref TreeNode tn ){
  504. string strRtn = "";
  505.  
  506. TreeNode*[] children = tn.child_nodes;
  507. foreach( child; children){
  508. NodeData nd = child.node_data;
  509. strRtn ~= get_openTag_commence( nd.type, nd.e_data );
  510. // --> add attributes if required
  511. strRtn ~= get_openTag_end( nd.type, nd.e_data );
  512. strRtn ~= getTreeAsText_r( *child );
  513. strRtn ~= get_closeTag( nd.type, nd.e_data );
  514. }
  515. return strRtn;
  516. }
  517. }
  518. /*
  519. Insert new node:
  520. add into the correct place, assign a new id <=-1.
  521. An ID is required to add to the map. Using negative IDs indicates that it is not a DB ID.
  522. Increment the order_id of following sibling nodes
  523. Mark the TreeNode parent as dirty indicating that children need adding and re-ordering
  524.  
  525. Delete node (branch)
  526. If the id<=-1, then it was a new node, unsaved, can be removed entirely
  527. Otherwise, move the node and all children into a delete-map.
  528. Mark the TreeNode parent as dirty indicating that children need removing. Re-ordering is
  529. not required but may be advantageous
  530. Move node
  531. Move the node and all children into new parent (same parent also works)
  532. Mark old parent TreeNode as dirty indicating a re-order is necessary, re-order ram children
  533. Mark new parent TreeNode as dirty indicating a re-order is necessary, re-order ram children
  534. update parent id of moved child
  535.  
  536. Save/flush
  537. Work through the map and check for dirty flags. The order does not matter since the tree is
  538. the final state to which the db needs to match.
  539. Edit DB and reset flags.
  540. Delete entries using the delete-map and clear.
  541. */
  542. unittest{
  543.  
  544. writeln( "Testing tree loading" );
  545.  
  546. auto db = Database( sqlite_filename );
  547. TreeNameID[] tree_list = Tree_Db.getTreeList( db );
  548. assert( tree_list.length==1 );
  549. Tree_Db tree = Tree_Db.loadTree( &db, tree_list[0].tree_id );
  550. TreeNode* tree_node = tree.getTreeNode();
  551. NodeData nd_t = tree_node.node_data;
  552. assert( nd_t.ID == tree_list[0].tree_id );
  553. assert( nd_t.e_data == tree_list[0].name );
  554. assert( nd_t.pid == 0 );
  555. assert( nd_t.type == TreeNodeType.tree );
  556.  
  557. TreeNode* html_node;
  558. int i=0;
  559. foreach( node_ptr; tree_node.child_nodes ){
  560.  
  561. NodeData c_node = (*node_ptr).node_data;
  562. switch(i){
  563. case 0:
  564. assert( c_node.ID == 2 );
  565. assert( c_node.e_data == "DOCTYPE html" );
  566. assert( c_node.pid == tree_list[0].tree_id );
  567. assert( c_node.type == TreeNodeType.docType );
  568. break;
  569.  
  570. case 1:
  571. html_node = node_ptr;
  572. assert( c_node.ID == 3 );
  573. assert( c_node.e_data == "html" );
  574. assert( c_node.pid == tree_list[0].tree_id );
  575. assert( c_node.type == TreeNodeType.element );
  576. break;
  577. default:
  578. }
  579. i+=1;
  580. }
  581. string html_out = tree.getTreeAsText( );
  582. assert( html_out == "<DOCTYPE html><html><head><!--This is my comment--></head><body>This is some text with more text<input/></body></html>");
  583.  
  584. writeln( "Testing tree element insertion" );
  585.  
  586. //get head element
  587. TreeNode* tn_head = html_node.child_nodes[0];
  588. //add an element to the head at position zero
  589. tn_head.insertChild( TreeNodeType.element, "script", 0 );
  590. html_out = tree.getTreeAsText( );
  591. assert( html_out == "<DOCTYPE html><html><head><script></script><!--This is my comment--></head><body>This is some text with more text<input/></body></html>");
  592.  
  593. writeln( "Testing tree editing" );
  594.  
  595. //edit the comment node (id=5)
  596. TreeNode* tn = tree.getTreeNodeById( 5 );
  597. tn.setData( "An edit took place");
  598. html_out = tree.getTreeAsText( );
  599. assert( html_out == "<DOCTYPE html><html><head><script></script><!--An edit took place--></head><body>This is some text with more text<input/></body></html>");
  600.  
  601. //check that the database entry is unchanged
  602. auto results = db.execute( "select e_data from doctree where id=5" );
  603. foreach (row; results){
  604. assert( "This is my comment" == row.peek!string(0) );
  605. }
  606. // save to database
  607. tree.flush();
  608. //check db contents using a new tree
  609. /*Tree_Db tree2 = Tree_Db.loadTree( &db, tree_list[0].tree_id );
  610. html_out = tree2.getTreeAsText( );
  611. assert( html_out == "<DOCTYPE html><html><head><script></script><!--An edit took place--></head><body>This is some text with more text<input/></body></html>");
  612. */
  613. }
  614.  
  615.  
  616. /**
  617. * Iterator starting at any given TreeNode, traversing each of the descendent nodes, depth first and increasing child index.
  618. */
  619. class DocOrderIterator {
  620. TreeNode* start_node;
  621. TreeNode* next_node;
  622. this( TreeNode* n ){
  623. start_node = n;
  624. next_node = n;
  625. }
  626. /**
  627. * The initial TreeNode is the first node to be returned.
  628. */
  629. TreeNode* nextNode(){
  630. //the node we will return this time
  631. TreeNode* rtnNode = next_node;
  632. if(rtnNode==null) return null;
  633. //now work out the node for the next call
  634. if( rtnNode.hasChildNodes() ){
  635. next_node = rtnNode.firstChild();
  636. return rtnNode;
  637. }
  638. TreeNode* anc_node = rtnNode;
  639. while( anc_node !is null && anc_node.nextSibling() is null){
  640. anc_node = anc_node.parentNode();
  641. if( anc_node == start_node ){
  642. anc_node=null;
  643. break;
  644. }
  645. }
  646. if(anc_node is null) {
  647. next_node=null;
  648. return rtnNode;
  649. }
  650. next_node = anc_node.nextSibling();
  651. return rtnNode;
  652. }
  653. }