001/* 002 * Copyright (c) 1998-2010 The Regents of the University of California. 003 * All rights reserved. 004 * 005 * '$Author: crawl $' 006 * '$Date: 2016-06-27 19:15:31 +0000 (Mon, 27 Jun 2016) $' 007 * '$Revision: 34499 $' 008 * 009 * Permission is hereby granted, without written agreement and without 010 * license or royalty fees, to use, copy, modify, and distribute this 011 * software and its documentation for any purpose, provided that the above 012 * copyright notice and the following two paragraphs appear in all copies 013 * of this software. 014 * 015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 019 * SUCH DAMAGE. 020 * 021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 026 * ENHANCEMENTS, OR MODIFICATIONS. 027 * 028 */ 029 030package org.sdm.spa; 031 032import java.sql.PreparedStatement; 033import java.sql.ResultSet; 034import java.sql.SQLException; 035import java.sql.Statement; 036import java.util.LinkedHashMap; 037import java.util.LinkedList; 038import java.util.List; 039 040import org.geon.DatabaseAccessor; 041import org.jdom.Document; 042import org.jdom.Element; 043import org.jdom.input.DOMBuilder; 044 045import ptolemy.actor.TypedIOPort; 046import ptolemy.actor.parameters.PortParameter; 047import ptolemy.data.ArrayToken; 048import ptolemy.data.BooleanToken; 049import ptolemy.data.DateToken; 050import ptolemy.data.DoubleToken; 051import ptolemy.data.IntToken; 052import ptolemy.data.LongToken; 053import ptolemy.data.MatrixToken; 054import ptolemy.data.RecordToken; 055import ptolemy.data.StringToken; 056import ptolemy.data.Token; 057import ptolemy.data.XMLToken; 058import ptolemy.data.expr.Parameter; 059import ptolemy.data.type.BaseType; 060import ptolemy.data.type.Type; 061import ptolemy.kernel.CompositeEntity; 062import ptolemy.kernel.util.IllegalActionException; 063import ptolemy.kernel.util.NameDuplicationException; 064import ptolemy.kernel.util.Workspace; 065 066////////////////////////////////////////////////////////////////////////// 067//// DatabaseWriter 068 069/** 070 * This actor performs database updates and returns the number of updated rows. 071 * If <i>input</i> is an SQL string, the SQL is run on the database. 072 * <p> 073 * If the input is a record token, the name-value pairs are inserted into the 074 * table specfied by <i>table</i>. Optionally, auto-increments for a column will 075 * be done when <i>autoIncColumnName</i> is specified; the incremented value is 076 * output in <i>autoIncValue</i>. 077 * </p> 078 * 079 * @author Yang Zhao, Daniel Crawl 080 * @version $Id: DatabaseWriter.java 34499 2016-06-27 19:15:31Z crawl $ 081 */ 082 083public class DatabaseWriter extends DatabaseAccessor { 084 085 /** 086 * Construct an actor with the given container and name. 087 * 088 * @param container 089 * The container. 090 * @param name 091 * The name of this actor. 092 * @exception IllegalActionException 093 * If the actor cannot be contained by the proposed 094 * container. 095 * @exception NameDuplicationException 096 * If the container already has an actor with this name. 097 */ 098 public DatabaseWriter(CompositeEntity container, String name) 099 throws IllegalActionException, NameDuplicationException { 100 super(container, name); 101 102 input = new PortParameter(this, "input"); 103 104 table = new PortParameter(this, "table"); 105 table.setStringMode(true); 106 table.getPort().setTypeEquals(BaseType.STRING); 107 108 autoIncColumnName = new PortParameter(this, "autoIncColumnName"); 109 autoIncColumnName.setStringMode(true); 110 autoIncColumnName.getPort().setTypeEquals(BaseType.STRING); 111 112 autoIncValue = new TypedIOPort(this, "autoIncValue", false, true); 113 autoIncValue.setTypeEquals(BaseType.INT); 114 115 usePreparedStatement = new Parameter(this, "usePreparedStatement"); 116 usePreparedStatement.setTypeEquals(BaseType.BOOLEAN); 117 usePreparedStatement.setToken(BooleanToken.FALSE); 118 119 // the output 120 result = new TypedIOPort(this, "result", false, true); 121 result.setTypeEquals(BaseType.INT); 122 123 _domBuilder = new DOMBuilder(); 124 } 125 126 // ///////////////////////////////////////////////////////////////// 127 // // ports and parameters //// 128 129 /** The input for the update. */ 130 public PortParameter input; 131 132 /** The number of rows successfully updated. */ 133 public TypedIOPort result; 134 135 /** Name of table. */ 136 public PortParameter table; 137 138 /** Name of column to auto-increment. */ 139 public PortParameter autoIncColumnName; 140 141 /** Auto-increment value. */ 142 public TypedIOPort autoIncValue; 143 144 /** If true, use a prepared statement instead of statement to perform 145 * the SQL command. Currently only supported for record token inputs. 146 */ 147 public Parameter usePreparedStatement; 148 149 // ///////////////////////////////////////////////////////////////// 150 // // public methods //// 151 152 @Override 153 public Object clone(Workspace workspace) throws CloneNotSupportedException { 154 DatabaseWriter newObject = (DatabaseWriter) super.clone(workspace); 155 newObject._domBuilder = new DOMBuilder(); 156 return newObject; 157 } 158 159 /** 160 * Read a string token for the query from the input port, execute it on the 161 * database and output the query result. 162 * 163 * @exception IllegalActionException 164 * If there is error to execute the query or if the base 165 * class throw it. 166 */ 167 @Override 168 public void fire() throws IllegalActionException { 169 super.fire(); 170 171 String cmd = ""; 172 int incVal = -1; 173 174 175 final boolean usePreparedStatementVal = 176 ((BooleanToken)usePreparedStatement.getToken()).booleanValue(); 177 178 try { 179 180 int nresult = -1; 181 182 // read the input 183 input.update(); 184 Token token = input.getToken(); 185 Type type = token.getType(); 186 187 if (_debugging) { 188 _debug("input: " + token); 189 } 190 191 // see what kind of input it is 192 if (type == BaseType.STRING) { 193 cmd = ((StringToken) token).stringValue(); 194 } else if (type == BaseType.XMLTOKEN 195 || token instanceof RecordToken) { 196 LinkedHashMap<String, String> map = null; 197 String tableName; 198 199 if (type == BaseType.XMLTOKEN) { 200 org.w3c.dom.Document d = ((XMLToken) token).getDomTree(); 201 Document doc = _domBuilder.build(d); 202 Element root = doc.getRootElement(); 203 tableName = root.getName(); 204 map = _parseXMLCmd(doc); 205 } else if(token instanceof RecordToken) { 206 table.update(); 207 tableName = ((StringToken) table.getToken()).stringValue(); 208 209 if(usePreparedStatementVal) { 210 nresult = _insertRecord(tableName, (RecordToken)token); 211 } else { 212 map = _parseRecordCmd((RecordToken) token); 213 } 214 } else { 215 throw new IllegalActionException(this, 216 "Unsupported type of input token: " + token.getType()); 217 } 218 219 if (tableName.equals("")) { 220 throw new IllegalActionException(this, 221 "No value for required table name"); 222 } 223 224 225 if(!usePreparedStatementVal) { 226 // check for auto increment column name 227 incVal = _checkAutoIncColumn(tableName, map); 228 cmd = _convertMapToSQLInsert(tableName, map); 229 } 230 231 } else { 232 throw new IllegalActionException(this, 233 "Unknown type of token in updateSQL: " + type); 234 } 235 236 if(!usePreparedStatementVal) { 237 if (_debugging) { 238 _debug("sql command: " + cmd); 239 } 240 241 try(Statement st = _db.createStatement();) { 242 nresult = st.executeUpdate(cmd); 243 result.broadcast(new IntToken(nresult)); 244 } 245 } 246 247 if (incVal > -1) { 248 autoIncValue.broadcast(new IntToken(incVal)); 249 } 250 } catch (SQLException e) { 251 throw new IllegalActionException(this, e, 252 "failed to execute the command: " + cmd); 253 } 254 } 255 256 // ///////////////////////////////////////////////////////////////// 257 // // private methods //// 258 259 /** Convert an XML document into a map. */ 260 private LinkedHashMap<String, String> _parseXMLCmd(Document doc) { 261 LinkedHashMap<String, String> retval = new LinkedHashMap<String, String>(); 262 Element root = doc.getRootElement(); 263 List<?> l = root.getChildren(); 264 for (int i = 0; i < l.size(); i++) { 265 Element child = (Element) l.get(i); 266 retval.put(child.getName(), child.getText()); 267 } 268 return retval; 269 } 270 271 /** Convert a record token into a map. */ 272 private LinkedHashMap<String, String> _parseRecordCmd(RecordToken token) { 273 LinkedHashMap<String, String> retval = new LinkedHashMap<String, String>(); 274 Object labels[] = token.labelSet().toArray(); 275 276 for (int i = 0; i < labels.length; i++) { 277 String str = (String) labels[i]; 278 Token val = token.get(str); 279 String value = _convertTokenToSQL(val); 280 if (value != null){ 281 retval.put(str, value); 282 } 283 else{ 284 retval.put(str, null); 285 } 286 } 287 288 return retval; 289 } 290 291 /** Convert a token's value to an SQL string. */ 292 private String _convertTokenToSQL(Token token) { 293 String retval = null; 294 295 if (token.isNil()){ 296 return null; 297 } 298 else if (token instanceof StringToken) { 299 retval = ((StringToken) token).stringValue(); 300 } else if ((token instanceof IntToken) 301 || (token instanceof DoubleToken)) { 302 retval = token.toString(); 303 } else if (token instanceof ptolemy.data.DateToken) { 304 long ms = ((DateToken) token).getValue(); 305 java.sql.Timestamp ts = new java.sql.Timestamp(ms); 306 retval = ts.toString(); 307 } else if (token instanceof ArrayToken) { 308 ArrayToken array = (ArrayToken) token; 309 StringBuffer buf = new StringBuffer("{"); 310 for (int i = 0; i < array.length(); i++) { 311 String str = _convertTokenToSQL(array.getElement(i)); 312 buf.append(str + ", "); 313 } 314 // remove the last comma and space 315 buf = buf.delete(buf.length() - 2, buf.length()); 316 317 buf.append("}"); 318 retval = buf.toString(); 319 if (retval.equals("{}")){ 320 return null; 321 } 322 } else if (token instanceof MatrixToken) { 323 ArrayToken array = MatrixToken.matrixToArray((MatrixToken) token); 324 retval = _convertTokenToSQL(array); 325 } else { 326 System.out.println("WARNING: unhandled token type " 327 + "converted to sql: " + token.getType()); 328 retval = token.toString(); 329 } 330 331 return retval; 332 } 333 334 /** 335 * Convert a map of key-values into a SQL insert string. 336 * 337 * @param tableName 338 * the name of the table 339 * @param map 340 * the map of column names and values 341 * @return an SQL insert string 342 */ 343 private String _convertMapToSQLInsert(String tableName, 344 LinkedHashMap<String, String> map) { 345 StringBuffer retval = new StringBuffer(); 346 347 retval.append("INSERT INTO " + tableName + " ("); 348 349 for (String name : map.keySet()) { 350 retval.append(name + ", "); 351 } 352 353 // remove the last comma and space 354 retval = retval.delete(retval.length() - 2, retval.length()); 355 356 retval.append(") VALUES ("); 357 358 for (String name : map.keySet()) { 359 if (map.get(name) == null){ 360 retval.append(null + ", "); 361 } 362 else{ 363 retval.append("'" + map.get(name) + "', "); 364 } 365 } 366 367 // remove the last comma and space 368 retval = retval.delete(retval.length() - 2, retval.length()); 369 370 retval.append(")"); 371 372 return retval.toString(); 373 } 374 375 /** 376 * Perform an auto-increment. Find the largest value in the column 377 * <i>colName</i>, return this value incremented by one and place it into the 378 * map. 379 */ 380 private int _checkAutoIncColumn(String tableName, 381 LinkedHashMap<String, String> map) throws IllegalActionException, 382 SQLException { 383 int retval = -1; 384 autoIncColumnName.update(); 385 String colName = ((StringToken) autoIncColumnName.getToken()) 386 .stringValue(); 387 388 // see if auto-inc column name was given 389 if (colName.length() > 0) { 390 String cmd = "SELECT max(" + colName + ") from " + tableName; 391 Statement st = _db.createStatement(); 392 ResultSet rs = st.executeQuery(cmd); 393 int val = 0; 394 395 // see if it exists 396 if (rs.next()) { 397 val = rs.getInt(1) + 1; 398 } 399 400 rs.close(); 401 st.close(); 402 403 retval = val; 404 405 map.put(colName, String.valueOf(val)); 406 } 407 408 return retval; 409 } 410 411 /** Insert the values in a record token using a prepared statement. */ 412 private int _insertRecord(String tableName, RecordToken token) 413 throws SQLException, IllegalActionException { 414 415 StringBuilder buf = new StringBuilder("INSERT INTO ") 416 .append(tableName) 417 .append(" ("); 418 419 StringBuilder paramsBuf = new StringBuilder(); 420 421 List<String> names = new LinkedList<String>(token.labelSet()); 422 for(int i = 0; i < names.size() - 1; i++) { 423 buf.append(names.get(i)) 424 .append(","); 425 paramsBuf.append("?,"); 426 } 427 buf.append(names.get(names.size() - 1)) 428 .append(") VALUES ("); 429 430 paramsBuf.append("?"); 431 432 buf.append(paramsBuf) 433 .append(")"); 434 435 if(_debugging) { 436 _debug("Executing: " + buf); 437 } 438 439 try(PreparedStatement ps = _db.prepareStatement(buf.toString())) { 440 for(int i = 0; i < names.size(); i++) { 441 _setPreparedStatementValue(ps, i + 1, token.get(names.get(i))); 442 } 443 444 return ps.executeUpdate(); 445 } 446 447 } 448 449 // ///////////////////////////////////////////////////////////////// 450 // // private members //// 451 452 /** Helper object to convert from w3c Document to jdom Document */ 453 private DOMBuilder _domBuilder = null; 454 455}