1 /*
2  * JasperReports - Free Java Reporting Library.
3  * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
4  * http://www.jaspersoft.com
5  *
6  * Unless you have purchased a commercial license agreement from Jaspersoft,
7  * the following license terms apply:
8  *
9  * This program is part of JasperReports.
10  *
11  * JasperReports is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * JasperReports is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
23  */

24 package net.sf.jasperreports.engine.query;
25
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.concurrent.ConcurrentHashMap;
34
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37
38 import net.sf.jasperreports.engine.JRRuntimeException;
39 import net.sf.jasperreports.engine.JRValueParameter;
40 import net.sf.jasperreports.engine.util.Pair;
41
42 /**
43  * @author Lucian Chirita (lucianc@users.sourceforge.net)
44  */

45 public class ParameterTypeSelectorClauseFunction implements JRClauseFunction
46 {
47     
48     private static final Log log = LogFactory.getLog(ParameterTypeSelectorClauseFunction.class);
49     public static final String EXCEPTION_MESSAGE_KEY_QUERY_PARAMETER_TYPE_SELECTOR_CLAUSE_IMPLEMENTATION_NOT_FOUND = "query.parameter.type.selector.clause.implementation.not.found";
50     public static final String EXCEPTION_MESSAGE_KEY_QUERY_PARAMETER_TYPE_SELECTOR_CLAUSE_REQUIRED_TOKEN_NOT_FOUND = "query.parameter.type.selector.clause.required.token.not.found";
51     
52     private static final String CONTEXT_KEY_FUNCTION_PER_TYPES_CACHE = 
53             "net.sf.jasperreports.engine.query.ParameterTypeSelectorClauseFunction.cache";
54     
55     private final int[] parameterPositions;
56     
57     public ParameterTypeSelectorClauseFunction(int ... parameterPositions)
58     {
59         this.parameterPositions = parameterPositions;
60     }
61
62     @Override
63     public void apply(JRClauseTokens clauseTokens, JRQueryClauseContext queryContext)
64     {
65         List<Class<?>> parameterTypes = new ArrayList<Class<?>>(parameterPositions.length);
66         for (int position : parameterPositions)
67         {
68             Class<?> parameterType = determineParameterType(clauseTokens,
69                     queryContext, position);
70             parameterTypes.add(parameterType);
71         }
72         
73         JRClauseFunction function = getForParameterTypes(clauseTokens, queryContext, parameterTypes);
74         if (function == null)
75         {
76             throw 
77                 new JRRuntimeException(
78                     EXCEPTION_MESSAGE_KEY_QUERY_PARAMETER_TYPE_SELECTOR_CLAUSE_IMPLEMENTATION_NOT_FOUND,
79                     new Object[]{clauseTokens.getClauseId(), parameterTypes});
80         }
81         
82         function.apply(clauseTokens, queryContext);
83     }
84
85     protected Class<?> determineParameterType(JRClauseTokens clauseTokens,
86             JRQueryClauseContext queryContext, int parameterPosition)
87     {
88         String parameterName = clauseTokens.getToken(parameterPosition);
89         if (parameterName == null)
90         {
91             throw 
92                 new JRRuntimeException(
93                     EXCEPTION_MESSAGE_KEY_QUERY_PARAMETER_TYPE_SELECTOR_CLAUSE_IMPLEMENTATION_NOT_FOUND,
94                     new Object[]{parameterPosition, clauseTokens.getClauseId()});
95         }
96         
97         // the method throws an exception if it doesn't find the parameter, 
98         // so we don't have to handle that case here
99         JRValueParameter parameter = queryContext.getValueParameter(parameterName);
100         
101         Class<?> parameterType;
102         Object parameterValue = parameter.getValue();
103         if (parameterValue == null)
104         {
105             // if we don't have a value, use the declared type
106             parameterType = parameter.getValueClass();
107         }
108         else
109         {
110             // use the actual value
111             parameterType = parameterValue.getClass();
112         }
113         
114         if (log.isDebugEnabled())
115         {
116             log.debug("query clause parameter " + parameterName 
117                     + " at position " + parameterPosition 
118                     + " has type " + parameterType.getName());
119         }
120         
121         return parameterType;
122     }
123
124     protected JRClauseFunction getForParameterTypes(JRClauseTokens clauseTokens, 
125             JRQueryClauseContext queryContext, List<Class<?>> parameterTypes)
126     {
127         Map<Object, JRClauseFunction> cache = getCache(queryContext);
128         Object typesKey = parameterTypesFunctionCacheKey(clauseTokens, queryContext, parameterTypes);
129         JRClauseFunction function = cache.get(typesKey);
130         if (function == null)
131         {
132             function = selectForParameterTypes(clauseTokens, queryContext, parameterTypes);
133             cache.put(typesKey, function);
134         }
135         else
136         {
137             if (log.isDebugEnabled())
138             {
139                 log.debug("found cached function " + function 
140                         + for clause " + clauseTokens.getClauseId() + " with types " + parameterTypes);
141             }
142         }
143         return function;
144     }
145
146     protected Map<Object, JRClauseFunction> getCache(
147             JRQueryClauseContext queryContext)
148     {
149         @SuppressWarnings("unchecked")
150         Map<Object, JRClauseFunction> cache = (Map<Object, JRClauseFunction>) queryContext.getJasperReportsContext().getOwnValue(
151                 CONTEXT_KEY_FUNCTION_PER_TYPES_CACHE);
152         if (cache == null)
153         {
154             // we need a concurrent map as the context and cache might be used by several threads
155             cache = new ConcurrentHashMap<Object, JRClauseFunction>();
156             
157             // we don't need to handle race conditions here as it's a lightweight cache
158             queryContext.getJasperReportsContext().setValue(CONTEXT_KEY_FUNCTION_PER_TYPES_CACHE, cache);
159         }
160         return cache;
161     }
162
163     protected Object parameterTypesFunctionCacheKey(JRClauseTokens clauseTokens, JRQueryClauseContext queryContext, 
164             List<Class<?>> parameterTypes)
165     {
166         Object typesKey;
167         int size = parameterTypes.size();
168         if (size == 1)
169         {
170             // use the single type as direct key
171             typesKey = parameterTypes.get(0);
172         }
173         else if (size == 2)
174         {
175             // use a pair of the two types
176             typesKey = new Pair<Class<?>, Class<?>>(parameterTypes.get(0), parameterTypes.get(1));
177         }
178         else
179         {
180             // use the list itself as key
181             typesKey = parameterTypes;
182         }
183         
184         Pair<String, String> clauseKey = new Pair<String, String>(
185                 queryContext.getCanonicalQueryLanguage(), clauseTokens.getClauseId());
186         return new Pair<Pair<String, String>, Object>(clauseKey, typesKey);
187     }
188
189     protected JRClauseFunction selectForParameterTypes(JRClauseTokens clauseTokens, 
190             JRQueryClauseContext queryContext, List<Class<?>> parameterTypes)
191     {
192         String queryLanguage = queryContext.getCanonicalQueryLanguage();
193         String clauseId = clauseTokens.getClauseId();
194         
195         if (log.isDebugEnabled())
196         {
197             log.debug("selecting clause function " + clauseId + for language " + queryLanguage
198                     + " and parameter types " + parameterTypes);
199         }
200         
201         // fetch extensions
202         List<ParameterTypesClauseFunctionBundle> functionsBundles = queryContext.getJasperReportsContext().getExtensions(
203                 ParameterTypesClauseFunctionBundle.class);
204         List<Pair<List<Class<?>>, JRClauseFunction>> candidateFunctions = new ArrayList<Pair<List<Class<?>>,JRClauseFunction>>();
205         for (ParameterTypesClauseFunctionBundle functionsBundle : functionsBundles)
206         {
207             Collection<? extends ParameterTypesClauseFunction> functions = functionsBundle.getTypeFunctions(queryLanguage, clauseId);
208             if (functions != null)
209             {
210                 // collect candidates by checking the types
211                 for (ParameterTypesClauseFunction typesFunction : functions)
212                 {
213                     List<Class<?>> supportedTypes = findSupportedTypes(typesFunction, parameterTypes);
214                     if (supportedTypes != null)
215                     {
216                         JRClauseFunction function = typesFunction.getFunction();
217                         if (log.isDebugEnabled())
218                         {
219                             log.debug("found candidate function " + function
220                                     + for types " + supportedTypes);
221                         }
222                         
223                         Pair<List<Class<?>>, JRClauseFunction> candidate = 
224                                 new Pair<List<Class<?>>, JRClauseFunction>(supportedTypes, function);
225                         candidateFunctions.add(candidate);
226                     }
227                 }
228             }
229         }
230         
231         return selectFromCandidates(candidateFunctions);
232     }
233
234     protected JRClauseFunction selectFromCandidates(List<Pair<List<Class<?>>, JRClauseFunction>> candidateFunctions)
235     {
236         if (candidateFunctions.isEmpty())
237         {
238             return null;
239         }
240         
241         if (candidateFunctions.size() == 1)
242         {
243             // only one candidate, return it
244             return candidateFunctions.get(0).second();
245         }
246         
247         // sort the candidates based on type specificity
248         Collections.sort(candidateFunctions, TypesCandidateComparator.INSTANCE);
249         // and return the first (which is actually the most specific or one of the most specific)
250         JRClauseFunction function = candidateFunctions.get(0).second();
251         if (log.isDebugEnabled())
252         {
253             log.debug("selected function " + function);
254         }
255         return function;
256     }
257
258     protected List<Class<?>> findSupportedTypes(
259             ParameterTypesClauseFunction typesFunction,
260             List<Class<?>> parameterTypes)
261     {
262         Collection<Class<?>> functionTypes = typesFunction.getSupportedTypes();
263         List<Class<?>> supportedTypes = new ArrayList<Class<?>>(parameterTypes.size());
264         for (Class<?> paramType : parameterTypes)
265         {
266             Class<?> supportedType = findSupportedType(functionTypes, paramType);
267             if (supportedType == null)
268             {
269                 break;
270             }
271             else
272             {
273                 supportedTypes.add(supportedType);
274             }
275         }
276         
277         if (supportedTypes.size() == parameterTypes.size())
278         {
279             // we found a supported type for each parameter
280             return supportedTypes;
281         }
282         return null;
283     }
284     
285     protected Class<?> findSupportedType(Collection<Class<?>> supportedTypes, Class<?> parameterType)
286     {
287         for (Class<?> supportedType : supportedTypes)
288         {
289             if (supportedType.isAssignableFrom(parameterType))
290             {
291                 return supportedType;
292             }
293         }
294         return null;
295     }
296 }
297
298 final class TypesCandidateComparator implements Comparator<Pair<List<Class<?>>, JRClauseFunction>>
299 {
300     public static final String EXCEPTION_MESSAGE_KEY_QUERY_PARAMETER_TYPE_SELECTOR_CANDIDATE_TYPE_SIZE_MISMATCH = "query.parameter.type.selector.candidate.type.size.mismatch";
301
302     protected static final TypesCandidateComparator INSTANCE = new TypesCandidateComparator();
303     
304     private TypesCandidateComparator()
305     {
306     }
307     
308     @Override
309     public int compare(Pair<List<Class<?>>, JRClauseFunction> o1,
310             Pair<List<Class<?>>, JRClauseFunction> o2)
311     {
312         List<Class<?>> types1 = o1.first();
313         List<Class<?>> types2 = o2.first();
314         
315         // should not happen, but checking
316         if (types1.size() != types2.size())
317         {
318             throw 
319                 new JRRuntimeException(
320                     EXCEPTION_MESSAGE_KEY_QUERY_PARAMETER_TYPE_SELECTOR_CANDIDATE_TYPE_SIZE_MISMATCH,
321                     new Object[]{types1.size(), types2.size()});
322         }
323         
324         // perform a lexicographical comparison by comparing each type sequentially until we find a difference
325         int order = 0;
326         for (Iterator<Class<?>> it1 = types1.iterator(), it2 = types2.iterator(); it1.hasNext() && it2.hasNext(); )
327         {
328             Class<?> type1 = it1.next();
329             Class<?> type2 = it2.next();
330             int typesOrder = compareTypes(type1, type2);
331             if (typesOrder != 0)
332             {
333                 order = typesOrder;
334                 break;
335             }
336         }
337         
338         return order;
339     }
340     
341     protected int compareTypes(Class<?> type1, Class<?> type2)
342     {
343         if (type1.equals(type2))
344         {
345             return 0;
346         }
347         
348         // more specific classes are "smaller"
349         if (type1.isAssignableFrom(type2))
350         {
351             return 1;
352         }
353         
354         if (type2.isAssignableFrom(type1))
355         {
356             return -1;
357         }
358         
359         // if the classes are independent, return an arbitrary order
360         return type1.getName().compareTo(type2.getName());
361     }
362     
363 }