001/**
002 * Copyright 2014 Tampere University of Technology, Pori Department
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 */
016package core.tut.pori.dao;
017
018import java.util.ArrayList;
019import java.util.HashSet;
020import java.util.Iterator;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map.Entry;
024
025import org.apache.commons.lang3.StringUtils;
026import org.apache.log4j.Logger;
027import org.springframework.dao.DataAccessException;
028import org.springframework.jdbc.core.JdbcTemplate;
029
030import core.tut.pori.dao.clause.JoinClause;
031import core.tut.pori.dao.clause.SQLClause;
032import core.tut.pori.dao.clause.SQLClause.SQLType;
033import core.tut.pori.dao.clause.WhereClause;
034import core.tut.pori.datatypes.IntPrimitiveList;
035import core.tut.pori.http.parameters.Limits;
036
037
038/**
039 * Helper class, which can be used to build an SQL SELECT clause.
040 */
041public class SQLSelectBuilder {
042  private static final Logger LOGGER = Logger.getLogger(SQLSelectBuilder.class);
043  private static final String SORT_ORDER_RANDOM = " RAND()";
044  private ArrayList<String> _groupBy = null;
045  private ArrayList<JoinClause> _joins = null;
046  private Limits _limits = null;
047  private LinkedHashMap<String, OrderDirection> _orderBy = null;
048  private HashSet<String> _selectColumns = new HashSet<>();
049  private String _tableName = null;
050  private ArrayList<WhereClause> _whereClauses = null;
051
052  /**
053   * The direction of an sort order in SQL order by clause.
054   */
055  public enum OrderDirection{
056    /** ascending order */
057    ASCENDING(" ASC"),
058    /** descending order */
059    DESCENDING(" DESC");
060
061    private String _value;
062
063    /**
064     * 
065     * @param value
066     */
067    private OrderDirection(String value){
068      _value = value;
069    }
070
071    /**
072     * 
073     * @return order direction as a string
074     */
075    public String toOrderDirectionString(){
076      return _value;
077    }
078  } // enum OrderDirection
079
080  /**
081   * 
082   * @param table
083   */
084  public SQLSelectBuilder(String table){
085    _tableName = table;
086  }
087  
088  /**
089   * clear the currently set list of where clauses
090   */
091  public void clearWhereClauses(){
092    _whereClauses = null;
093  }
094  
095  /**
096   * 
097   * @return number of added where clauses
098   */
099  public int getWhereClauseCount() {
100    return (_whereClauses == null ? 0 : _whereClauses.size());
101  }
102  
103  /**
104   * 
105   * @param clause
106   */
107  public void addJoin(JoinClause clause){
108    if(_joins == null){
109      _joins = new ArrayList<>();
110    }
111    _joins.add(clause);
112  }
113  
114  /**
115   * 
116   * @param column
117   */
118  public void addGroupBy(String column){
119    if(_groupBy == null){
120      _groupBy = new ArrayList<>();
121    }
122    _groupBy.add(column);
123  }
124
125  /**
126   * @param startItem the startItem to set, negative value disables limit
127   */
128  public void setStartItem(int startItem) {
129    if(_limits == null){
130      _limits = new Limits(startItem, Limits.DEFAULT_MAX_ITEMS);
131    }else{
132      _limits.setTypeLimits(startItem, _limits.getEndItem(), null);
133    }
134  }
135
136  /**
137   * @param maxItems the maxItems to set, negative value disables limit
138   */
139  public void setMaxItems(int maxItems) {
140    if(_limits == null){
141      _limits = new Limits(0, maxItems-1);
142    }else{
143      _limits.setTypeLimits(_limits.getStartItem(), maxItems-1, null);
144    }
145  }
146  
147  /**
148   * 
149   * @param limits if null, this is a no-op
150   */
151  public void setLimits(Limits limits){
152    _limits = limits;
153  }
154  
155  /**
156   * 
157   * @param template to use for retrieval
158   * @return record count for the given sql query or &lt; 0 on error
159   */
160  public long getRecordCount(JdbcTemplate template){
161    return getRecordCount(template, null);  
162  }
163  
164  /**
165   * 
166   * @param template
167   * @param type
168   * @return record count (SQL COUNT(*)) for the given select query parameters
169   */
170  public long getRecordCount(JdbcTemplate template, String type){
171    try{
172      Long count = template.queryForObject(toSQLString(true, type), getValues(), getValueTypes(), Long.class);
173      return (count == null ? 0 : count);
174    } catch (DataAccessException ex){
175      LOGGER.error(ex, ex);
176      return -1;
177    } 
178  }
179  
180  /**
181   * 
182   * @return value types or null if none
183   */
184  public int[] getValueTypes(){
185    return getValueTypes(_whereClauses);
186  }
187  
188  /**
189   * 
190   * @param clauses
191   * @return value types or null if none
192   */
193  protected static int[] getValueTypes(List<? extends SQLClause> clauses){
194    if(clauses == null || clauses.isEmpty()){
195      return null;
196    }
197    
198    IntPrimitiveList list = new IntPrimitiveList();
199    for(Iterator<? extends SQLClause> iter = clauses.iterator();iter.hasNext();){
200      SQLClause c = iter.next();
201      SQLType[] t = c.getValueTypes();
202      if(t != null){
203        for(int i=0;i<t.length;++i){
204          list.add(t[i].toInt());
205        }
206      }
207    }
208    return list.toArray();
209  }
210  
211  /**
212   * 
213   * @return values in an array or null if none
214   */
215  public Object[] getValues(){
216    return getValues(_whereClauses);
217  }
218  
219  /**
220   * 
221   * @param clauses
222   * @return array of values or null if none
223   */
224  protected static Object[] getValues(List<? extends SQLClause> clauses){
225    if(clauses == null || clauses.isEmpty()){
226      return null;
227    }
228    
229    List<Object> objects = new ArrayList<>();
230    for(Iterator<? extends SQLClause> iter = clauses.iterator();iter.hasNext();){
231      SQLClause c = iter.next();
232      Object[] o = c.getValues();
233      if(o != null){
234        for(int i=0;i<o.length;++i){
235          objects.add(o[i]);
236        }
237      }
238    }
239    if(objects.isEmpty()){
240      return null;
241    }else{
242      return objects.toArray();
243    }
244  }
245
246  /**
247   * 
248   * @return the query as a SQL string
249   */
250  public String toSQLString(){
251    return toSQLString(false, null);
252  }
253  
254  /**
255   * 
256   * @param type
257   * @return the query as a SQL string using type information for parameters (e.g. Limits). See {@link core.tut.pori.http.parameters.Limits}
258   */
259  public String toSQLString(String type){
260    return toSQLString(false, type);
261  }
262  
263  /**
264   * helper method for generating SQL query string
265   * 
266   * @param onlyCount
267   * @param type
268   * @return the query as a SQL string
269   */
270  private String toSQLString(boolean onlyCount, String type){
271    StringBuilder sql = new StringBuilder("SELECT ");
272    
273    /* create select columns */
274    if(onlyCount){
275      sql.append("COUNT (*) FROM ");
276    }else if(_selectColumns.isEmpty()){
277      sql.append("* FROM ");
278    }else{
279      Iterator<String> iter = _selectColumns.iterator();
280      sql.append(iter.next());
281      while(iter.hasNext()){
282        sql.append(',');
283        sql.append(iter.next());
284      }
285      sql.append(" FROM ");
286    }
287    sql.append(_tableName);
288    
289    /* create joins */
290    if(_joins != null){
291      for(Iterator<JoinClause> iter = _joins.iterator();iter.hasNext();){
292        iter.next().toSQLString(sql);
293      }
294    }
295
296    /* create where clauses */
297    if(_whereClauses != null){
298      sql.append(" WHERE ");
299      Iterator<WhereClause> iter = _whereClauses.iterator();
300      iter.next().toSQLString(sql);
301      while(iter.hasNext()){
302        WhereClause clause = iter.next();
303        sql.append(clause.getClauseType().toClauseString());
304        clause.toSQLString(sql);
305      }
306    }
307    
308    /* create group by */
309    if(_groupBy != null){
310      sql.append(" GROUP BY ");
311      Iterator<String> gIter = _groupBy.iterator();
312      sql.append(gIter.next());
313      while(gIter.hasNext()){
314        sql.append(',');
315        sql.append(gIter.next());
316      }
317    }
318
319    /* create order by */
320    if(_orderBy != null){
321      sql.append(" ORDER BY ");
322      for(Entry<String, OrderDirection> e : _orderBy.entrySet()){
323        String column = e.getKey();
324        if(StringUtils.isBlank(column)){
325          sql.append(SORT_ORDER_RANDOM);
326        }else{
327          sql.append(column);
328          sql.append(e.getValue().toOrderDirectionString());
329        }
330        sql.append(',');
331      }
332      sql.setLength(sql.length()-1);  // chop the tailing ,
333    }
334    
335    /* create limit */
336    if(_limits != null){
337      sql.append(" LIMIT ");
338      sql.append(_limits.getStartItem(type));
339      sql.append(',');
340      sql.append(_limits.getMaxItems());
341    }
342    
343    return sql.toString();
344  }
345
346  /**
347   * 
348   * @param column null column name is accepted, though it equals to random sort order
349   * @param direction
350   */
351  public void addOrderBy(String column, OrderDirection direction){
352    if(_orderBy == null){
353      _orderBy = new LinkedHashMap<>();
354    }
355    _orderBy.put(column,direction);
356  }
357
358  /**
359   * 
360   * @param clause
361   */
362  public void addWhereClause(WhereClause clause){
363    if(_whereClauses == null){
364      _whereClauses = new ArrayList<>();
365    }
366    _whereClauses.add(clause);
367  }
368
369  /**
370   * 
371   * @param column
372   */
373  public void addSelectColumn(String column){
374    _selectColumns.add(column);
375  }
376
377  /**
378   * 
379   * @param columns
380   */
381  public void addSelectColumns(String[] columns){
382    for(int i=0;i<columns.length;++i){
383      _selectColumns.add(columns[i]);
384    }
385  }
386}