001/* 
002 * JKNIV, whinstone one contract to access your database.
003 * 
004 * Copyright (C) 2017, the original author or authors.
005 *
006 * This library is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 2.1 of the License.
010 * 
011 * This library is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 * 
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with this library; if not, write to the Free Software Foundation, Inc., 
018 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
019 */
020package net.sf.jkniv.whinstone.jdbc.statement;
021
022import java.sql.PreparedStatement;
023import java.sql.ResultSet;
024import java.sql.ResultSetMetaData;
025import java.sql.SQLException;
026import java.util.Collections;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.slf4j.Logger;
033
034import net.sf.jkniv.exception.HandlerException;
035import net.sf.jkniv.experimental.TimerKeeper;
036import net.sf.jkniv.reflect.beans.CapitalNameFactory;
037import net.sf.jkniv.reflect.beans.Capitalize;
038import net.sf.jkniv.reflect.beans.ObjectProxy;
039import net.sf.jkniv.reflect.beans.ObjectProxyFactory;
040import net.sf.jkniv.reflect.beans.PropertyAccess;
041import net.sf.jkniv.sqlegance.OneToMany;
042import net.sf.jkniv.sqlegance.RepositoryException;
043import net.sf.jkniv.sqlegance.logger.DataMasking;
044import net.sf.jkniv.whinstone.JdbcColumn;
045import net.sf.jkniv.whinstone.Param;
046import net.sf.jkniv.whinstone.Queryable;
047import net.sf.jkniv.whinstone.ResultRow;
048import net.sf.jkniv.whinstone.ResultSetParser;
049import net.sf.jkniv.whinstone.classification.Groupable;
050import net.sf.jkniv.whinstone.classification.GroupingBy;
051import net.sf.jkniv.whinstone.classification.NoGroupingBy;
052import net.sf.jkniv.whinstone.classification.Transformable;
053import net.sf.jkniv.whinstone.jdbc.DefaultJdbcColumn;
054import net.sf.jkniv.whinstone.jdbc.LoggerFactory;
055import net.sf.jkniv.whinstone.statement.AutoKey;
056import net.sf.jkniv.whinstone.statement.StatementAdapter;
057import net.sf.jkniv.whinstone.types.Convertible;
058import net.sf.jkniv.whinstone.types.NoConverterType;
059
060/**
061 * 
062 * @param <T>
063 * @param <R>
064 * 
065 * @author Alisson Gomes
066 * @since 0.6.0
067 */
068@SuppressWarnings({ "unchecked", "rawtypes" })
069public class JdbcPreparedStatementAdapter<T, R> implements StatementAdapter<T, ResultSet>
070{
071    private static final Logger      LOG     = LoggerFactory.getLogger();
072    private static final Logger      SQLLOG  = net.sf.jkniv.whinstone.jdbc.LoggerFactory.getLogger();
073    private static final DataMasking MASKING = LoggerFactory.getDataMasking();
074    private static final Capitalize  CAPITAL_SETTER  = CapitalNameFactory.getInstanceOfSetter();
075    
076    private final PreparedStatement  stmt;
077    private final HandlerException   handlerException;
078    private int                      index;
079    private ResultRow<T, ResultSet>  resultRow;
080    private Queryable                queryable;
081    private AutoKey                  autoKey;
082    private String[]                 paramNames;
083    
084    public JdbcPreparedStatementAdapter(PreparedStatement stmt, Queryable queryable)
085    {
086        this.stmt = stmt;
087        // FIXME handler exception for default message with custom params
088        this.handlerException = new HandlerException(RepositoryException.class, "Cannot set parameter [%s] value [%s]");
089        this.queryable = queryable;
090        this.paramNames = this.queryable.getParamsNames();
091        this.reset();
092    }
093    
094    @Override
095    public StatementAdapter<T, ResultSet> with(ResultRow<T, ResultSet> resultRow)
096    {
097        this.resultRow = resultRow;
098        return this;
099    }
100    
101    // FIXME Converter/Mapping/Translate values from/to jdbc
102    @Override
103    public StatementAdapter<T, ResultSet> bind(String name, Object value)
104    {
105        log(name, value);
106        try {
107            if (name.toLowerCase().startsWith("in:"))
108            {
109                setValue((Object[]) value);
110            }
111            else
112            {
113                setValue(new Param(value, name, this.index));
114            }
115        }
116        catch (SQLException e)
117        {
118            this.handlerException.handle(e);
119        }
120        return this;
121    }
122    
123    // FIXME Converter/Mapping/Translate values from/to jdbc
124    @Override
125    public StatementAdapter<T, ResultSet> bind(Param param)
126    {
127        log(param);
128        setValue(param);
129        return this;
130    }
131    
132    @Override
133    public StatementAdapter<T, ResultSet> bind(Param... values)
134    {
135        for (int j = 0; j < values.length; j++)
136        {
137            Param v = values[j];
138            bind(v);
139        }
140        return this;
141    }
142    
143    @Override
144    public List<T> rows()
145    {
146        ResultSet rs = null;
147        ResultSetParser<T, ResultSet> rsParser = null;
148        Groupable<T, ?> grouping = new NoGroupingBy<T, T>();
149        List<T> list = Collections.emptyList();
150        try
151        {
152            TimerKeeper.start();
153            rs = stmt.executeQuery();
154            queryable.getDynamicSql().getStats().add(TimerKeeper.clear());
155            
156            JdbcColumn<ResultSet>[] columns = getJdbcColumns(rs.getMetaData());
157            setResultRow(columns);
158            
159            Transformable<T> transformable = resultRow.getTransformable();
160            if (hasGroupingBy())
161            {
162                grouping = new GroupingBy(getGroupingBy(), queryable.getReturnType(), transformable);
163            }
164            rsParser = new ObjectResultSetParser(resultRow, grouping);
165            list = rsParser.parser(rs);
166        }
167        catch (SQLException e)
168        {
169            queryable.getDynamicSql().getStats().add(e);
170            handlerException.handle(e, e.getMessage());
171        }
172        finally
173        {
174            TimerKeeper.clear();
175        }
176        return list;
177    }
178    
179    @Override
180    public void bindKey()
181    {
182        String[] properties = queryable.getDynamicSql().asInsertable().getAutoGeneratedKey().getPropertiesAsArray();
183        ObjectProxy<?> proxy = ObjectProxyFactory.of(queryable.getParams());
184        Iterator<Object> it = autoKey.iterator();
185        for (int i = 0; i < properties.length; i++)
186            setValueOfKey(proxy, properties[i], it.next());
187        /*
188        try
189        {
190            if (!this.queryable.isTypeOfArray() && !this.queryable.isTypeOfCollection())
191            {
192                ObjectProxy<?> proxy = ObjectProxyFactory.newProxy(queryable.getParams());
193                ResultSet generatedKeys = stmt.getGeneratedKeys();
194                
195                if (generatedKeys.next())
196                {
197                    for(int i=0; i<properties.length; i++)
198                        setValue(proxy, properties[i], generatedKeys.getObject(i+1));
199                }
200                while(generatedKeys.next())
201                {
202                    for(int i=0; i<properties.length; i++)
203                        setValue(proxy, properties[i], generatedKeys.getObject(i+1));
204                }
205            }
206            else if(this.queryable.isTypeOfMap())
207            {
208                Map<String, Object> instance = (Map)queryable.getParams();
209                ResultSet generatedKeys = stmt.getGeneratedKeys();
210                if (generatedKeys.next())
211                {
212                    for(int i=0; i<properties.length; i++)
213                        instance.put(properties[i], generatedKeys.getObject(i+1));
214                }
215                while(generatedKeys.next())
216                {
217                    for(int i=0; i<properties.length; i++)
218                        instance.put(properties[i], generatedKeys.getObject(i+1));
219                }                
220            }
221            else
222                handlerException.throwMessage("Cannot set auto generated key for collections or array instance of parameters at query [%s]", queryable.getName());
223        }
224        catch (SQLException sqle)
225        {
226            handlerException.handle(sqle);
227        }
228        */
229    }
230    
231    @Override
232    public StatementAdapter<T, ResultSet> with(AutoKey autoKey)
233    {
234        this.autoKey = autoKey;
235        return this;
236    }
237    
238    public int execute()
239    {
240        int ret = 0;
241        try
242        {
243            TimerKeeper.start();
244            ret = stmt.executeUpdate();
245            queryable.getDynamicSql().getStats().add(TimerKeeper.clear());
246        }
247        catch (SQLException e)
248        {
249            queryable.getDynamicSql().getStats().add(e);
250            handlerException.handle(e, e.getMessage());
251        }
252        return ret;
253    }
254    
255    @Override
256    public int reset()
257    {
258        int before = index;
259        index = 1;
260        return before;
261    }
262    
263    private void setValueOfKey(ObjectProxy<?> proxy, String property, Object value)
264    {
265        Convertible<Object, Object> converter = queryable.getRegisterType().toJdbc(new PropertyAccess(property, proxy.getTargetClass()), proxy);
266        Object parsedValue = value;
267        if (!converter.getType().isInstance(value))
268            parsedValue = converter.toAttribute(value);
269        proxy.invoke(CAPITAL_SETTER.does(property), parsedValue);
270    }
271    
272    private void setValue(Param param)
273    {
274        try
275        {
276            int i = currentIndex();
277            stmt.setObject(i, param.getValueAs());
278        }
279        catch (SQLException e)
280        {
281            this.handlerException.handle(e);
282        }     
283    }
284    
285    private void setValue(Object[] paramsIN) throws SQLException
286    {
287        int j = 0;
288        for (; j < paramsIN.length; j++) {
289            Convertible<Object, Object> convertible = getConverter(this.paramNames[index+j-1]);
290            stmt.setObject(index + j, convertible.toJdbc(paramsIN[j]));
291        }
292    }
293    
294    /**
295     * Retrieve a {@link Convertible} instance to customize the
296     * value of parameter to database field.
297     * @param fieldName name of field
298     * @return A convertible instance if found into class proxy or {@link NoConverterType}
299     * instance when the field or method is not annotated.
300     */
301    private Convertible<Object, Object> getConverter(String fieldName)
302    {
303        Convertible<Object, Object> convertible = NoConverterType.getInstance();
304        if(queryable.getParams() != null &&
305           (queryable.isTypeOfPojo() || queryable.isTypeOfCollectionPojo() || queryable.isTypeOfArrayPojo()))
306        {
307            ObjectProxy<?> proxy = ObjectProxyFactory.of(queryable.getParams());
308            convertible = queryable.getRegisterType().toJdbc(new PropertyAccess(fieldName, queryable.getParams().getClass()), proxy);
309        }
310        return convertible;
311    }
312    
313    private void setResultRow(JdbcColumn<ResultSet>[] columns)
314    {
315        Class<?> returnType = queryable.getReturnType();
316        if (resultRow != null)
317            return;
318        
319        if (queryable.isScalar())
320        {
321            resultRow = new ScalarResultRow(columns);
322        }
323        else if (Map.class.isAssignableFrom(returnType))
324        {
325            resultRow = new MapResultRow(returnType, columns);
326        }
327        else if (Number.class.isAssignableFrom(returnType)) 
328            // FIXME implements for date, calendar, boolean improve design
329        {
330            resultRow = new NumberResultRow(returnType, columns);
331        }
332        else if (String.class.isAssignableFrom(returnType))
333        {
334            resultRow = new StringResultRow(columns);
335        }
336        else if (Boolean.class.isAssignableFrom(returnType))
337        {
338            resultRow = new BooleanResultRow(columns);
339        }
340        else if (!hasOneToMany())
341        {
342            resultRow = new FlatObjectResultRow(returnType, columns);
343        }
344        else
345        {
346            resultRow = new PojoResultRow(returnType, columns, getOneToMany(), queryable.getRegisterType());
347        }
348    }
349    
350    private int currentIndex()
351    {
352        return (index++);
353    }
354    
355    private void log(String name, Object value)
356    {
357        if (SQLLOG.isDebugEnabled())
358            SQLLOG.debug("Setting SQL Parameter from index [{}] with name [{}] with value of [{}] type of [{}]", index,
359                    name, MASKING.mask(name, value), (value == null ? "NULL" : value.getClass()));
360    }
361    
362    private void log(Param param)
363    {
364        //String name = this.paramNames[index-1];
365        if (SQLLOG.isDebugEnabled())
366            SQLLOG.debug("Setting SQL Parameter from index [{}] with name [{}] with value of [{}] type of [{}]", param.getIndex(),
367                    param.getName(), MASKING.mask(param.getName(), param.getValue()), (param.getValue() == null ? "NULL" : param.getValue().getClass()));
368    }
369    
370    /**
371     * Summarize the columns from SQL result in binary data or not.
372     * @param metadata  object that contains information about the types and properties of the columns in a <code>ResultSet</code> 
373     * @return Array of columns with name and index
374     * @throws SQLException Errors that occurs when access {@code ResultSetMetaData} methods.
375     */
376    private JdbcColumn<ResultSet>[] getJdbcColumns(ResultSetMetaData metadata) throws SQLException
377    {
378        JdbcColumn<ResultSet>[] columns = new JdbcColumn[metadata.getColumnCount()];
379        for (int i = 0; i < columns.length; i++)
380        {
381            int columnNumber = i + 1;
382            String columnName = getColumnName(metadata, columnNumber);
383            int columnType = metadata.getColumnType(columnNumber);
384            columns[i] = new DefaultJdbcColumn(columnNumber, columnName, columnType, queryable.getRegisterType(), queryable.getReturnType());
385        }
386        return columns;
387    }
388    
389    private String getColumnName(ResultSetMetaData metaData, int columnIndex) throws SQLException
390    {
391        try
392        {
393            return metaData.getColumnLabel(columnIndex);
394        }
395        catch (SQLException e)
396        {
397            return metaData.getColumnName(columnIndex);
398        }
399    }
400    
401    @Override
402    public void close()
403    {
404        if (this.stmt != null)
405        {
406            try
407            {
408                this.stmt.close();
409            }
410            catch (SQLException e)
411            {
412                LOG.warn("Cannot close prepared statement from query [{}]", this.queryable.getName(), e);
413            }
414        }
415    }
416    
417    @Override
418    public void setFetchSize(int rows)
419    {
420        try
421        {
422            this.stmt.setFetchSize(rows);
423        }
424        catch (SQLException e)
425        {
426            // TODO handler exception for Statement setFetchSize exception
427            this.handlerException.handle(e);
428        }
429    }
430    
431    private boolean hasOneToMany()
432    {
433        return !queryable.getDynamicSql().asSelectable().getOneToMany().isEmpty();
434    }
435    
436    private Set<OneToMany> getOneToMany()
437    {
438        return queryable.getDynamicSql().asSelectable().getOneToMany();
439    }
440    
441    private boolean hasGroupingBy()
442    {
443        return !queryable.getDynamicSql().asSelectable().getGroupByAsList().isEmpty();
444    }
445    
446    private List<String> getGroupingBy()
447    {
448        return queryable.getDynamicSql().asSelectable().getGroupByAsList();
449    }
450}