001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012package org.nuxeo.ecm.core.storage.sql.jdbc; 013 014import java.io.Serializable; 015import java.sql.PreparedStatement; 016import java.sql.ResultSet; 017import java.sql.SQLException; 018import java.util.Iterator; 019import java.util.Map; 020import java.util.NoSuchElementException; 021 022import org.nuxeo.ecm.core.api.IterableQueryResult; 023import org.nuxeo.ecm.core.query.QueryFilter; 024import org.nuxeo.ecm.core.storage.sql.Session.PathResolver; 025 026/** 027 * Iterable query result implemented as a cursor on a SQL {@link ResultSet}. 028 */ 029public class ResultSetQueryResult implements IterableQueryResult, Iterator<Map<String, Serializable>> { 030 031 private QueryMaker.Query q; 032 033 private PreparedStatement ps; 034 035 private ResultSet rs; 036 037 private Map<String, Serializable> next; 038 039 private boolean eof; 040 041 private long pos; 042 043 private long size = -1; 044 045 private final JDBCLogger logger; 046 047 public ResultSetQueryResult(QueryMaker queryMaker, String query, QueryFilter queryFilter, PathResolver pathResolver, 048 JDBCMapper mapper, Object... params) throws SQLException { 049 logger = mapper.logger; 050 q = queryMaker.buildQuery(mapper.sqlInfo, mapper.model, pathResolver, query, queryFilter, params); 051 if (q == null) { 052 logger.log("Query cannot return anything due to conflicting clauses"); 053 ps = null; 054 rs = null; 055 eof = true; 056 return; 057 } else { 058 eof = false; 059 } 060 if (logger.isLogEnabled()) { 061 logger.logSQL(q.selectInfo.sql, q.selectParams); 062 } 063 ps = mapper.connection.prepareStatement(q.selectInfo.sql, ResultSet.TYPE_SCROLL_INSENSITIVE, 064 ResultSet.CONCUR_READ_ONLY); 065 int i = 1; 066 for (Serializable object : q.selectParams) { 067 mapper.setToPreparedStatement(ps, i++, object); 068 } 069 rs = ps.executeQuery(); 070 mapper.countExecute(); 071 // rs.setFetchDirection(ResultSet.FETCH_UNKNOWN); fails in H2 072 } 073 074 protected static void closePreparedStatement(PreparedStatement ps) throws SQLException { 075 try { 076 ps.close(); 077 } catch (IllegalArgumentException e) { 078 // ignore 079 // http://bugs.mysql.com/35489 with JDBC 4 and driver <= 5.1.6 080 } 081 } 082 083 @Override 084 public void close() { 085 if (rs == null) { 086 return; 087 } 088 try { 089 rs.close(); 090 closePreparedStatement(ps); 091 } catch (SQLException e) { 092 logger.error("Error closing statement: " + e.getMessage(), e); 093 } finally { 094 pos = -1; 095 rs = null; 096 ps = null; 097 } 098 } 099 100 @Override 101 public boolean isLife() { 102 return rs != null; 103 } 104 105 public static class ClosedIteratorException extends IllegalStateException { 106 107 private static final long serialVersionUID = 1L; 108 109 public final QueryMaker.Query query; 110 111 protected ClosedIteratorException(QueryMaker.Query q) { 112 super("Query results iterator closed (" + q.selectInfo.sql + ")"); 113 this.query = q; 114 } 115 116 } 117 118 protected void checkLife() { 119 if (rs == null) { 120 throw new ClosedIteratorException(q); 121 } 122 } 123 124 @Override 125 public long size() { 126 checkLife(); 127 if (size != -1) { 128 return size; 129 } 130 try { 131 // save cursor pos 132 int old = rs.isBeforeFirst() ? -1 : rs.isAfterLast() ? -2 : rs.getRow(); 133 // find size 134 rs.last(); 135 size = rs.getRow(); 136 // set back cursor 137 if (old == -1) { 138 rs.beforeFirst(); 139 } else if (old == -2) { 140 rs.afterLast(); 141 } else if (old != 0) { 142 rs.absolute(old); 143 } 144 return size; 145 } catch (SQLException e) { 146 throw new RuntimeException(e); 147 } 148 } 149 150 @Override 151 public long pos() { 152 checkLife(); 153 return pos; 154 } 155 156 @Override 157 public void skipTo(long pos) { 158 checkLife(); 159 try { 160 boolean available = rs.absolute((int) pos + 1); 161 if (available) { 162 next = fetchCurrent(); 163 eof = false; 164 this.pos = pos; 165 } else { 166 // after last row 167 next = null; 168 eof = true; 169 this.pos = -1; // XXX 170 } 171 } catch (SQLException e) { 172 logger.error("Error skipping to: " + pos + ": " + e.getMessage(), e); 173 } 174 } 175 176 @Override 177 public Iterator<Map<String, Serializable>> iterator() { 178 checkLife(); 179 return this; 180 } 181 182 protected Map<String, Serializable> fetchNext() throws SQLException { 183 checkLife(); 184 if (!rs.next()) { 185 if (logger.isLogEnabled()) { 186 logger.log(" -> END"); 187 } 188 return null; 189 } 190 return fetchCurrent(); 191 } 192 193 protected Map<String, Serializable> fetchCurrent() throws SQLException { 194 checkLife(); 195 Map<String, Serializable> map = q.selectInfo.mapMaker.makeMap(rs); 196 if (logger.isLogEnabled()) { 197 logger.logMap(map); 198 } 199 return map; 200 } 201 202 @Override 203 public boolean hasNext() { 204 checkLife(); 205 if (next != null) { 206 return true; 207 } 208 if (eof) { 209 return false; 210 } 211 try { 212 next = fetchNext(); 213 } catch (SQLException e) { 214 logger.error("Error fetching next: " + e.getMessage(), e); 215 } 216 eof = next == null; 217 return !eof; 218 } 219 220 @Override 221 public Map<String, Serializable> next() { 222 checkLife(); 223 if (!hasNext()) { 224 pos = -1; 225 throw new NoSuchElementException(); 226 } 227 Map<String, Serializable> n = next; 228 next = null; 229 pos++; 230 return n; 231 } 232 233 @Override 234 public void remove() { 235 throw new UnsupportedOperationException(); 236 } 237 238}