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}