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.*; 013import org.apache.log4j.Logger; 014 015import com.randomnoun.common.Struct; 016 017/** A bit like a StructuredListResultSetExtractor, but executes a callback on each object, rather than returning List of them. 018 * 019 * @see StructuredListResultSetExtractor 020 * 021 * 022 * @author knoxg 023 */ 024public class StructuredMapCallbackHandlerResultSetExtractor 025 implements ResultSetExtractor<Object> { 026 027 028 029 /** Logger instance for this class */ 030 private static Logger logger = Logger.getLogger(StructuredMapCallbackHandlerResultSetExtractor.class); 031 032 /** Contains mappings from source column names to (structured) target column names */ 033 Map<String, String> columnMapping; 034 035 /** List to save results in */ 036 private final List<Map<String, Object>> results; 037 private List<String> levels; 038 039 /** Row mapper */ 040 private final RowMapper<Map<String, Object>> rowMapper; 041 private Map<String, Object> lastResultRow = null; 042 043 private StructuredMapCallbackHandler smch; 044 045 public static interface StructuredMapCallbackHandler { 046 public void processMap(Map<String, Object> row); 047 } 048 049 /** The counter used to count rows */ 050 private int rowNum = 0; 051 052 /** 053 * Create a new RowMapperResultReader. 054 * 055 * @param jt jdbcTemplate for some reason 056 */ 057 public StructuredMapCallbackHandlerResultSetExtractor(JdbcTemplate jt, String mappings, StructuredMapCallbackHandler smch) { 058 this(new ColumnMapRowMapper(), mappings, smch); 059 } 060 061 062 /** 063 * Create a new RowMapperResultReader. 064 * 065 * @param rowMapper the RowMapper which creates an object for each row 066 */ 067 private StructuredMapCallbackHandlerResultSetExtractor(RowMapper<Map<String, Object>> rowMapper, String mappings, StructuredMapCallbackHandler smch) { 068 this.smch = smch; 069 if (mappings == null) { throw new NullPointerException("mappings cannot be null"); } 070 071 // Use the more efficient collection if we know how many rows to expect: 072 // ArrayList in case of a known row count, LinkedList if unknown 073 this.results = new ArrayList<Map<String, Object>>(); 074 this.rowMapper = rowMapper; 075 this.columnMapping = new HashMap<String, String>(); 076 this.levels = new ArrayList<String>(3); // we're not going to go higher than this too often 077 078 StringTokenizer st = new StringTokenizer(mappings, ","); 079 StringTokenizer st2; 080 StringTokenizer st3; 081 String column = null; 082 String columnTarget = null; 083 String token; 084 String mapping; 085 086 while (st.hasMoreTokens()) { 087 mapping = st.nextToken().trim(); 088 089 if (mapping.indexOf(' ') == -1) { 090 column = mapping; 091 columnTarget = mapping; 092 } else { 093 // parse state (note that this uses a StringTokenizer, 094 // rather than a character-based parser) 095 // 096 // 0 = start of parse 097 // 1 = consumed column name 098 // 2 = consumed 'as' 099 // 3 = consumed mapping 100 int state = 0; // 0=initial, 1=got column name, 2=got 'as', 3=got mapping 101 st2 = new StringTokenizer(mapping, " "); 102 while (st2.hasMoreTokens()) { 103 token = st2.nextToken().trim(); 104 if (token.equals("")) { continue; } 105 106 if (state == 0) { 107 column = token; 108 state = 1; 109 } else if (state == 1) { 110 if (!token.equalsIgnoreCase("as")) { 111 throw new IllegalArgumentException("Invalid mapping '" + mapping + "'; expected AS"); 112 } 113 state = 2; 114 } else if (state == 2) { 115 columnTarget = token; 116 state = 3; 117 } else if (state == 3) { 118 throw new IllegalArgumentException("Invalid mapping '" + mapping + "'; too many tokens"); 119 } 120 } 121 } 122 123 // check target for levels 124 int levelIdx = 0; 125 st3 = new StringTokenizer(columnTarget, "."); 126 if (st3.hasMoreTokens()) { 127 String level = st3.nextToken(); 128 129 while (st3.hasMoreTokens()) { 130 if (levelIdx < levels.size()) { 131 if (!levels.get(levelIdx).equals(level)) { 132 throw new IllegalArgumentException("Multiple lists in mapping at level " + levelIdx + ": '" + levels.get(levelIdx) + "' and '" + level + "'"); 133 } 134 } else { 135 levels.add(level); 136 // System.out.println("Levels now: " + levels); 137 } 138 level = st3.nextToken(); 139 levelIdx++; 140 } 141 } 142 columnMapping.put(column.toUpperCase(), columnTarget); 143 } 144 } 145 146 /** Required to support ResultSetExtractor interface 147 * 148 * @param rs resultSet to process 149 * 150 * @return null 151 */ 152 public Object extractData(ResultSet rs) throws SQLException, DataAccessException 153 { 154 while (rs.next()) { 155 processRow(rs); 156 } 157 if (results.size()>0) { smch.processMap(results.get(0)); } 158 return null; 159 } 160 161 162 /** Used by the ResultReader interface to return the results read by this class 163 * 164 * @see org.springframework.jdbc.core.ResultReader#getResults() 165 * 166 public List getResults() { 167 return null; 168 } 169 */ 170 171 /** 172 * Used by the ResultReader interface to process a single row from the database. 173 * 174 * <p>The row is read and matched against the 'levels' specified in the 175 * object constructor. As values change, tree branches are created in the returned 176 * structured List. 177 * 178 * @see org.springframework.jdbc.core.RowCallbackHandler#processRow(java.sql.ResultSet) 179 */ 180 @SuppressWarnings("unchecked") 181 public void processRow(ResultSet rs) 182 throws SQLException { 183 Map<String, Object> row = (Map<String, Object>) rowMapper.mapRow(rs, this.rowNum++); // ClobRowMapper always returns a Map. 184 // System.out.println("Processing row " + Struct.structuredMapToString("row", row)); 185 // logger.debug("row is " + Struct.structuredMapToJson(row)); 186 187 int createLevel = 0; 188 List<Map<String, Object>> createList = results; 189 Map<String, Object> createRow = new HashMap<String, Object>(); 190 String createPrefix = ""; 191 192 // determine highest level that we can create at 193 if (lastResultRow != null) { 194 // System.out.println("lastResultRow processing"); 195 createLevel = levels.size() + 1; 196 197 // find lowest level that has a different value 198 for (Iterator<Map.Entry<String, String>> i = columnMapping.entrySet().iterator(); i.hasNext();) { 199 Map.Entry<String, String> entry = (Map.Entry<String, String>) i.next(); 200 String column = (String) entry.getKey(); 201 String columnTarget = (String) entry.getValue(); 202 203 List<Map<String,Object>> containerList = results; // maybe 204 Map<String, Object> containerMap = lastResultRow; 205 206 String component; 207 int pos = columnTarget.indexOf('.'); 208 int level = 0; 209 210 while (pos != -1) { 211 component = columnTarget.substring(0, pos); 212 columnTarget = columnTarget.substring(pos + 1); 213 214 if (!containerMap.containsKey(component)) { 215 throw new IllegalStateException("Missing field '" + component + "' in " + Struct.structuredMapToString("containerMap", containerMap) + "; last result row is " + Struct.structuredMapToString("lastResultRow", lastResultRow)); 216 } 217 218 if (component.equals(levels.get(level))) { 219 level++; 220 containerList = (List<Map<String,Object>>) containerMap.get(component); 221 containerMap = (Map<String, Object>) containerList.get(containerList.size() - 1); 222 if (containerMap==null) { 223 logger.error("null containerMap"); 224 } 225 } else { 226 containerMap = (Map<String, Object>) containerMap.get(component); 227 if (containerMap==null) { 228 logger.error("null containerMap"); 229 } 230 231 } 232 233 pos = columnTarget.indexOf('.'); 234 } 235 236 Object thisValue = row.get(column); 237 Object lastValue = containerMap.get(columnTarget); 238 // System.out.println("Comparing thisValue '" + thisValue + "' to lastValue '" + lastValue + "'"); 239 240 if ((thisValue == null && lastValue != null) || (thisValue != null && !thisValue.equals(lastValue))) { 241 // values are different; create row 242 if (createLevel > level) { 243 createList = containerList; 244 245 // System.out.println("Reducing level to '" + level + "' because of " + 246 // "column '" + columnTarget + "' differing (" + thisValue + " instead of previous: " + lastValue); 247 createLevel = level; 248 } 249 } 250 } 251 } 252 253 if (createLevel > levels.size()) { 254 // rows are completely identical -- don't add it to the list 255 return; 256 } 257 258 for (int i = 0; i < createLevel; i++) { 259 createPrefix = createPrefix + levels.get(i) + "."; 260 } 261 262 // generate 'createRow' 263 for (Iterator<Map.Entry<String, String>> i = columnMapping.entrySet().iterator(); i.hasNext();) { 264 Map.Entry<String, String> entry = i.next(); 265 String column = (String) entry.getKey(); 266 String columnTarget = (String) entry.getValue(); 267 if (!columnTarget.startsWith(createPrefix)) { 268 continue; 269 } 270 Object value = row.get(column); 271 272 //logger.debug("About to add column '" + columnTarget + "' from rs column '" + column + "'; createPrefix = '" + createPrefix + "' with value '" + value + "'"); 273 274 columnTarget = columnTarget.substring(createPrefix.length()); 275 //logger.debug(" columnTarget '" + columnTarget + "'"); 276 277 List<Map<String, Object>> containerList = createList; 278 Map<String, Object> containerMap = createRow; 279 280 int level = createLevel; // ** was 0 ? 281 String component; 282 int pos = columnTarget.indexOf('.'); 283 284 while (pos != -1) { 285 component = columnTarget.substring(0, pos); 286 columnTarget = columnTarget.substring(pos + 1); 287 288 if (component.equals(levels.get(level))) { 289 level++; 290 containerList = (List<Map<String, Object>>) containerMap.get(component); 291 292 if (containerList == null) { 293 containerList = new ArrayList<Map<String, Object>>(); 294 containerMap.put(component, containerList); 295 containerList.add(new HashMap<String, Object>()); 296 } 297 298 containerMap = (Map<String, Object>) containerList.get(containerList.size() - 1); 299 if (containerMap==null) { 300 logger.error("C null containerMap"); 301 } 302 303 } else { 304 containerMap = (Map<String, Object>) containerMap.get(component); 305 if (containerMap==null) { 306 logger.error("D null containerMap"); 307 } 308 309 } 310 311 pos = columnTarget.indexOf('.'); 312 } 313 314 containerMap.put(columnTarget, value); 315 } 316 317 if (createList == results) { 318 if (results.size()>0) { smch.processMap(results.get(0)); } 319 results.clear(); // @TODO don't use a list for this, since we only ever hold a single object. Keeps it vaguely similar to StructuredResultReader a.k.a. StructuredListResultSetExtractor though. 320 } else { 321 // smch.processSomethingElsePerhaps(); 322 } 323 324 createList.add(createRow); 325 lastResultRow = results.get(results.size() - 1); 326 327 } 328 329}