001    /*
002    @license.text@
003     */
004    package biz.hammurapi.convert;
005    
006    import java.io.File;
007    import java.io.FileReader;
008    import java.io.Reader;
009    import java.lang.reflect.Constructor;
010    import java.lang.reflect.Method;
011    import java.math.BigDecimal;
012    import java.util.ArrayList;
013    import java.util.Collections;
014    import java.util.Comparator;
015    import java.util.Iterator;
016    import java.util.List;
017    
018    import biz.hammurapi.util.ClassHierarchyVisitable;
019    import biz.hammurapi.util.Visitor;
020    import biz.hammurapi.xml.dom.CompositeDomSerializer;
021    import biz.hammurapi.xml.dom.DomSerializable;
022    
023    
024    /**
025     * @author Pavel Vlasov
026     * @version $Revision: 1.7 $
027     */
028    public class CompositeConverter {
029    
030        /**
031         * Creates new composite converter populated by default with some generic converters.
032         */
033        public CompositeConverter() {
034                    _addConverter(Number.class, byte.class, "byteValue", null);
035                    _addConverter(Number.class, double.class, "doubleValue", null);
036                    _addConverter(Number.class, float.class, "floatValue", null);
037                    _addConverter(Number.class, int.class, "intValue", null);
038                    _addConverter(Number.class, long.class, "longValue", null);
039                    _addConverter(Number.class, short.class, "shortValue", null);
040                    
041                    _addConverter(Number.class, Byte.class, "byteValue", null);
042                    _addConverter(Number.class, Double.class, "doubleValue", null);
043                    _addConverter(Number.class, Float.class, "floatValue", null);
044                    _addConverter(Number.class, Integer.class, "intValue", null);
045                    _addConverter(Number.class, Long.class, "longValue", null);
046                    _addConverter(Number.class, Short.class, "shortValue", null);
047                    
048                    discoverConverter(String.class, Integer.class);
049                    discoverConverter(String.class, Long.class);
050                    discoverConverter(String.class, Double.class);
051                    discoverConverter(String.class, Float.class);
052                    discoverConverter(String.class, Byte.class);
053                    discoverConverter(String.class, Short.class);
054                    discoverConverter(String.class, BigDecimal.class);
055                    
056                    try {
057                            _addConverter(File.class, Reader.class, null, FileReader.class.getConstructor(new Class[] {File.class}));
058                    } catch (SecurityException e) {
059                            throw new ConversionException(e);
060                    } catch (NoSuchMethodException e) {
061                            throw new ConversionException(e);
062                    }
063                    
064                    addConverter(String.class, boolean.class, new Converter() {
065    
066                            public Object convert(Object source) {
067                                    if (source==null) {
068                                            return Boolean.FALSE;
069                                    }
070                                    
071                                    String str = ((String) source).trim();
072                                    if (str.length()==0 || "no".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str) || "0".equals(source)) {
073                                            return Boolean.FALSE;
074                                    } else if ("yes".equalsIgnoreCase(str) || "true".equalsIgnoreCase(str) || "1".equals(source)) {
075                                            return Boolean.TRUE;
076                                    } 
077                                    
078                                    throw new ConversionException("Cannot convert string '"+source+"' to boolean");
079                            }
080                            
081                    });
082                    
083                    addConverter(String.class, byte.class, new Converter() {
084    
085                            public Object convert(Object source) {
086                                    return new Byte((String) source);
087                            }
088                            
089                    });
090                    
091                    addConverter(String.class, double.class, new Converter() {
092    
093                            public Object convert(Object source) {
094                                    return new Double((String) source);
095                            }
096                            
097                    });
098                    
099                    addConverter(String.class, float.class, new Converter() {
100    
101                            public Object convert(Object source) {
102                                    return new Float((String) source);
103                            }
104                            
105                    });
106                    
107    
108                    addConverter(String.class, int.class, new Converter() {
109    
110                            public Object convert(Object source) {
111                                    return new Integer((String) source);
112                            }
113                            
114                    });
115                    
116    
117                    addConverter(String.class, long.class, new Converter() {
118    
119                            public Object convert(Object source) {
120                                    return new Long((String) source);
121                            }
122                            
123                    });
124                    
125    
126                    addConverter(String.class, short.class, new Converter() {
127    
128                            public Object convert(Object source) {
129                                    return new Short((String) source);
130                            }
131                            
132                    });
133                    
134                    addConverter(Object.class, DomSerializable.class, new Converter() {
135    
136                            public Object convert(Object source) {                          
137                                    return CompositeDomSerializer.getThreadInstance().toDomSerializable(source);
138                            }
139                            
140                    });
141                    
142        }
143        
144        private boolean immutable;
145    
146        /**
147         * @return true if no additional converters can be added to this converter.
148         */
149            public boolean isImmutable() {
150                    return immutable;
151            }
152            
153            /**
154             * Makes converter immutable.
155             */
156            public void setImmutable() {
157                    this.immutable = true;
158            }
159            
160            private static CompositeConverter defaultConverter;
161            
162            static {
163                    defaultConverter=new CompositeConverter();
164                    defaultConverter.setImmutable();
165            }
166            
167        public static CompositeConverter getDefaultConverter() {
168           return defaultConverter; 
169        }
170        
171            // TODO - chained conversions
172            
173            private static class ReflectionConverter implements Converter {
174                    Method accessor;
175                    Constructor constructor;
176                    
177                    /**
178                     * @param accessor
179                     * @param constructor
180                     */
181                    ReflectionConverter(Method accessor, Constructor constructor) {
182                            super();
183                            this.accessor = accessor;
184                            this.constructor = constructor;
185                    }
186                    
187                    public Object convert(Object source) {
188                            try {
189                                    Object param = accessor==null ? source : accessor.invoke(source, null);
190                                    return constructor==null ? param : constructor.newInstance(new Object[] {param});
191                            } catch (Exception e) {
192                                    throw new ConversionException("Cannot convert "+source.getClass().getName()+" to " + constructor.getDeclaringClass(), e);
193                            }
194                    }
195            }
196            
197            private static class ConverterEntry {
198                    Class source;
199                    Class target;
200            private Converter converter;
201                    
202                    /**
203                     * @param accessor
204                     * @param constructor
205                     */
206                    ConverterEntry(Class source, Class target, Converter converter) {
207                            super();
208                            this.source = source;
209                            this.target = target;
210                            this.converter = converter;
211                    }
212                    
213                    boolean isCompatible(Class source, Class target) {
214                            return this.source.isAssignableFrom(source) && target.isAssignableFrom(this.target);
215                    }
216                    
217                    boolean isCompatible(ConverterEntry otherEntry) {
218                            return isCompatible(otherEntry.source, otherEntry.target);
219                    }               
220            }
221            
222            private List converters=new ArrayList();
223            
224            /**
225             * Adds a converter which uses method of source object and constructor of target object to
226             * perform conversion. 
227             * @param source Source object.
228             * @param target Target class.
229             * @param accessor Method name to invoke on source to obtain intermediate object. Can be null.
230             * @param constructor Target constructor. Can be null.
231             */
232            public Converter addConverter(Class source, Class target, String accessor, Constructor constructor) {
233                    if (immutable) {
234                            throw new ConversionException("Converter is immutable");
235                    }
236                    return _addConverter(source, target, accessor, constructor);
237            }
238            
239            private Converter _addConverter(Class source, Class target, String accessor, Constructor constructor) {
240                    ReflectionConverter ret;
241                    try {
242                            ret = new ReflectionConverter(accessor==null ? null : source.getMethod(accessor, null), constructor);
243                    } catch (SecurityException e) {
244                            throw new ConversionException(e);
245                    } catch (NoSuchMethodException e) {
246                            throw new ConversionException(e);
247                    }
248                    _addConverter(source, target, ret);
249                    return ret;
250            }
251            
252            /**
253             * Adds a converter from source object to target class.
254             * @param source
255             * @param target
256             * @param converter
257             */
258            public void addConverter(Class source, Class target, Converter converter) {
259                    if (immutable) {
260                            throw new ConversionException("Converter is immutable");
261                    }
262                    _addConverter(source, target, converter);
263            }
264                    
265            private void _addConverter(final Class source, Class target, final Converter converter) {
266                    synchronized (converters) {
267                            new ClassHierarchyVisitable(target).accept(
268                                            new Visitor() {
269    
270                                                    public boolean visit(Object target) {
271                                                            Iterator it = converters.iterator();
272                                                            while (it.hasNext()) {
273                                                                    ConverterEntry ce = (ConverterEntry) it.next();
274                                                                    if (ce.source.equals(source) && ce.target.equals(target)) {
275                                                                            return true; // Converter already exists.
276                                                                    }
277                                                            }
278                                                            converters.add(new ConverterEntry(source, (Class) target, converter));                                                  
279                                                            return true;
280                                                    }
281                                                    
282                                            });
283                            
284                            Collections.sort(
285                                            converters, 
286                                            new Comparator() {
287    
288                                                    public int compare(Object o1, Object o2) {
289                                                            if (o1==o2) {
290                                                                    return 0;
291                                                            }
292                                                            
293                                                            if (o1 instanceof ConverterEntry && o2 instanceof ConverterEntry) {
294                                                                ConverterEntry c1=(ConverterEntry) o1;
295                                                                ConverterEntry c2=(ConverterEntry) o2;
296                                                                    
297                                                                    // More specific converters go first.
298                                                                    if (c1.isCompatible(c2)) {
299                                                                            return c2.isCompatible(c1) ? 0 : 1;
300                                                                    }
301                                                                    
302                                                                    if (c2.isCompatible(c1)) {
303                                                                            return -1;
304                                                                    }
305                                                                    
306                                                                    return 0;
307                                                            }
308                                                                                                                    
309                                                            return o1.hashCode()-o2.hashCode();
310                                                    }
311                                                    
312                                            });
313                            
314                    }
315            }
316            
317            private Converter findConverter(Class source, Class target) {
318                    synchronized (converters) {
319                            Iterator it=converters.iterator();
320                            while (it.hasNext()) {
321                                ConverterEntry ret=(ConverterEntry) it.next();
322                                    if (ret.isCompatible(source, target)) {
323                                            return ret.converter;
324                                    }
325                            }
326                            return null;
327                    }
328            }
329            
330        /**
331         * Converts source object to target class instance
332         * @param source Source object
333         * @param target Target class. If target class is String then toString() is always used.
334         * @param lenient When true null is returned if conversion cannot be performed, 
335         * otherwise ConversionException is thrown
336         * @return Instance of target class.
337         * @throws ConversionException If lenient=false and conversion cannot be performed.
338         */
339            public Object convert(Object source, Class target, boolean lenient) {
340                    if (source==null) {
341                            return null;
342                    } else if (target.isInstance(source)) {
343                            return source;
344                    } else if (String.class.equals(target)) {
345                            return source.toString();
346                    } else {
347                            try {
348                                    Converter converter=findConverter(source.getClass(), target);
349                                    
350                                    if (converter==null) {
351                                            converter = discoverConverter(source.getClass(), target);
352                                    }
353                                    
354                                    if (converter==null) {
355                                            throw new ConversionException("No appropriate converter from "+source.getClass()+" to "+target);
356                                    }
357                                    
358                                    return converter.convert(source);
359                            } catch (ConversionException e) {
360                                    if (lenient) {
361                                            return null;
362                                    }
363                                    
364                                    throw e;
365                            }
366                    }
367            }
368    
369            /**
370             * @param class1
371             * @param target
372             * @return Discovered converter or null.
373             */
374            private Converter discoverConverter(Class source, Class target) {
375                    Converter ret=null;
376                    Constructor constructor=null;
377                    Constructor stringConstructor=null;
378                    for (int i=0, cc=target.getConstructors().length; i<cc; i++) {
379                            Constructor candidate = target.getConstructors()[i];
380                            if (candidate.getParameterTypes().length==1) {
381                                    Converter c=_addConverter(candidate.getParameterTypes()[0], candidate.getDeclaringClass(), null, candidate);
382                                    
383                                    if (candidate.getParameterTypes()[0].isInstance(source) && (constructor==null || constructor.getParameterTypes()[0].isAssignableFrom(candidate.getParameterTypes()[0]))) {
384                                            constructor=candidate;
385                                            ret=c;
386                                    }       
387                                    
388                                    if (stringConstructor==null     && java.lang.String.class.equals(candidate.getParameterTypes()[0])) {
389                                            stringConstructor=candidate;
390                                    }                                                               
391                            }                       
392                    }
393                    
394                    if (ret!=null) {
395                            return ret;
396                    }
397                    
398                    if (stringConstructor!=null) {
399                            return _addConverter(Object.class, target, "toString", stringConstructor);
400                    }
401                    
402                    if (target.isInterface()) {
403                            ret = DuckConverterFactory.getConverter(source, target);
404                            if (ret==null) {
405                                    ret=ContextConverterFactory.getConverter(source, target);
406                            }
407                    }
408                    
409                    return ret;
410            }
411    
412        
413    }