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