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            // no result
060            size = 0;
061            ps = null;
062            rs = null;
063            eof = true;
064            return;
065        } else {
066            eof = false;
067        }
068        if (logger.isLogEnabled()) {
069            logger.logSQL(q.selectInfo.sql, q.selectParams);
070        }
071        ps = mapper.connection.prepareStatement(q.selectInfo.sql, ResultSet.TYPE_SCROLL_INSENSITIVE,
072                ResultSet.CONCUR_READ_ONLY);
073        int i = 1;
074        for (Serializable object : q.selectParams) {
075            mapper.setToPreparedStatement(ps, i++, object);
076        }
077        rs = ps.executeQuery();
078        mapper.countExecute();
079        // rs.setFetchDirection(ResultSet.FETCH_UNKNOWN); fails in H2
080    }
081
082    protected static void closePreparedStatement(PreparedStatement ps) throws SQLException {
083        try {
084            ps.close();
085        } catch (IllegalArgumentException e) {
086            // ignore
087            // http://bugs.mysql.com/35489 with JDBC 4 and driver <= 5.1.6
088        }
089    }
090
091    @Override
092    public void close() {
093        if (rs == null) {
094            return;
095        }
096        try {
097            rs.close();
098            closePreparedStatement(ps);
099        } catch (SQLException e) {
100            logger.error("Error closing statement: " + e.getMessage(), e);
101        } finally {
102            pos = -1;
103            rs = null;
104            ps = null;
105        }
106    }
107
108    @Override
109    public boolean isLife() {
110        return rs != null;
111    }
112
113    @Override
114    public boolean mustBeClosed() {
115        return rs != null;
116    }
117
118    public static class ClosedIteratorException extends IllegalStateException {
119
120        private static final long serialVersionUID = 1L;
121
122        public final QueryMaker.Query query;
123
124        protected ClosedIteratorException(QueryMaker.Query q) {
125            super("Query results iterator closed (" + q.selectInfo.sql + ")");
126            this.query = q;
127        }
128
129    }
130
131    protected void checkNotClosed() {
132        if (rs == null) {
133            throw new ClosedIteratorException(q);
134        }
135    }
136
137    @Override
138    public long size() {
139        if (size != -1) {
140            return size;
141        }
142        checkNotClosed();
143        try {
144            // save cursor pos
145            int old = rs.isBeforeFirst() ? -1 : rs.isAfterLast() ? -2 : rs.getRow();
146            // find size
147            rs.last();
148            size = rs.getRow();
149            // set back cursor
150            if (old == -1) {
151                rs.beforeFirst();
152            } else if (old == -2) {
153                rs.afterLast();
154            } else if (old != 0) {
155                rs.absolute(old);
156            }
157            return size;
158        } catch (SQLException e) {
159            throw new RuntimeException(e);
160        }
161    }
162
163    @Override
164    public long pos() {
165        checkNotClosed();
166        return pos;
167    }
168
169    @Override
170    public void skipTo(long pos) {
171        checkNotClosed();
172        try {
173            boolean available = rs.absolute((int) pos + 1);
174            if (available) {
175                next = fetchCurrent();
176                eof = false;
177                this.pos = pos;
178            } else {
179                // after last row
180                next = null;
181                eof = true;
182                this.pos = -1; // XXX
183            }
184        } catch (SQLException e) {
185            logger.error("Error skipping to: " + pos + ": " + e.getMessage(), e);
186        }
187    }
188
189    @Override
190    public Iterator<Map<String, Serializable>> iterator() {
191        checkNotClosed();
192        return this; // NOSONAR this iterable does not support multiple traversals
193    }
194
195    protected Map<String, Serializable> fetchNext() throws SQLException {
196        checkNotClosed();
197        if (!rs.next()) {
198            if (logger.isLogEnabled()) {
199                logger.log("  -> END");
200            }
201            return null;
202        }
203        return fetchCurrent();
204    }
205
206    protected Map<String, Serializable> fetchCurrent() throws SQLException {
207        checkNotClosed();
208        Map<String, Serializable> map = q.selectInfo.mapMaker.makeMap(rs);
209        if (logger.isLogEnabled()) {
210            logger.logMap(map);
211        }
212        return map;
213    }
214
215    @Override
216    public boolean hasNext() {
217        if (eof) {
218            return false;
219        }
220        checkNotClosed();
221        if (next != null) {
222            return true;
223        }
224        try {
225            next = fetchNext();
226        } catch (SQLException e) {
227            logger.error("Error fetching next: " + e.getMessage(), e);
228        }
229        eof = next == null;
230        return !eof;
231    }
232
233    @Override
234    public Map<String, Serializable> next() {
235        checkNotClosed();
236        if (!hasNext()) {
237            pos = -1;
238            throw new NoSuchElementException();
239        }
240        Map<String, Serializable> n = next;
241        next = null;
242        pos++;
243        return n;
244    }
245
246    @Override
247    public void remove() {
248        throw new UnsupportedOperationException();
249    }
250
251}