001package com.randomnoun.common.spring; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.sql.ResultSet; 008import java.sql.SQLException; 009import java.util.*; 010 011import org.springframework.dao.DataAccessException; 012import org.springframework.jdbc.core.*; 013 014import org.apache.log4j.Logger; 015 016import com.randomnoun.common.Struct; 017 018// this could probably delegate to StructuredMapCallbackHandlerResultSetExtractor these days 019// 020 021/** 022 * This class returns structured object graphs composed of lists/maps. 023 * Think of it as a poor man's JDO, using Maps and Lists rather than concrete 024 * classes, which allows us to omit the POJO generation step. 025 * 026 * <p>This ResultReader is to be used in the JdbcTemplate class to retrieve structured 027 * representations of queries covering more than one table. In a simple case, say a 028 * cowGroup contains many cows, and we wish to return the cowGroup ID and name as 029 * well as the cow ID and name in the one query; typically, this would be done 030 * through the following hypothetical SQL: 031 * 032 * <pre class="code"> 033 * SELECT cowGroupId, cowGroupName, cowId, cowName 034 * FROM cowGroups, cows 035 * WHERE cows.cowGroupId = cowGroups.cowGroupId 036 * </pre> 037 * 038 * Normally, Spring would return this as a List containing a map containing four 039 * keys (cowGroupId, cowGroupName, cowId and cowName), containing the values of 040 * each column returned by the RDBMS; e.g. the code fragment 041 * 042 * <pre class="code"> 043 List result1 = jt.queryForList(sql); 044 System.out.println(Text.structuredListToString("result1", result1)); 045 </pre> 046 (with the SQL above) would produce this output (a three-element list): 047 * <pre class="code"> 048 result1 = [ 049 0 = { 050 COWGROUPID => 1 051 COWGROUPNAME => 'ROLLINGFIELD7' 052 COWID => 1000 053 COWNAME => 'DAISY' 054 } 055 1 = { 056 COWGROUPID => 2 057 COWGROUPNAME => 'SLAUGHTERHOUSE5' 058 COWID => 1001 059 COWNAME => 'BUTTERCUP' 060 } 061 2 = { 062 COWGROUPID => 2 063 COWGROUPNAME => 'SLAUGHTERHOUSE5' 064 COWID => 1002 065 COWNAME => 'STEVE' 066 } 067 ] 068 </pre> 069 * 070 * note how both BUTTERCUP and STEVE belong to the SLAUGHTERHOUSE5 cowGroup, but 071 * DAISY is within it's own cowGroup. If we wanted to express this relationship 072 * by having a list of cows within each cowgroup, we would have to scan through 073 * this list manually, and filter each cow into it's parent cowGroup. 074 * 075 * <p>This object automates this process; e.g. the same structure above, when 076 * processed using the following code snippet: 077 * 078 * <pre class="code"> 079 String mapping = "cowGroupId, cowGroupName, " + 080 "cowId AS cows.cowId, cowName AS cows.cowName"; 081 List result2 = jt.query(sql, new StructuredResultReader(mapping)); 082 System.out.println(Text.structuredListToString("result2", result2)); 083 </pre> 084 * ... would produce this output (a two-element list) ... 085 <pre style="code"> 086 result2 = [ 087 0 = { 088 cowGroupName => 'ROLLINGFIELD7' 089 cowGroupId => 2 090 cows = [ 091 0 = { 092 cowId => 1000 093 cowName => 'DAISY' 094 } 095 ] 096 } 097 1 = { 098 cowGroupName => 'SLAUGHTERHOUSE5' 099 cowGroupId => 1 100 cows = [ 101 0 = { 102 cowId => 1001 103 cowName => 'BUTTERCUP' 104 } 105 1 = { 106 cowId => 1002 107 cowName => 'STEVE' 108 } 109 ] 110 } 111 ] 112 </pre> 113 * 114 * which may be more suitable for processing. This structure can also be used within 115 * JSTL, e.g. 116 * 117 * <pre class="code"> 118 * <c:out value="${result2[0].cows[1].cowName}"/> 119 * </pre> 120 * 121 * would generate the text "STEVE". 122 * 123 * <p>This class can generate an arbitrary number of levels within the returned structure 124 * by specifying more groupings in the mapping text. This class also performs 125 * camelCasing of returned columns (e.g. to return 'cowName' rather than 'COWNAME'). 126 * 127 * <p>The guts of the interface lies in the mapping string passed to the StructuredResultReader 128 * constructor. The syntax of this mapping string is a list of column names, separated by 129 * commas; a column name may also have an "AS" definition, which defines how it 130 * is to be represented in the returned structure. 131 * 132 * <i>e.g.</i> assuming a department contains many people, this mapping will group each 133 * person within their department: 134 * 135 * <pre class="code"> 136 * departmentName, personName AS people.name, personId AS people.id 137 * </pre> 138 * 139 * which will return a List of Maps, containing two keys, 'departmentName' and 'people'. 140 * The people value will be a List of Maps, containing two keys, 'name' and 'id'. 141 * 142 * <p>If each person now has a number of addresses, we could perform a two-level grouping, 143 * using the following mapping: 144 * 145 * <pre class="code"> 146 * department, personName AS people.name, personId AS people.id, 147 * addressType AS people.addresses.type 148 * </pre> 149 * 150 * <p>Each people value will now be a List of Maps containing three keys, 'name', 'id' and 151 * 'addresses'; the 'addresses' value will be a List of Maps with only one key, 'type'. 152 * 153 * <p>Hope that all makes sense. 154 * 155 * <p>Implementation note: The resultset returned by the SQL must be sorted by 156 * the top-level group, then by the second-level group, and so on, for this code to 157 * work as expected. 158 * 159 * 160 * @author knoxg 161 */ 162public class StructuredListResultSetExtractor 163 implements ResultSetExtractor<Object> { 164 165 166 167 /** Logger instance for this class */ 168 private static Logger logger = Logger.getLogger(StructuredListResultSetExtractor.class); 169 170 /** Contains mappings from source column names to (structured) target column names */ 171 Map<String, String> columnMapping; 172 173 /** List to save results in */ 174 private final List<Map<String, Object>> results; 175 private List<String> levels; 176 177 /** Row mapper */ 178 private final RowMapper<Map<String, Object>> rowMapper; 179 private Map<String, Object> lastResultRow = null; 180 181 /** The counter used to count rows */ 182 private int rowNum = 0; 183 184 /** 185 * Create a new RowMapperResultReader. 186 * @param rowMapper the RowMapper which creates an object for each row 187 */ 188 public StructuredListResultSetExtractor(JdbcTemplate jt, String mappings) { 189 this(new ColumnMapRowMapper(), mappings, 0); 190 } 191 192 /** 193 * Create a new RowMapperResultReader. 194 * @param rowMapper the RowMapper which creates an object for each row 195 * @param rowsExpected the number of expected rows 196 * (just used for optimized collection handling) 197 */ 198 public StructuredListResultSetExtractor(RowMapper<Map<String, Object>> rowMapper, String mappings, int rowsExpected) { 199 200 if (mappings == null) { throw new NullPointerException("mappings cannot be null"); } 201 202 // Use the more efficient collection if we know how many rows to expect: 203 // ArrayList in case of a known row count, LinkedList if unknown 204 this.results = (rowsExpected > 0) ? new ArrayList<Map<String, Object>>(rowsExpected) : new LinkedList<Map<String, Object>>(); 205 this.rowMapper = rowMapper; 206 this.columnMapping = new HashMap<String, String>(); 207 this.levels = new ArrayList<String>(3); // we're not going to go higher than this too often 208 209 StringTokenizer st = new StringTokenizer(mappings, ","); 210 StringTokenizer st2; 211 StringTokenizer st3; 212 String column = null; 213 String columnTarget = null; 214 String token; 215 String mapping; 216 217 while (st.hasMoreTokens()) { 218 mapping = st.nextToken().trim(); 219 220 if (mapping.indexOf(' ') == -1) { 221 column = mapping; 222 columnTarget = mapping; 223 } else { 224 // parse state (note that this uses a StringTokenizer, 225 // rather than a character-based parser) 226 // 227 // 0 = start of parse 228 // 1 = consumed column name 229 // 2 = consumed 'as' 230 // 3 = consumed mapping 231 int state = 0; // 0=initial, 1=got column name, 2=got 'as', 3=got mapping 232 st2 = new StringTokenizer(mapping, " "); 233 while (st2.hasMoreTokens()) { 234 token = st2.nextToken().trim(); 235 if (token.equals("")) { continue; } 236 237 if (state == 0) { 238 column = token; 239 state = 1; 240 } else if (state == 1) { 241 if (!token.equalsIgnoreCase("as")) { 242 throw new IllegalArgumentException("Invalid mapping '" + mapping + "'; expected AS"); 243 } 244 state = 2; 245 } else if (state == 2) { 246 columnTarget = token; 247 state = 3; 248 } else if (state == 3) { 249 throw new IllegalArgumentException("Invalid mapping '" + mapping + "'; too many tokens"); 250 } 251 } 252 } 253 254 // check target for levels 255 int levelIdx = 0; 256 st3 = new StringTokenizer(columnTarget, "."); 257 if (st3.hasMoreTokens()) { 258 String level = st3.nextToken(); 259 260 while (st3.hasMoreTokens()) { 261 if (levelIdx < levels.size()) { 262 if (!levels.get(levelIdx).equals(level)) { 263 throw new IllegalArgumentException("Multiple lists in mapping at level " + levelIdx + ": '" + levels.get(levelIdx) + "' and '" + level + "'"); 264 } 265 } else { 266 levels.add(level); 267 // System.out.println("Levels now: " + levels); 268 } 269 level = st3.nextToken(); 270 levelIdx++; 271 } 272 } 273 columnMapping.put(column.toUpperCase(), columnTarget); 274 } 275 } 276 277 /** Required to support ResultSetExtractor interface 278 * 279 * @param rs resultSet to process 280 * 281 * @return a List of structured Maps, as described in the class javadoc 282 */ 283 public Object extractData(ResultSet rs) throws SQLException, DataAccessException 284 { 285 // int rowNum = 0; 286 while (rs.next()) { 287 // results.add(this.rowMapper.mapRow(rs, rowNum++)); 288 processRow(rs); 289 } 290 return results; 291 } 292 293 294 /** 295 * Used by the ResultReader interface to return the results read by this class 296 * 297 * @see org.springframework.jdbc.core.ResultReader#getResults() 298 * 299 public List getResults() { 300 return results; 301 } 302 */ 303 304 /** 305 * Used by the ResultReader interface to process a single row from the database. 306 * 307 * <p>The row is read and matched against the 'levels' specified in the 308 * object constructor. As values change, tree branches are created in the returned 309 * structured List. 310 * 311 * @see org.springframework.jdbc.core.RowCallbackHandler#processRow(java.sql.ResultSet) 312 */ 313 @SuppressWarnings("unchecked") 314 public void processRow(ResultSet rs) 315 throws SQLException { 316 Map<String, Object> row = (Map<String, Object>) rowMapper.mapRow(rs, this.rowNum++); // ClobRowMapper always returns a Map. 317 // System.out.println("Processing row " + Struct.structuredMapToString("row", row)); 318 319 // logger.debug("row is " + Struct.structuredMapToJson(row)); 320 321 int createLevel = 0; 322 List<Map<String, Object>> createList = results; 323 Map<String, Object> createRow = new HashMap<String, Object>(); 324 String createPrefix = ""; 325 326 // determine highest level that we can create at 327 if (lastResultRow != null) { 328 // System.out.println("lastResultRow processing"); 329 createLevel = levels.size() + 1; 330 331 // find lowest level that has a different value 332 for (Iterator<Map.Entry<String, String>> i = columnMapping.entrySet().iterator(); i.hasNext();) { 333 Map.Entry<String, String> entry = (Map.Entry<String, String>) i.next(); 334 String column = (String) entry.getKey(); 335 String columnTarget = (String) entry.getValue(); 336 337 List<Map<String,Object>> containerList = results; 338 Map<String, Object> containerMap = lastResultRow; 339 340 String component; 341 int pos = columnTarget.indexOf('.'); 342 int level = 0; 343 344 while (pos != -1) { 345 component = columnTarget.substring(0, pos); 346 columnTarget = columnTarget.substring(pos + 1); 347 348 if (!containerMap.containsKey(component)) { 349 throw new IllegalStateException("Missing field '" + component + "' in " + Struct.structuredMapToString("containerMap", containerMap) + "; last result row is " + Struct.structuredMapToString("lastResultRow", lastResultRow)); 350 } 351 352 if (component.equals(levels.get(level))) { 353 level++; 354 containerList = (List<Map<String,Object>>) containerMap.get(component); 355 containerMap = (Map<String, Object>) containerList.get(containerList.size() - 1); 356 if (containerMap==null) { 357 logger.error("null containerMap"); 358 } 359 } else { 360 containerMap = (Map<String, Object>) containerMap.get(component); 361 if (containerMap==null) { 362 logger.error("null containerMap"); 363 } 364 365 } 366 367 pos = columnTarget.indexOf('.'); 368 } 369 370 Object thisValue = row.get(column); 371 Object lastValue = containerMap.get(columnTarget); 372 // System.out.println("Comparing thisValue '" + thisValue + "' to lastValue '" + lastValue + "'"); 373 374 if ((thisValue == null && lastValue != null) || (thisValue != null && !thisValue.equals(lastValue))) { 375 // values are different; create row 376 if (createLevel > level) { 377 createList = containerList; 378 379 // System.out.println("Reducing level to '" + level + "' because of " + 380 // "column '" + columnTarget + "' differing (" + thisValue + " instead of previous: " + lastValue); 381 createLevel = level; 382 } 383 } 384 } 385 } 386 387 if (createLevel > levels.size()) { 388 // rows are completely identical -- don't add it to the list 389 return; 390 } 391 392 for (int i = 0; i < createLevel; i++) { 393 createPrefix = createPrefix + levels.get(i) + "."; 394 } 395 396 // generate 'createRow' 397 for (Iterator<Map.Entry<String, String>> i = columnMapping.entrySet().iterator(); i.hasNext(); ) { 398 Map.Entry<String, String> entry = i.next(); 399 String column = (String) entry.getKey(); 400 String columnTarget = (String) entry.getValue(); 401 if (!columnTarget.startsWith(createPrefix)) { 402 continue; 403 } 404 Object value = row.get(column); 405 406 //logger.debug("About to add column '" + columnTarget + "' from rs column '" + column + "'; createPrefix = '" + createPrefix + "' with value '" + value + "'"); 407 408 columnTarget = columnTarget.substring(createPrefix.length()); 409 //logger.debug(" columnTarget '" + columnTarget + "'"); 410 411 List<Map<String, Object>> containerList = createList; 412 Map<String, Object> containerMap = createRow; 413 int level = createLevel; // ** was 0 ? 414 String component; 415 int pos = columnTarget.indexOf('.'); 416 417 while (pos != -1) { 418 component = columnTarget.substring(0, pos); 419 columnTarget = columnTarget.substring(pos + 1); 420 421 /* if (!containerMap.containsKey(component)) { 422 containerMap.put(component, new HashMap()); 423 } */ 424 425 // System.out.println("Setting component '" + component + "' in " + 426 // Codec.structuredMapToString("containerMap", containerMap)); 427 if (component.equals(levels.get(level))) { 428 level++; 429 containerList = (List<Map<String, Object>>) containerMap.get(component); 430 431 if (containerList == null) { 432 containerList = new ArrayList<Map<String,Object>>(); 433 containerMap.put(component, containerList); 434 containerList.add(new HashMap<String, Object>()); 435 } 436 437 containerMap = (Map<String, Object>) containerList.get(containerList.size() - 1); 438 if (containerMap==null) { 439 logger.error("C null containerMap"); 440 } 441 442 } else { 443 containerMap = (Map<String, Object>) containerMap.get(component); 444 if (containerMap==null) { 445 logger.error("D null containerMap"); 446 } 447 448 } 449 450 pos = columnTarget.indexOf('.'); 451 } 452 453 containerMap.put(columnTarget, value); 454 } 455 456 createList.add(createRow); 457 lastResultRow = (Map<String, Object>) results.get(results.size() - 1); 458 /* 459 if (logger.isDebugEnabled()) { 460 logger.debug("processRow complete; added row " + Struct.structuredMapToString("createRow", createRow)); 461 logger.debug("resultSet now = " + Struct.structuredListToString("results", results)); 462 } 463 */ 464 } 465 466 467}