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}