View Javadoc
1   package com.randomnoun.common.spring;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.sql.ResultSet;
8   import java.sql.SQLException;
9   import java.util.*;
10  
11  import org.springframework.dao.DataAccessException;
12  import org.springframework.jdbc.core.*;
13  import org.apache.log4j.Logger;
14  
15  import com.randomnoun.common.Struct;
16  
17  /** A bit like a StructuredListResultSetExtractor, but executes a callback on each object, rather than returning List of them.
18   *
19   * @see StructuredListResultSetExtractor
20   *
21   * 
22   * @author knoxg
23   */
24  public class StructuredMapCallbackHandlerResultSetExtractor 
25      implements ResultSetExtractor<Object> {
26      
27      
28      
29      /** Logger instance for this class */
30      private static Logger logger = Logger.getLogger(StructuredMapCallbackHandlerResultSetExtractor.class);  
31      
32      /** Contains mappings from source column names to (structured) target column names */
33      Map<String, String> columnMapping;
34  
35      /** List to save results in */
36      private final List<Map<String, Object>> results;
37      private List<String> levels;
38  
39      /** Row mapper */
40      private final RowMapper<Map<String, Object>> rowMapper;
41      private Map<String, Object> lastResultRow = null;
42      
43      private StructuredMapCallbackHandler smch;
44  
45      public static interface StructuredMapCallbackHandler {
46      	public void processMap(Map<String, Object> row);
47      }
48      
49      /** The counter used to count rows */
50      private int rowNum = 0;
51  
52      /**
53       * Create a new RowMapperResultReader.
54       * 
55       * @param jt jdbcTemplate for some reason
56       */
57      public StructuredMapCallbackHandlerResultSetExtractor(JdbcTemplate jt, String mappings, StructuredMapCallbackHandler smch) {
58          this(new ColumnMapRowMapper(), mappings, smch);
59      }
60      
61  
62      /**
63       * Create a new RowMapperResultReader.
64       * 
65       * @param rowMapper the RowMapper which creates an object for each row
66       */
67      private StructuredMapCallbackHandlerResultSetExtractor(RowMapper<Map<String, Object>> rowMapper, String mappings, StructuredMapCallbackHandler smch) {
68          this.smch = smch;
69          if (mappings == null) { throw new NullPointerException("mappings cannot be null"); }
70  
71          // Use the more efficient collection if we know how many rows to expect:
72          // ArrayList in case of a known row count, LinkedList if unknown
73          this.results = new ArrayList<Map<String, Object>>();
74          this.rowMapper = rowMapper;
75          this.columnMapping = new HashMap<String, String>();
76          this.levels = new ArrayList<String>(3); // we're not going to go higher than this too often
77  
78          StringTokenizer st = new StringTokenizer(mappings, ",");
79          StringTokenizer st2;
80          StringTokenizer st3;
81          String column = null;
82          String columnTarget = null;
83          String token;
84          String mapping;
85  
86          while (st.hasMoreTokens()) {
87              mapping = st.nextToken().trim();
88  
89              if (mapping.indexOf(' ') == -1) {
90                  column = mapping;
91                  columnTarget = mapping;
92              } else {
93                  // parse state (note that this uses a StringTokenizer, 
94                  // rather than a character-based parser)
95                  // 
96                  // 0 = start of parse
97                  // 1 = consumed column name
98                  // 2 = consumed 'as'
99                  // 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 }