001    package biz.hammurapi.convert;
002    
003    import java.lang.reflect.InvocationTargetException;
004    import java.lang.reflect.Method;
005    import java.util.ArrayList;
006    import java.util.Collection;
007    import java.util.Collections;
008    import java.util.HashMap;
009    import java.util.HashSet;
010    import java.util.Iterator;
011    import java.util.List;
012    import java.util.Map;
013    import java.util.Set;
014    
015    import biz.hammurapi.config.Context;
016    import biz.hammurapi.config.DomConfigFactory;
017    
018    public class ConvertingService {
019            
020            private interface ConvertersBucket {
021                    
022                    Object convert(Object source, Context context);
023            }
024            
025            /**
026             * Converter which delegates to convert() method.
027             */
028            public static final Converter CONVERTER = new Converter() {
029    
030                    public Object convert(Object source, Class targetType, Context context) throws ConversionException {
031                            return ConvertingService.convert(source, targetType, context);
032                    }
033                    
034            };                      
035            
036            /**
037             * Converts source to target type using converters loaded from 
038             * META-INF/config/biz.hammurapi.convert.Converter resources.
039             * If no converter is found, this method performs converter discovery through
040             * constructors if target type is a class and through duck conversion if target
041             * type is an interface. 
042             * Invocation of this method is equivalent to invocation of  
043             * <code>convert(source, targetType, false)</code>
044             * @param source Source object.
045             * @param targetType Target type.
046             * @return Source object converted to target type or null if no converter
047             * is available for source/targetType combination
048             * @throws ConversionException
049             */
050            public static Object convert(Object source, Class targetType) throws ConversionException {
051                    return convert(source, targetType, null, false, null);
052            }
053            
054            /**
055             * Converts source to target type using converters loaded from 
056             * META-INF/config/biz.hammurapi.convert.Converter resources.
057             * If no converter is found, this method performs converter discovery through
058             * constructors if target type is a class and through duck conversion if target
059             * type is an interface. 
060             * Invocation of this method is equivalent to invocation of  
061             * <code>convert(source, targetType, false)</code>
062             * @param source Source object.
063             * @param targetType Target type.
064             * @param context Conversion context.
065             * @return Source object converted to target type or null if no converter
066             * is available for source/targetType combination
067             * @throws ConversionException
068             */
069            public static Object convert(Object source, Class targetType, Context context) throws ConversionException {
070                    return convert(source, targetType, context, false, null);
071            }
072            
073            /**
074             * Converts source to target type using converters loaded from 
075             * META-INF/config/biz.hammurapi.convert.Converter resources.
076             * If no converter is found, this method performs converter discovery through
077             * constructors if target type is a class and through duck conversion if target
078             * type is an interface. 
079             * Invocation of this method is equivalent to invocation of  
080             * <code>convert(source, targetType, false)</code>
081             * @param source Source object.
082             * @param targetType Target type.
083             * @param classLoader Class loader, source of converters.
084             * @return Source object converted to target type or null if no converter
085             * is available for source/targetType combination
086             * @throws ConversionException
087             */
088            public static Object convert(Object source, Class targetType, ClassLoader classLoader) throws ConversionException {
089                    return convert(source, targetType, null, false, classLoader);
090            }
091            
092            private static class ConverterKey {
093                    Class sourceType;
094                    Class targetType;
095                    
096                    public ConverterKey(Class sourceType, Class targetType) {
097                            super();
098                            this.sourceType = sourceType;
099                            this.targetType = targetType;
100                    }
101    
102                    public int hashCode() {
103                            final int prime = 31;
104                            int result = 1;
105                            result = prime * result + ((sourceType == null) ? 0 : sourceType.hashCode());
106                            result = prime * result + ((targetType == null) ? 0 : targetType.hashCode());
107                            return result;
108                    }
109    
110                    public boolean equals(Object obj) {
111                            return 
112                                    sourceType == ((ConverterKey) obj).sourceType 
113                                    && targetType == ((ConverterKey) obj).targetType; 
114                    }               
115                    
116            }
117            
118            private static class ClassLoaderConvertersEntry {
119                    private static final String CONVERT = "convert";
120                    
121                    private Converter master = new Converter() {
122    
123                            public Object convert(Object source, Class targetType, Context context) throws ConversionException {
124                                    if (source==null) {
125                                            return null;
126                                    }
127                                    
128                                    if (targetType.isInstance(source)) {
129                                            return source;
130                                    }
131                                    
132                                    final ConvertersBucket bucket = findConverter(source.getClass(), targetType);
133                                    return bucket==null ? null : bucket.convert(source, context);
134                            }
135                            
136                    };
137    
138                    private Converter chainingMaster = new Converter() {
139    
140                            public Object convert(Object source, Class targetType, Context context) throws ConversionException {
141                                    if (source==null) {
142                                            return null;
143                                    }
144                                    
145                                    if (targetType.isInstance(source)) {
146                                            return source;
147                                    }
148                                    
149                                    ConvertersBucket bucket = findChainingConverter(source.getClass(), targetType);
150                                    return bucket==null ? null : bucket.convert(source, context);
151                            }
152                            
153                    };
154    
155                    Collection atomicConverters = new ArrayList();
156                    
157                    /**
158                     * Map of resolved converters: Collection[source, target] -&gt; ConverterClosure. 
159                     */
160                    Map converters = new HashMap(); 
161                    
162                    Set introspectedTargetTypes = new HashSet();
163    
164                    /**
165                     * Map of resolved chaining converters: Collection[source, target] -&gt; ConverterClosure. 
166                     */
167                    Map chainingConverters = new HashMap();
168                    
169                    synchronized ConvertersBucket findConverter(Class sourceType, Class targetType) {
170                            ConverterKey key = new ConverterKey(sourceType, targetType);
171                            Object ret = converters.get(key);
172                            if (Boolean.FALSE.equals(ret)) { // Indicates that previous converter resolution didn't yield results.
173                                    return null;
174                            }
175                            
176                            if (ret == null) {
177                                    if (!targetType.isInterface() && introspectedTargetTypes.add(targetType)) {
178                                            atomicConverters.addAll(ReflectionConverter.discoverConstructorConverters(targetType));
179                                    }
180                                    final List theConverters  = new ArrayList();
181                                    
182                                    class ConverterEntry implements Comparable {
183                                            public ConverterEntry(AtomicConverter converter, int affinity, int position) {
184                                                    super();
185                                                    this.converter = converter;
186                                                    this.affinity = affinity;
187                                                    this.position = position;
188                                            }
189                                            AtomicConverter converter;
190                                            int affinity;
191                                            int position;
192                                            public int compareTo(Object obj) {
193                                                    int cret = affinity - ((ConverterEntry) obj).affinity;
194                                                    if (cret==0) {
195                                                            return position - ((ConverterEntry) obj).position;
196                                                    }
197                                                    return cret;
198                                            }
199                                    }
200                                    
201                                    Iterator it = atomicConverters.iterator();
202                                    while (it.hasNext()) {
203                                            AtomicConverter ac = (AtomicConverter) it.next();
204                                            int position = 0;
205                                            if (ac.getSourceType().isAssignableFrom(sourceType) && targetType.isAssignableFrom(ac.getTargetType()) ) {
206                                                    Integer newSourceAffinity = DuckConverterFactory.classAffinity(sourceType, ac.getSourceType());
207                                                    Integer newTargetAffinity = DuckConverterFactory.classAffinity(ac.getTargetType(), targetType);
208                                                    if (newSourceAffinity==null || newTargetAffinity==null) {
209                                                            throw new IllegalArgumentException("kosyak");
210                                                    }
211                                                    theConverters.add(new ConverterEntry(ac, newSourceAffinity.intValue()+newTargetAffinity.intValue(), ++position));
212                                            }
213                                    }
214                                    if (theConverters.isEmpty()) {
215                                            converters.put(key, Boolean.FALSE);
216                                    } else {
217                                            Collections.sort(theConverters);
218                                            ret = new ConvertersBucket() {
219    
220                                                    public Object convert(Object source, Context context) {
221                                                            Iterator it = theConverters.iterator();
222                                                            while (it.hasNext()) {
223                                                                    ConverterEntry ce = (ConverterEntry) it.next();
224                                                                    Object ret = ce.converter.convert(source, master, context);
225                                                                    if (ret!=null) {
226                                                                            return ret;
227                                                                    }
228                                                            }
229                                                            return null;
230                                                    }
231                                                    
232                                            };
233                                            converters.put(key, ret);
234                                    }
235                            }
236                            
237                            return (ConvertersBucket) ret;
238                    }
239    
240                    synchronized ConvertersBucket findChainingConverter(Class sourceType, Class targetType) {
241                            ConvertersBucket ret = findConverter(sourceType, targetType);
242                            if (ret!=null) {
243                                    return ret;
244                            }
245                            
246                            // TODO 777 - Resolve chain.
247                            return ret;
248                    }
249                    
250                    void addConverters(final Object provider) {
251                            if (provider instanceof AtomicConverter) {
252                                    atomicConverters.add(provider);
253                            } else if (provider instanceof AtomicConvertersBundle) {
254                                    atomicConverters.addAll(((AtomicConvertersBundle) provider).getConverters());
255                            } else if (provider instanceof Collection) {
256                                    Iterator it = ((Collection) provider).iterator();
257                                    while (it.hasNext()) {
258                                            addConverters(it.next());
259                                    }
260                            } else { // Introspection
261                                    Class pClass = provider.getClass();
262                                    Method[] methods = pClass.getMethods();
263                                    for (int i=0; i<methods.length; ++i) {
264                                            final Method method = methods[i];
265                                            if (CONVERT.equals(method.getName()) && !void.class.equals(method.getReturnType())) {
266                                                    Class[] pTypes = method.getParameterTypes();
267                                                    if (pTypes.length == 1) {
268                                                            atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {
269    
270                                                                    public Object convert(Object source, Converter master, Context context) {
271                                                                            try {
272                                                                                    return method.invoke(provider, new Object[] {source});
273                                                                            } catch (IllegalAccessException e) {
274                                                                                    throw new ConversionException(e);
275                                                                            } catch (InvocationTargetException e) {
276                                                                                    throw new ConversionException(e);
277                                                                            }
278                                                                    }
279                                                                    
280                                                            });
281                                                    } else if (pTypes.length == 2 && Converter.class.equals(pTypes[1])) {
282                                                            atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {
283    
284                                                                    public Object convert(Object source, Converter master, Context context) {
285                                                                            try {
286                                                                                    return method.invoke(provider, new Object[] {source, master});
287                                                                            } catch (IllegalAccessException e) {
288                                                                                    throw new ConversionException(e);
289                                                                            } catch (InvocationTargetException e) {
290                                                                                    throw new ConversionException(e);
291                                                                            }
292                                                                    }
293                                                                    
294                                                            });
295                                                            
296                                                    } else if (pTypes.length == 2 && Context.class.equals(pTypes[1])) {
297                                                            atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {
298    
299                                                                    public Object convert(Object source, Converter master, Context context) {
300                                                                            try {
301                                                                                    return method.invoke(provider, new Object[] {source, context});
302                                                                            } catch (IllegalAccessException e) {
303                                                                                    throw new ConversionException(e);
304                                                                            } catch (InvocationTargetException e) {
305                                                                                    throw new ConversionException(e);
306                                                                            }
307                                                                    }
308                                                                    
309                                                            });
310                                                            
311                                                    } else if (pTypes.length == 3 && Converter.class.equals(pTypes[1]) && Context.class.equals(pTypes[2])) {
312                                                            atomicConverters.add(new AtomicConverterBase(pTypes[0], method.getReturnType()) {
313    
314                                                                    public Object convert(Object source, Converter master, Context context) {
315                                                                            try {
316                                                                                    return method.invoke(provider, new Object[] {source, master, context});
317                                                                            } catch (IllegalAccessException e) {
318                                                                                    throw new ConversionException(e);
319                                                                            } catch (InvocationTargetException e) {
320                                                                                    throw new ConversionException(e);
321                                                                            }
322                                                                    }
323                                                                    
324                                                            });
325                                                            
326                                                    }
327                                                    
328                                            }
329                                    }
330                            }
331                    }
332                    
333            }
334            
335            /**
336             * Map of resolved converters: ClassLoader -&gt; ClassLoaderConvertersEntry. 
337             */
338            private static Map classLoaderEntries = new HashMap();          
339    
340            /**
341             * Converts source to target type using converters loaded from 
342             * <code>META-INF/config/biz.hammurapi.convert.Converter</code> resources.
343             * If no converter is found, this method performs converter discovery through
344             * constructors if target type is a class and through duck conversion if target
345             * type is an interface. 
346             * @param source Source object.
347             * @param targetType Target type.
348             * @param context Conversion context.
349             * @param classLoader Class loader.
350             * @return Source object converted to target type or null if no converter
351             * is available for source/targetType combination
352             * @throws ConversionException
353             */
354            public static Object convert(Object source, Class targetType, Context context, ClassLoader classLoader) throws ConversionException {
355                    return convert(source, targetType, context, false, classLoader);
356            }
357            
358            /**
359             * Converts source to target type using converters loaded from 
360             * <code>META-INF/config/biz.hammurapi.convert.Converter</code> resources.
361             * If no converter is found, this method performs converter discovery through
362             * constructors if target type is a class and through duck conversion if target
363             * type is an interface. 
364             * @param source Source object.
365             * @param targetType Target type.
366             * @param chain If true, the converter will attempt to build a chain of 
367             * conversions from source type to target type. 
368             * For example, StringBuffer doesn't have a constructor from Integer. If chain
369             * is <code>false</code> then this method will return <code>null</code>. If 
370             * <code>chain</code> is true, then the converter will convert Integer to String
371             * and then will create StringBuffer from it. If multiple conversion chains 
372             * are available, the shortest is chosen.
373             * @return Source object converted to target type or null if no converter
374             * is available for source/targetType combination
375             * @throws ConversionException
376             */
377            private static Object convert(Object source, Class targetType, Context context, boolean chain, ClassLoader classLoader) throws ConversionException {
378                    if (source==null) {
379                            return null;
380                    }
381                    
382                    if (targetType.isInstance(source)) {
383                            return source;
384                    }
385                    
386                    ConverterClosure converter = getConverter(source.getClass(), targetType, context, chain, classLoader);
387                    return converter==null ? null : converter.convert(source);              
388            }
389            
390            /**
391             * Converts source to target type using converters loaded from 
392             * <code>META-INF/config/biz.hammurapi.convert.Converter</code> resources.
393             * If no converter is found, this method performs converter discovery through
394             * constructors if target type is a class and through duck conversion if target
395             * type is an interface. 
396             * @param sourceType Source type.
397             * @param targetType Target type.
398             * @return Converter from source type to target type, or null if such converter
399             * is not available.
400             * @throws ConversionException
401             */     
402            public static ConverterClosure getConverter(Class sourceType, Class targetType) { 
403                    return getConverter(sourceType, targetType, null, false, null);
404            }
405            
406            /**
407             * Converts source to target type using converters loaded from 
408             * <code>META-INF/config/biz.hammurapi.convert.Converter</code> resources.
409             * If no converter is found, this method performs converter discovery through
410             * constructors if target type is a class and through duck conversion if target
411             * type is an interface. 
412             * @param sourceType Source type.
413             * @param targetType Target type.
414             * @param classLoader Class loader to retrieve converters from.
415             * @return Converter from source type to target type, or null if such converter
416             * is not available.
417             * @throws ConversionException
418             */     
419            public static ConverterClosure getConverter(Class sourceType, Class targetType, ClassLoader classLoader) { 
420                    return getConverter(sourceType, targetType, null, false, classLoader);
421            }
422            
423            /**
424             * Converts source to target type using converters loaded from 
425             * <code>META-INF/config/biz.hammurapi.convert.Converter</code> resources.
426             * If no converter is found, this method performs converter discovery through
427             * constructors if target type is a class and through duck conversion if target
428             * type is an interface. 
429             * @param sourceType Source type.
430             * @param targetType Target type.
431             * @param context Conversion context.
432             * @return Converter from source type to target type, or null if such converter
433             * is not available.
434             * @throws ConversionException
435             */     
436            public static ConverterClosure getConverter(Class sourceType, Class targetType, Context context) { 
437                    return getConverter(sourceType, targetType, context, false, null);
438            }
439            
440            /**
441             * Converts source to target type using converters loaded from 
442             * <code>META-INF/config/biz.hammurapi.convert.Converter</code> resources.
443             * If no converter is found, this method performs converter discovery through
444             * constructors if target type is a class and through duck conversion if target
445             * type is an interface. 
446             * @param sourceType Source type.
447             * @param targetType Target type.
448             * @param context Conversion context.
449             * @param classLoader Class loader to retrieve converters from.
450             * @return Converter from source type to target type, or null if such converter
451             * is not available.
452             * @throws ConversionException
453             */     
454            public static ConverterClosure getConverter(Class sourceType, Class targetType, Context context, ClassLoader classLoader) { 
455                    return getConverter(sourceType, targetType, context, false, classLoader);
456            }
457            
458            private static ClassLoader promote(ClassLoader present, ClassLoader candidate) {
459                    ClassLoader ret = DuckConverterFactory.getChildClassLoader(present, candidate);
460                    return ret==null ? present : ret;
461            }
462            
463            /**
464             * 
465             * @param sourceType
466             * @param targetType
467             * @param context
468             * @param chain If true, the converter will attempt to build a chain of 
469             * conversions from source type to target type. 
470             * For example, StringBuffer doesn't have a constructor from Integer. If chain
471             * is <code>false</code> then this method will return <code>null</code>. If 
472             * <code>chain</code> is true, then the converter will convert Integer to String
473             * and then will create StringBuffer from it. If multiple conversion chains 
474             * are available, the shortest is chosen.
475             * @param classLoader
476             * @return
477             */
478            private static ConverterClosure getConverter(Class sourceType, Class targetType, final Context context, boolean chain, final ClassLoader pClassLoader) {
479                    ClassLoader classLoader = promote(pClassLoader, promote(targetType.getClassLoader(), sourceType.getClassLoader()));
480                                            
481                    ClassLoaderConvertersEntry clce;
482                    synchronized (classLoaderEntries) {
483                            clce = (ClassLoaderConvertersEntry) classLoaderEntries.get(classLoader);
484                            if (clce == null) {
485                                    clce = new ClassLoaderConvertersEntry();
486                                    Iterator pit = DomConfigFactory.loadProviders(Converter.class, classLoader);
487                                    while (pit.hasNext()) {
488                                            clce.addConverters(pit.next());
489                                    }
490                                    classLoaderEntries.put(classLoader, clce);
491                            }                       
492                    }
493                    final ConvertersBucket bucket = chain ? clce.findChainingConverter(sourceType, targetType) : clce.findConverter(sourceType, targetType);
494                    return bucket==null ? null : new ConverterClosure() {
495    
496                            public Object convert(Object source) {
497                                    return bucket.convert(source, context);
498                            }
499                            
500                    };
501            }
502    }