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.jpa2.statement;
021
022import java.sql.ResultSet;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import javax.persistence.Query;
030
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import net.sf.jkniv.exception.HandleableException;
035import net.sf.jkniv.experimental.TimerKeeper;
036import net.sf.jkniv.reflect.NumberFactory;
037import net.sf.jkniv.reflect.Numerical;
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.LanguageType;
042import net.sf.jkniv.sqlegance.RepositoryException;
043import net.sf.jkniv.sqlegance.dialect.SqlDialect;
044import net.sf.jkniv.sqlegance.dialect.SqlFeatureSupport;
045import net.sf.jkniv.sqlegance.logger.DataMasking;
046import net.sf.jkniv.sqlegance.statement.ColumnParserFactory;
047import net.sf.jkniv.whinstone.Param;
048import net.sf.jkniv.whinstone.Queryable;
049import net.sf.jkniv.whinstone.ResultRow;
050import net.sf.jkniv.whinstone.classification.Groupable;
051import net.sf.jkniv.whinstone.classification.GroupingBy;
052import net.sf.jkniv.whinstone.classification.Transformable.TransformableType;
053import net.sf.jkniv.whinstone.statement.AutoKey;
054import net.sf.jkniv.whinstone.statement.StatementAdapter;
055import net.sf.jkniv.whinstone.types.Convertible;
056import net.sf.jkniv.whinstone.types.RegisterType;
057import net.sf.jkniv.whinstone.types.NoConverterType;
058
059public class JpaStatementAdapter<T, R> implements StatementAdapter<T, ResultSet>
060{
061    private static final Logger       LOG     = LoggerFactory.getLogger(JpaStatementAdapter.class);
062    private static final Logger       SQLLOG  = net.sf.jkniv.whinstone.jpa2.LoggerFactory.getLogger();
063    private static final DataMasking  MASKING = net.sf.jkniv.whinstone.jpa2.LoggerFactory.getDataMasking();
064    private final Query               query;
065    private int                       index;
066    private final HandleableException handlerException;
067    private Queryable                 queryable;
068    private boolean                   scalar;
069    
070    public JpaStatementAdapter(Query query, Queryable queryable, HandleableException handlerException)
071    {
072        this.query = query;
073        this.queryable = queryable;
074        this.handlerException = handlerException;
075        this.reset();
076    }
077    
078    @Override
079    public StatementAdapter<T, ResultSet> bind(String name, Object value)
080    {
081        log(new Param(value, name, index));
082        //Convertible<Object,Object> convertible = getConverter(new PropertyAccess(name));
083        query.setParameter(++index, value);
084        return this;
085    }
086    
087    @Override
088    public StatementAdapter<T, ResultSet> bind(Param... values)
089    {
090        for (int j=0; j < values.length; j++)
091        {
092            Param param = values[j];
093            log(param);
094            query.setParameter(++index, param.getValueAs());
095        }
096        return this;
097    }
098    
099    @Override
100    public int reset()
101    {
102        int before = index;
103        index = 0;
104        return before;
105    }
106    
107    @Override
108    public StatementAdapter<T, ResultSet> bind(Param value)
109    {
110        log(value);
111        query.setParameter(++index, value.getValue());
112        return this;
113    }
114    
115    @Override
116    public StatementAdapter<T, ResultSet> with(ResultRow<T, ResultSet> resultRow)
117    {
118        //this.resultRow = resultRow;
119        return this;
120    }
121    
122    @Override
123    public void bindKey()
124    {
125        // FIXME auto-key forJPA has, How is behavior with NATIVE query
126        //        String[] properties = queryable.getDynamicSql().asInsertable().getAutoGeneratedKey().getPropertiesAsArray();
127        //        ObjectProxy<?> proxy = ObjectProxyFactory.newProxy(queryable.getParams());
128        //        Iterator<Object> it = autoKey.iterator();
129        //        for(int i=0; i<properties.length; i++)
130        //            setValueOfKey(proxy, properties[i], it.next());        
131    }
132    
133    @Override
134    public StatementAdapter<T, ResultSet> with(AutoKey generateKey)
135    {
136        // FIXME auto-key for JPA native , How is behavior with NATIVE query
137        return this;
138    }
139    
140    /*
141    @Override
142    public List<T> rows()
143    {
144        //ResultSet rs = null;
145        //ResultSetParser<T, ResultSet> rsParser = null;
146        Groupable<T, ?> grouping = new NoGroupingBy<T, T>();
147        List<T> list = Collections.emptyList();
148        try
149        {
150            TimerKeeper.start();
151            rs = query.getResultList();
152            if (queryable != null)
153                queryable.getDynamicSql().getStats().add(TimerKeeper.clear());
154            
155            JdbcColumn<ResultSet>[] columns = getJdbcColumns(rs.getMetaData());
156            setResultRow(columns);
157            
158            Transformable<T> transformable = resultRow.getTransformable();
159            if (!groupingBy.isEmpty())
160            {
161                grouping = new GroupingBy(groupingBy, queryable.getReturnType(), transformable);
162            }
163            rsParser = new ObjectResultSetParser(resultRow, grouping);
164            list = rsParser.parser(rs);
165        }
166        catch (SQLException e)
167        {
168            if (queryable != null)
169                queryable.getDynamicSql().getStats().add(e);
170            handlerException.handle(e, e.getMessage());
171        }
172        return list;
173    }
174    */
175    
176    @Override
177    @SuppressWarnings("unchecked")
178    public List<T> rows()
179    {
180        List<T> list = Collections.emptyList();
181        try
182        {
183            TimerKeeper.start();
184            if (queryable.isPaging()) 
185            {
186                SqlDialect sqlDialect = queryable.getDynamicSql().getSqlDialect();
187                if (sqlDialect.supportsFeature(SqlFeatureSupport.LIMIT))
188                    query.setMaxResults(queryable.getMax());
189                    if (sqlDialect.supportsFeature(SqlFeatureSupport.LIMIT_OFF_SET))
190                        query.setFirstResult(queryable.getOffset());
191            }
192            TimerKeeper.start();
193            list = query.getResultList();
194            queryable.getDynamicSql().getStats().add(TimerKeeper.clear());
195            
196            if (queryable.getDynamicSql().getLanguageType() == LanguageType.NATIVE
197                    && queryable.getDynamicSql().hasReturnType() && list.size() > 0)
198            {
199                list = cast((List<Object[]>) list, queryable.getDynamicSql().getReturnTypeAsClass());
200            }
201            int totalBeforeGroup = list.size();
202            
203            list = handleGroupingBy(list);
204            if (LOG.isDebugEnabled())
205                LOG.debug("Executed [{}] query, {}/{} rows fetched transformed to -> {}", queryable.getName(),
206                        totalBeforeGroup, queryable.getTotal(), list.size());
207        }
208        catch (Exception e)
209        {
210            queryable.getDynamicSql().getStats().add(e);
211            handlerException.handle(e);
212        }
213        finally {
214            TimerKeeper.clear();
215        }
216        
217        return list;
218    }
219    
220    @SuppressWarnings("unchecked")
221    private List<T> handleGroupingBy(List<T> list)
222    {
223        List<T> newList = Collections.emptyList();
224        List<String> groupingBy = Collections.emptyList();
225        groupingBy = queryable.getDynamicSql().asSelectable().getGroupByAsList();
226        if (!list.isEmpty() && !groupingBy.isEmpty())
227        {
228            Class<T> returnedType = (Class<T>) list.get(0).getClass();
229            Groupable<T, T> grouping = new GroupingBy<T, T>(groupingBy, returnedType, TransformableType.OBJECT);
230            for (T row : list)
231                grouping.classifier(row);
232            
233            newList = grouping.asList();
234        }
235        else
236            newList = list;
237        return newList;
238    }
239    
240    /**
241     * Make the conversion from object list to another T
242     * @param list list of original values
243     * @param returnType class type of {@code T}
244     * @param <T> class type 
245     * @return list of casted objects
246     */
247    @SuppressWarnings("unchecked")
248    private List<T> cast(List<?> list, Class<?> returnType)// TODO test me case when jpa return array of objects (native query or select specific columns
249    {
250        List<T> castedList = null;
251        Object firstValue = list.get(0);
252        if (firstValue.getClass().getName().equals(returnType) || returnType == null)
253        {
254            castedList = (List<T>)list;
255        }
256        else if (firstValue instanceof Number)
257        {
258            Numerical factory = NumberFactory.getInstance(returnType.getName());
259            castedList = new ArrayList<T>(list.size());
260            List<?> listArray = (List<?>) list;
261            for (Object o : listArray)
262            {
263                T casted = (T) factory.valueOf(o);
264                castedList.add(casted);
265            }
266        }
267        else if (firstValue instanceof String)
268        {
269            castedList = new ArrayList<T>(list.size());
270            List<?> listArray = (List<?>) list;
271            for (Object o : listArray)
272            {
273                castedList.add((T)String.valueOf(o));
274            }
275        }
276        //        else if(BASIC_TYPE.isBasicType(firstValue.getClass()))
277        //        {
278        //            castedList = new ArrayList<T>(list.size());
279        //            List<?> listArray = (List<?>)list;
280        //            for (Object o : listArray)
281        //            {
282        //                ObjectProxy<T> proxy = ObjectProxyFactory.newProxy(returnType);
283        //                proxy.setConstructorArgs(o);
284        //                T casted = proxy.newInstance();
285        //                castedList.add(casted);
286        //            }
287        //        }
288        else if (Map.class.isAssignableFrom(returnType))
289        {
290            // TODO check match between columns size and object array
291            String[] columns = ColumnParserFactory.getInstance().extract(queryable.query());
292            castedList = new ArrayList<T>(list.size());
293            List<Object[]> listArray = (List<Object[]>) list;
294            for (Object[] tupla : listArray)
295            {
296                Map<String, Object> map = new HashMap<String, Object>();
297                for(int i=0; i<tupla.length; i++)
298                    map.put(columns[i], tupla[i]);
299                
300                castedList.add((T)map);
301            }
302        }
303        else if (returnType != null)
304        {
305            castedList = new ArrayList<T>(list.size());
306            List<Object[]> listArray = (List<Object[]>) list;
307            for (Object[] o : listArray)
308            {
309                ObjectProxy<?> proxy = ObjectProxyFactory.of(returnType);
310                proxy.setConstructorArgs(o);
311                T casted = (T)proxy.newInstance();
312                castedList.add(casted);
313            }            
314        }
315        if (castedList.size() != list.size())
316            throw new RepositoryException("Wrong conversion type from List<Object[]> to List of [" + returnType + "]");
317        return castedList;
318    }
319    
320    /*
321    @Override
322    public void batch()
323    {
324        // TODO batch adapter
325    }
326    */
327    @Override
328    public int execute()
329    {
330        int ret = 0;
331        try
332        {
333            TimerKeeper.start();
334            ret = query.executeUpdate();
335            queryable.getDynamicSql().getStats().add(TimerKeeper.clear());
336        }
337        catch (Exception e)
338        {
339            queryable.getDynamicSql().getStats().add(e);
340            handlerException.handle(e, e.getMessage());
341        }
342        return ret;
343    }
344    
345    @Override
346    public void close()
347    {
348        // TODO implements close for JpaStatementAdapter
349        LOG.warn("close Statement Adapter not implemented for RepositoryJpa!");
350    }
351    
352    @Override
353    public void setFetchSize(int rows)
354    {
355        query.setMaxResults(rows);
356    }
357    
358    private void log(Param param)
359    {
360        if (SQLLOG.isDebugEnabled())
361            SQLLOG.debug("Setting SQL Parameter from index [{}] with name [{}] with value of [{}] type of [{}]", index,
362                    param.getName(), MASKING.mask(param.getName(), param.getValue()), (param.getValue() == null ? "NULL" : param.getValue().getClass()));
363    }
364    
365    private void log(int position, Object value)
366    {
367        String name = String.valueOf(position);
368        if (SQLLOG.isDebugEnabled())
369            SQLLOG.debug("Setting SQL Parameter from index [{}] with name [{}] with value of [{}] type of [{}]", index,
370                    name, MASKING.mask(name, value), (value == null ? "NULL" : value.getClass()));
371    }
372    
373    private boolean hasGroupBy()
374    {
375        return !queryable.getDynamicSql().asSelectable().getGroupByAsList().isEmpty();
376    }
377}