001/*
002 * Copyright 2006-2020 the original author or authors.
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 *      https://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
017package org.springframework.batch.item.database.support;
018
019import java.util.ArrayList;
020import java.util.LinkedHashMap;
021import java.util.List;
022import java.util.Map;
023import javax.sql.DataSource;
024
025import org.springframework.batch.item.database.JdbcParameterUtils;
026import org.springframework.batch.item.database.Order;
027import org.springframework.batch.item.database.PagingQueryProvider;
028import org.springframework.dao.InvalidDataAccessApiUsageException;
029import org.springframework.util.Assert;
030import org.springframework.util.StringUtils;
031
032/**
033 * Abstract SQL Paging Query Provider to serve as a base class for all provided
034 * SQL paging query providers.
035 * 
036 * Any implementation must provide a way to specify the select clause, from
037 * clause and optionally a where clause. In addition a way to specify a single
038 * column sort key must also be provided. This sort key will be used to provide
039 * the paging functionality. It is recommended that there should be an index for
040 * the sort key to provide better performance.
041 * 
042 * Provides properties and preparation for the mandatory "selectClause" and
043 * "fromClause" as well as for the optional "whereClause". Also provides
044 * property for the mandatory "sortKeys".  <b>Note:</b> The columns that make up 
045 * the sort key must be a true key and not just a column to order by. It is important
046 * to have a unique key constraint on the sort key to guarantee that no data is lost
047 * between executions.
048 * 
049 * @author Thomas Risberg
050 * @author Dave Syer
051 * @author Michael Minella
052 * @author Mahmoud Ben Hassine
053 * @author Benjamin Hetz
054 * @since 2.0
055 */
056public abstract class AbstractSqlPagingQueryProvider implements PagingQueryProvider {
057
058        private String selectClause;
059
060        private String fromClause;
061
062        private String whereClause;
063        
064        private Map<String, Order> sortKeys = new LinkedHashMap<String, Order>();
065
066        private String groupClause;
067
068        private int parameterCount;
069
070        private boolean usingNamedParameters;
071        
072        /**
073         * The setter for the group by clause
074         * 
075         * @param groupClause SQL GROUP BY clause part of the SQL query string
076         */
077        public void setGroupClause(String groupClause) {
078                if (StringUtils.hasText(groupClause)) {
079                        this.groupClause = removeKeyWord("group by", groupClause);
080                }
081                else {
082                        this.groupClause = null;
083                }
084        }
085        
086        /**
087         * The getter for the group by clause
088         * 
089         * @return SQL GROUP BY clause part of the SQL query string
090         */
091        public String getGroupClause() {
092                return this.groupClause;
093        }
094
095        /**
096         * @param selectClause SELECT clause part of SQL query string
097         */
098        public void setSelectClause(String selectClause) {
099                this.selectClause = removeKeyWord("select", selectClause);
100        }
101
102        /**
103         * 
104         * @return SQL SELECT clause part of SQL query string
105         */
106        protected String getSelectClause() {
107                return selectClause;
108        }
109
110        /**
111         * @param fromClause FROM clause part of SQL query string
112         */
113        public void setFromClause(String fromClause) {
114                this.fromClause = removeKeyWord("from", fromClause);
115        }
116
117        /**
118         * 
119         * @return SQL FROM clause part of SQL query string
120         */
121        protected String getFromClause() {
122                return fromClause;
123        }
124
125        /**
126         * @param whereClause WHERE clause part of SQL query string
127         */
128        public void setWhereClause(String whereClause) {
129                if (StringUtils.hasText(whereClause)) {
130                        this.whereClause = removeKeyWord("where", whereClause);
131                }
132                else {
133                        this.whereClause = null;
134                }
135        }
136
137        /**
138         * 
139         * @return SQL WHERE clause part of SQL query string
140         */
141        protected String getWhereClause() {
142                return whereClause;
143        }
144
145        /**
146         * @param sortKeys key to use to sort and limit page content
147         */
148        public void setSortKeys(Map<String, Order> sortKeys) {
149                this.sortKeys = sortKeys;
150        }
151
152        /**
153         * A Map&lt;String, Boolean&gt; of sort columns as the key and boolean for ascending/descending (ascending = true).
154         * 
155         * @return sortKey key to use to sort and limit page content
156         */
157    @Override
158        public Map<String, Order> getSortKeys() {
159                return sortKeys;
160        }
161
162    @Override
163        public int getParameterCount() {
164                return parameterCount;
165        }
166
167    @Override
168        public boolean isUsingNamedParameters() {
169                return usingNamedParameters;
170        }
171
172        /**
173         * The sort key placeholder will vary depending on whether named parameters
174         * or traditional placeholders are used in query strings.
175         * 
176         * @return place holder for sortKey.
177         */
178    @Override
179        public String getSortKeyPlaceHolder(String keyName) {
180                return usingNamedParameters ? ":_" + keyName : "?";
181        }
182
183        /**
184         * Check mandatory properties.
185         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
186         */
187    @Override
188        public void init(DataSource dataSource) throws Exception {
189                Assert.notNull(dataSource, "A DataSource is required");
190                Assert.hasLength(selectClause, "selectClause must be specified");
191                Assert.hasLength(fromClause, "fromClause must be specified");
192                Assert.notEmpty(sortKeys, "sortKey must be specified");
193                StringBuilder sql = new StringBuilder();
194                sql.append("SELECT ").append(selectClause);
195                sql.append(" FROM ").append(fromClause);
196                if (whereClause != null) {
197                        sql.append(" WHERE ").append(whereClause);
198                }
199                if(groupClause != null) {
200                        sql.append(" GROUP BY ").append(groupClause);
201                }
202                List<String> namedParameters = new ArrayList<String>();
203                parameterCount = JdbcParameterUtils.countParameterPlaceholders(sql.toString(), namedParameters);
204                if (namedParameters.size() > 0) {
205                        if (parameterCount != namedParameters.size()) {
206                                throw new InvalidDataAccessApiUsageException(
207                                                "You can't use both named parameters and classic \"?\" placeholders: " + sql);
208                        }
209                        usingNamedParameters = true;
210                }
211        }
212
213        /**
214         * Method generating the query string to be used for retrieving the first
215         * page. This method must be implemented in sub classes.
216         * 
217         * @param pageSize number of rows to read per page
218         * @return query string
219         */
220    @Override
221        public abstract String generateFirstPageQuery(int pageSize);
222
223        /**
224         * Method generating the query string to be used for retrieving the pages
225         * following the first page. This method must be implemented in sub classes.
226         * 
227         * @param pageSize number of rows to read per page
228         * @return query string
229         */
230    @Override
231        public abstract String generateRemainingPagesQuery(int pageSize);
232
233        /**
234         * Method generating the query string to be used for jumping to a specific
235         * item position. This method must be implemented in sub classes.
236         * 
237         * @param itemIndex the index of the item to jump to
238         * @param pageSize number of rows to read per page
239         * @return query string
240         */
241    @Override
242        public abstract String generateJumpToItemQuery(int itemIndex, int pageSize);
243
244        private String removeKeyWord(String keyWord, String clause) {
245                String temp = clause.trim();
246                int length = keyWord.length();
247                if (temp.toLowerCase().startsWith(keyWord) && Character.isWhitespace(temp.charAt(length)) && temp.length() > length + 1) {
248                        return temp.substring(length + 1);
249                }
250                else {
251                        return temp;
252                }
253        }
254
255        /**
256         *
257         * @return sortKey key to use to sort and limit page content (without alias)
258         */
259        @Override
260        public Map<String, Order> getSortKeysWithoutAliases() {
261                Map<String, Order> sortKeysWithoutAliases = new LinkedHashMap<String, Order>();
262
263                for (Map.Entry<String, Order> sortKeyEntry : sortKeys.entrySet()) {
264                        String key = sortKeyEntry.getKey();
265                        int separator = key.indexOf('.');
266                        if (separator > 0) {
267                                int columnIndex = separator + 1;
268                                if (columnIndex < key.length()) {
269                                        sortKeysWithoutAliases.put(key.substring(columnIndex), sortKeyEntry.getValue());
270                                }
271                        } else {
272                                sortKeysWithoutAliases.put(sortKeyEntry.getKey(), sortKeyEntry.getValue());
273                        }
274                }
275
276                return sortKeysWithoutAliases;
277        }
278}