001/* A program to make a web site from a database 002 * 003 * Copyright (C) 2015 Sidney Marshall (swm@cs.rit.edu) 004 * 005 * This program is free software: you can redistribute it and/or 006 * modify it under the terms of the GNU General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013 * General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see 017 * <http://www.gnu.org/licenses/>. 018 */ 019 020import java.io.File; 021import java.io.FileWriter; 022import java.io.IOException; 023import java.sql.Connection; 024import java.sql.DriverManager; 025import java.sql.PreparedStatement; 026import java.sql.ResultSet; 027import java.sql.ResultSetMetaData; 028import java.sql.SQLException; 029import java.sql.Statement; 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.Comparator; 033 034/* 035 * java -cp sqlite-jdbc-3.7.8-20111025.014814-1.jar\;. MakeWeb 036 * use : separator on linux 037 * 038 * jar file can have class path specified 039*/ 040/* full join: 041 * 042 * SELECT * FROM table_name_1 043 * LEFT OUTER JOIN table_name_2 ON id_1 = id_2 044 * UNION SELECT * FROM table_name_2 045 * LEFT OUTER JOIN table_name_1 ON id_1 = id_2 ; 046 */ 047 048// Note that 'xxx' is a string and "xxx" is an identifier. 049 050/* ordered group_concat 051 * 052 * select parent_name, group_concat(child_name) 053 * from 054 * (select parent_name, child_name from children order by child_name) 055 * group by parent_name; 056 */ 057 058/** 059 * This class creates a web site from a database 060 */ 061public class MakeWeb { 062 static final boolean printFlag = true; 063 static Connection conn; // the database connection 064 065 /** 066 * This method implements easy access to the specified rows of a 067 * query. The method queries the database and sets up parameters to 068 * read specified columns from the returned table. If the number of 069 * specified columns is zero then all columns are read in 070 * order. This constructor will cause errors if a column name is 071 * requested that does not exist. If there is no result set then an 072 * empty result is returned allowing this method to be used for 073 * general sql statements. 074 * 075 * @param query the query used for obtaining the table 076 * @param columns the names of the columns to be returned 077 * @return an ArrayList of ArrayList of the result of a query 078 * @throws SQLException throws if problem with SQLite 079 */ 080 static ArrayList<ArrayList<String>> table(String query, String...columns) 081 throws SQLException { 082 if(query == null) throw new SQLException("null query in table"); 083 int ncolumns = columns.length; 084 ArrayList<ArrayList<String>> result = new ArrayList<ArrayList<String>>(); 085 Statement stat = conn.createStatement(); 086 try { 087 stat.execute(query); 088 } catch(SQLException e) { 089 System.out.println("Bad SQL statement in table function:"); 090 System.out.println(query); 091 throw e; 092 } 093 int count = stat.getUpdateCount(); 094 if(count >= 0) { 095 stat.close(); 096 return result; 097 } 098 ResultSet rs = stat.getResultSet(); 099 ResultSetMetaData md = rs.getMetaData(); 100 int tablecolumns = md.getColumnCount(); 101 ArrayList<Integer> columnIndex = new ArrayList<Integer>(); 102 if(columns.length > 0) { 103 ArrayList<String> columnName = new ArrayList<String>(); 104 columnName.add(""); // 0th entry 105 for(int i = 1; i <= tablecolumns; i++) { 106 columnName.add(md.getColumnName(i)); 107 } 108 iloop: for(int i = 0; i < columns.length; i++) { 109 for(int j = 1; j <= tablecolumns; j++) { 110 if(columns[i].equals(columnName.get(j))) { 111 columnIndex.add(j); 112 continue iloop; 113 } 114 } 115 columnIndex.add(null); // error - no such column name 116 } 117 } else { 118 ncolumns = tablecolumns; 119 for(int i = 0; i <= tablecolumns; i++) { 120 columnIndex.add(i + 1); 121 } 122 } 123 while(rs.next()) { 124 ArrayList<String> data = new ArrayList<String>(); 125 for(int i = 0; i < ncolumns; i++) { 126 data.add(rs.getString(columnIndex.get(i))); 127 } 128 result.add(data); 129 } 130 stat.close(); 131 return result; 132 } 133 134 /** 135 * This method creates all pages specified by the database. It 136 * assumes three tables in the database: substitutions, pagesets, 137 * and scripts. 138 * 139 * substitutions(pageset, substitutions, comment) 140 * pageset - identifies the type of page being generated (unique) 141 * substitutions - an sql statement returning a table: 142 * first field is file name or null, rest replace $1-$9 (and $$) 143 * comment - a human comment telling what scripts in this line do 144 * 145 * pagesets(pageset, position, pagepart, comment) 146 * pageset - identifies the type of page being generated 147 * position - order in which to run pageparts in a pageset 148 * pagepart - an index into the scripts table for the scripts 149 * comment - a human comment telling what scripts in this line do 150 * 151 * scripts(pagepart, position, script, comment) 152 * pagepart - identifies the part of the page being generated 153 * position - order in which to run scripts in a pagepart 154 * script - an sql statement returning a "page" segment 155 * result each row is fields concatenated to be written to file 156 * comment - a human comment telling what scripts in this line do 157 * 158 * ---------- for reference only ---------- 159 * styles(selector, property, value, comment) 160 * selector - a css selector 161 * property - a css property name 162 * value - the value of this property 163 * comment - a human comment telling where this property is used 164 * 165 * ---------- algorithm ---------- 166 * 167 * for each substitution 168 * get table of substitutions 169 * for each substitution (each table row) 170 * get and open file specified by first column (or no file for null) 171 * get matching pageset in order from pagesets and scripts 172 * for each script (after substitutions) 173 * for each line 174 * concatenate fields and write line to file 175 * close file 176 * @param pagesets pagesets to be made - if empty all pagesets are made 177 * @throws SQLException catch-all for SQLExceptions 178 * @throws IOException catch-all for IOExceptions 179 */ 180 static void make(String...pagesets) throws SQLException, IOException { 181 // we need error messages that tell where we are in the scripts 182 // Get and process pagegroups from master table substitutions 183 long totalTime = 0; 184 ArrayList<ArrayList<String>> subs = table("select * from substitutions order by pageset", 185 "pageset", "substitutions"); 186 Arrays.sort(pagesets); 187 for(ArrayList<String> subsLine : subs) { 188 String pageset = subsLine.get(0); 189 if(pagesets.length != 0 190 && Arrays.binarySearch(pagesets, pageset) < 0) continue; 191 if(printFlag) System.out.print("pageset: " + pageset + " - "); 192 long startTime = System.nanoTime(); 193 // run substitution sql to get list of substitutions for pagegroup 194 ArrayList<ArrayList<String>> subsList = table(subsLine.get(1)); 195 for(ArrayList<String> subsListLine : subsList) { 196 String filename = subsListLine.get(0); 197 FileWriter out = null; 198 if(filename != null) { 199 out = new FileWriter(filename); 200 } 201 // get scripts for pageset 202 ArrayList<ArrayList<String>> scripts 203 = table("select script from pagesets join scripts where pageset is '" + pageset + "' and pagesets.pagepart is scripts.pagepart order by pagesets.position, scripts.position"); 204 for(ArrayList<String> scriptsLine : scripts) { 205 // run substituted sql to get the tables to write out 206 if(filename != null) { 207 ArrayList<ArrayList<String>> page = table(substitute(scriptsLine.get(0), subsListLine)); 208 for(ArrayList<String> pageLine : page) { 209 // write scriptsLine concatenated 210 StringBuilder sb = new StringBuilder(); 211 for(int i = 0; i < pageLine.size(); i++) { 212 String item = pageLine.get(i); 213 if(item != null) sb.append(item); 214 } 215 sb.append('\n'); 216 out.write(sb.toString()); 217 } 218 } else { 219 String s = substitute(scriptsLine.get(0), subsListLine); 220 Statement stat = conn.createStatement(); 221 try { 222 stat.execute(s); 223 } catch(SQLException e) { 224 System.out.println("In pageset: " + pageset + " filename: " + filename + " command: " + s); 225 throw e; 226 } 227 stat.close(); 228 } 229 } 230 if(out != null) { 231 out.close(); 232 } 233 } 234 long stopTime = System.nanoTime(); 235 long milliseconds = (stopTime - startTime) / 1000000L; 236 totalTime += milliseconds; 237 if(printFlag) System.out.println(milliseconds + " msec"); 238 } 239 if(printFlag) System.out.println("Total Time = " + totalTime); 240 } 241 242 /** 243 * replaces $0-$9 in String s with subs.get(0) - subs.get(9). Also 244 * replaces $$ with $. (Note that $0 is normally replaced by the 245 * file name.) 246 * 247 * @param s the String to perform substitutions on 248 * @param subs an ArrayList specifying substitutions for $0, $1 etc. 249 * @return the string with substitutions 250 */ 251 static String substitute(String s, ArrayList<String> subs) { 252 StringBuilder sb = new StringBuilder(); 253 for(int i = 0; i < s.length(); i++) { 254 char c; 255 if((c = s.charAt(i)) != '$' || i == s.length() - 1) { 256 sb.append(c); 257 } else { 258 i++; 259 c = s.charAt(i); 260 int val = Character.digit(c, 10); 261 if(val < subs.size()) { 262 sb.append(subs.get(val)); 263 } else if(c == '$') { 264 sb.append(c); 265 } else { 266 sb.append('$'); 267 sb.append(c); 268 } 269 } 270 } 271 return sb.toString(); 272 } 273 274 /** 275 * if null return the string "null" otherwise surround the string 276 * with ' and escape ' characters by '' to allow in SQL 277 * statements. Commented out because now using ? for all variable 278 * strings and this handles nulls too. 279 * 280 * We are now using prepared statements and '?' so this method is 281 * not currently used. 282 * 283 * @param s the string to be escaped 284 * @return the escaped string 285 */ 286 /* 287 static String s(String s) { 288 if(s == null) return "null"; 289 return "'" + s.replace("'", "''") + "'"; 290 } 291 */ 292 /** 293 * An interface defining a callback for a file tree walk (used by 294 * the sitemap option) 295 */ 296 static interface FileCallBack { 297 /** 298 * @param entryid a unique reference for this file system object 299 * @param name the name of directory entry 300 * @param type 0 = file, 1 = dir, -1 = unknown (, 2 = link etc.) 301 * @param path the path from root to this file object 302 * @param parentid a unique reference of containing directory 303 * @param length length of the file 304 * @param date the modification date of the file 305 */ 306 void file(int entryid, String name, int type, String path, int parentid, long length, long date); 307 } 308 309 /** 310 * This class, given a top-level directory, creates in a database a 311 * table "sitemap" containing entries representing the file and 312 * directory structure under this top-level directory. The format of 313 * the database tables is: 314 * 315 * Command: java -cp makeweb.jar MakeWeb\$SiteMap test.db map . 316 * 317 * Table sitemap(parent, child) 318 * 319 * The top-level directory is represented by the empty string and all 320 * other files are named relative to this top-level directory 321 */ 322 static class SiteMap { 323 324 /** 325 * Gets the canonical name for a File or returns the name 326 * "UNKNOWN" 327 * 328 * @param f the file name to be canonized 329 * @return the canonical file name 330 */ 331 static String canonicalName(File f) { 332 try { 333 return f.getCanonicalPath(); 334 } catch(Exception e) { 335 e.printStackTrace(); 336 } 337 return "UNKNOWN"; 338 } 339 340 /** 341 * Return a relative path relative to the given absolute path. If 342 * the prefix is not a prefix of the absolute path just return the 343 * unmodified absolute path. 344 * 345 * @param prefix the prefix to be stripped off of the path 346 * @param absolute the path to be stripped 347 * @return the stripped path 348 */ 349 static String relativePath(String prefix, String absolute) { 350 int index = absolute.indexOf(prefix); 351 if(index == 0 && prefix.length() != absolute.length()) { 352 String suffix = absolute.substring(prefix.length()); 353 if(suffix.charAt(0) == File.separatorChar) { 354 suffix = suffix.substring(1); 355 } 356 return suffix; 357 } 358 return absolute; 359 } 360 361 String topBase; 362 int id = 0; // current unique id 363 Connection conn; // the database connection 364 String table; // the table to update 365 PreparedStatement ustat; 366 367 /** 368 * Insert into a database a file map of the specified 369 * directory. The specified table is cleared before inserting the 370 * file map. The table should be created with at least the 371 * columns: 372 * 373 * create table xxx(entryid, name, type, path, parentid, length, date) 374 * 375 * @param conn the connection to the database 376 * @param table the name of the table to use 377 * @param top the top of the file system to create a map on 378 */ 379 SiteMap(Connection conn, String table, File top) { 380 this.conn = conn; 381 this.table = table; 382 String updateQuery = "insert into " + table + 383 "(entryid, name, type, path, parentid, length, date)" + 384 "values(?1,?2,?3,?4,?5,?6,?7)"; 385 Statement stat; 386 try { 387 stat = conn.createStatement(); 388 stat.executeUpdate("begin"); 389 stat.executeUpdate("delete from " + table); 390 ustat = conn.prepareStatement(updateQuery); 391 topBase = canonicalName(top); 392 siteMap(new File(topBase), id, new DBInsert()); // top of hierarchy is 0 393 ustat.close(); 394 stat.executeUpdate("end"); 395 stat.close(); 396 } catch(SQLException e) { 397 e.printStackTrace(); 398 } 399 } 400 401 /** 402 * Extend a site map for a directory - argument must be a 403 * directory and must exist. Second argument must be a canonical 404 * name string for the parent 405 * 406 * We want to fill in a sitemap table: 407 * 408 * sitemap(entryid, name, path, parentid, length, date) 409 * 410 * @param parentDir the current directory we are working on 411 * @param parent its unique id of the directory we are working on 412 * @param callBack the callback object for dealing with a child 413 */ 414 void siteMap(File parentDir, int parent, FileCallBack callBack) { 415 File[] childFiles = parentDir.listFiles(); 416 Arrays.sort(childFiles, new Comparator<File>(){ 417 public int compare(File f1, File f2) { 418 return f1.getName().compareToIgnoreCase(f2.getName()); 419 } 420 }); 421 for(int i = 0; i < childFiles.length; i++) { 422 File childFile = new File(parentDir, childFiles[i].getName()); 423 String child = childFiles[i].getName(); 424 String fullName = relativePath(topBase, childFiles[i].getAbsolutePath()); 425 long length = childFile.length(); 426 long date = childFile.lastModified(); 427 ++id; 428 if(childFile.isFile()) { 429 callBack.file(id, child, 0, fullName, parent, length, date); 430 } else if(childFile.isDirectory()) { 431 callBack.file(id, child, 1, fullName, parent, length, date); 432 siteMap(childFile, id, callBack); 433 } else { 434 callBack.file(id, child, -1, fullName, parent, length, date); 435 } 436 } 437 } 438 439 /** 440 * insert into tablename (name1, name2) values(exp, exp) 441 * 442 * Suggest inserting: 443 * 444 * entryid - id of this entry 445 * name - text name of entry 446 * type - 0 is file, 1 is directory, -1 is unknown 447 * path - string path from top 448 * parentid - id of containing directory with top = 0 449 * length - byte length of file, 0 for directory on windows, size on linux 450 * date - milliseconds since the epoch (00:00:00 GMT, January 1, 1970) 451 */ 452 class DBInsert implements FileCallBack { 453 /* 454 query = insert into <table> (entryid, name, type, path, parentid, length, date) 455 values(?1,?2,?3,?4,?5,?6,?7) 456 */ 457 /** 458 * The prepared statement ustat is: 459 * 460 * insert into <table> (entryid, name, type, path, parentid, length, date) 461 * 462 * Insert the arguments into the database. 463 * 464 * @param entryid a unique reference for this file system object 465 * @param name the name of directory entry 466 * @param type 0 = file, 1 = dir, -1 = unknown (, 2 = link etc.) 467 * @param path the path from root to this file object 468 * @param parentid a unique reference of containing directory 469 * @param length length of the file 470 * @param date the modification date of the file 471 */ 472 public void file(int entryid, String name, int type, String path, int parentid, long length, long date) { 473 try { 474 ustat.setInt(1, entryid); 475 ustat.setString(2, name); 476 ustat.setInt(3, type); 477 ustat.setString(4, path.replace('\\', '/')); 478 ustat.setInt(5, parentid); 479 ustat.setLong(6, length); 480 ustat.setLong(7, date); 481 ustat.executeUpdate(); 482 } catch(SQLException e) { 483 e.printStackTrace(); 484 } 485 } 486 } // class DBInsert implements FileCallBack 487 488 /** 489 * arg0 = database file name<br> 490 * arg1 = database table name<br> 491 * arg2 = name of top of file system<br><br> 492 * 493 * can be called from command line by:<br><br> 494 * 495 * java -cp makeweb.jar MakeWeb\$SiteMap test.db map .<br><br> 496 * 497 * where "." can be replaced by any existing directory 498 * @param args command line arguments 499 */ 500 public static void main(String...args) { 501 Connection conn; 502 try { 503 Class.forName("org.sqlite.JDBC"); 504 conn = DriverManager.getConnection("jdbc:sqlite:" + args[0]); 505 File top = new File(args[2]); 506 new SiteMap(conn, args[1], top); 507 } catch(ClassNotFoundException ex) { 508 ex.printStackTrace(); 509 } catch(SQLException ex) { 510 ex.printStackTrace(); 511 } 512 } 513 } // class SiteMap 514 515 /** 516 * If first arg is "-map" then a sitemap structure is created in the 517 * table "sitemap". If there is a second argument it will be used as 518 * the root of the filemap, otherwise the root will be ".". 519 * 520 * All subsequent arguments are the "pagesets" to be made. If there 521 * are no subsequent arguments then all "pagesets" are made. Note 522 * that skipping some pagesets can result in errors. 523 * 524 * @param args command line arguments 525 * @throws SQLException catch-all for SQLExceptions 526 * @throws IOException catch-all for IOExceptions 527 */ 528 public static void main(String...args) throws SQLException, IOException { 529 String root = "."; 530 /* 531 if(args.length > 2) { 532 System.err.println("Usage: java -jar makeweb [-map [root]]"); 533 return; 534 } 535 */ 536 int index = 0; // where pagesets list (if any) starts 537 try { 538 Class.forName("org.sqlite.JDBC"); 539 } catch(ClassNotFoundException ex) { 540 ex.printStackTrace(); 541 } 542 conn = DriverManager.getConnection("jdbc:sqlite:" + "squaredance.db"); 543 if(args.length >= 1) { 544 if(args[0].equals("-map")) { 545 index = 1; 546 if(args.length >= 2) { 547 root = args[1]; 548 index = 2; 549 } 550 File top = new File(root); 551 new SiteMap(conn, "sitemap", top); 552 } 553 } 554 555 ArrayList<String> subs = new ArrayList<String>(); 556 for(int i = index; i < args.length; i++) { 557 subs.add(args[i]); 558 } 559 560 make(subs.toArray(new String[0])); 561 System.out.println("Done."); 562 } 563} // class MakeWeb