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<String, Boolean> 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}