View Javadoc

1   package org.apache.velocity.tools.config;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.Arrays;
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.Locale;
26  import org.apache.commons.beanutils.Converter;
27  import org.apache.commons.beanutils.converters.BooleanConverter;
28  import org.apache.commons.beanutils.converters.StringConverter;
29  import org.apache.velocity.tools.ClassUtils;
30  import org.apache.velocity.tools.ConversionUtils;
31  
32  /**
33   * <p>This class represents configured data.  If added to a
34   * {@link FactoryConfiguration}, its values will be made
35   * available in the application-scoped toolboxes
36   * produced by any ToolboxFactory configured using
37   * that configuration.</p>
38   * <p>This class also implements all the functionality of
39   * {@link Property}s, which may added to <strong>any</strong>
40   * {@link Configuration} subclass, including
41   * {@link ToolConfiguration}, {@link ToolboxConfiguration},
42   * and {@link FactoryConfiguration}.  In other words,
43   * anything you can do in a {@link Data} configuration, you
44   * can do with a {@link Property}.</p>
45   * <p>Some features supported here are:
46   * <ul>
47   * <li>built in {@link Type}s for strings, booleans, numbers, fields 
48   *     and lists thereof</li>
49   * <li>auto-conversion of numbers, booleans and fields in data
50   *     with no explicit type</li>
51   * <li>support for any Commons-BeanUtils {@link Converter} implementation</li>
52   * </ul>
53   * </p>
54   *
55   * @author Nathan Bubna
56   * @version $Id: Data.java 511959 2007-02-26 19:24:39Z nbubna $
57   */
58  public class Data implements Comparable<Data>
59  {
60      protected static final Type DEFAULT_TYPE = Type.AUTO;
61  
62      private String key;
63      private String typeValue;
64      private Object value;
65      private boolean isList;
66      private Class target;
67      private Converter converter;
68  
69      public Data()
70      {
71          setType(DEFAULT_TYPE);
72      }
73  
74      public void setKey(String key)
75      {
76          this.key = key;
77      }
78  
79      public void setValue(Object value)
80      {
81          this.value = value;
82      }
83  
84      public void setClassname(String classname)
85      {
86          try
87          {
88              setTargetClass(ClassUtils.getClass(classname));
89          }
90          catch (ClassNotFoundException cnfe)
91          {
92              throw new IllegalArgumentException("Class "+classname+" could not be found.", cnfe);
93          }
94      }
95  
96      /**
97       * This doesn't take a {@link Class} parameter because
98       * this class was not created for all-java configuration.
99       */
100     public void setClass(String classname)
101     {
102         setClassname(classname);
103     }
104 
105     protected void setType(Type type)
106     {
107         this.isList = type.isList();
108 
109         // make sure we don't override a custom target or converter
110         if (!type.isCustom())
111         {
112             this.typeValue = type.value();
113             this.target = type.getTarget();
114             this.converter = type.getConverter();
115         }
116         else if (type.isList())
117         {
118             // go ahead and set the target and type value for custom lists
119             this.typeValue = type.value();
120             this.target = type.getTarget();
121         }
122     }
123 
124     public void setType(String t)
125     {
126         // save the set type value (good for error feedback and whatnot)
127         this.typeValue = t;
128         // and try to convert it to a Type
129         Type type = Type.get(this.typeValue);
130         if (type != null)
131         {
132             setType(type);
133         }
134     }
135 
136     public void setTargetClass(Class clazz)
137     {
138         this.target = clazz;
139     }
140 
141     public void setConverter(Class clazz)
142     {
143         try
144         {
145             convertWith((Converter)clazz.newInstance());
146         }
147         catch (Exception e)
148         {
149             throw new IllegalArgumentException("Class "+clazz+" is not a valid "+Converter.class, e);
150         }
151     }
152 
153     public void setConverter(String classname)
154     {
155         try
156         {
157             convertWith((Converter)ClassUtils.getInstance(classname));
158         }
159         catch (Exception e)
160         {
161             throw new IllegalArgumentException("Class "+classname+" is not a valid "+Converter.class, e);
162         }
163     }
164 
165     /**
166      * This is a convenience method for those doing configuration in java.
167      * It cannot be named setConverter(), or else it would confuse BeanUtils.
168      */
169     public void convertWith(Converter converter)
170     {
171         this.converter = converter;
172     }
173 
174     public String getKey()
175     {
176         return this.key;
177     }
178 
179     public String getType()
180     {
181         return this.typeValue;
182     }
183 
184     public Object getValue()
185     {
186         return this.value;
187     }
188 
189     public Class getTargetClass()
190     {
191         return this.target;
192     }
193 
194     public Converter getConverter()
195     {
196         return this.converter;
197     }
198 
199     public Object getConvertedValue()
200     {
201         return convert(this.value);
202     }
203 
204     public void validate()
205     {
206         // make sure the key is not null
207         if (getKey() == null)
208         {
209             throw new NullKeyException(this);
210         }
211 
212         // make sure we have value and that it can be converted
213         if (getValue() == null)
214         {
215             throw new ConfigurationException(this, "No value has been set for '"+getKey()+'\'');
216         }
217         else if (this.converter != null)
218         {
219             try
220             {
221                 if (getConvertedValue() == null && getValue() != null)
222                 {
223                     throw new ConfigurationException(this, "Conversion of "+getValue()+" for '"+getKey()+"' failed and returned null");
224                 }
225             }
226             catch (Throwable t)
227             {
228                 throw new ConfigurationException(this, t);
229             }
230         }
231     }
232 
233     public int compareTo(Data datum)
234     {
235         if (getKey() == null && datum.getKey() == null)
236         {
237             return 0;
238         }
239         else if (getKey() == null)
240         {
241             return -1;
242         }
243         else if (datum.getKey() == null)
244         {
245             return 1;
246         }
247         else
248         {
249             return getKey().compareTo(datum.getKey());
250         }
251     }
252 
253     @Override
254     public int hashCode()
255     {
256         if (getKey() == null)
257         {
258             return super.hashCode();
259         }
260         return getKey().hashCode();
261     }
262 
263     @Override
264     public boolean equals(Object obj)
265     {
266         if (getKey() == null || !(obj instanceof Data))
267         {
268             return super.equals(obj);
269         }
270         return getKey().equals(((Data)obj).getKey());
271     }
272 
273     @Override
274     public String toString()
275     {
276         StringBuilder out = new StringBuilder();
277         out.append("Data '");
278         out.append(key);
279         out.append('\'');
280         out.append(" -");
281         out.append(this.typeValue);
282         out.append("-> ");
283         out.append(value);
284         return out.toString();
285     }
286 
287 
288 
289     protected Object convert(Object value)
290     {
291         if (this.isList)
292         {
293             return convertList(value);
294         }
295         else if (this.converter == null)
296         {
297             return value;
298         }
299         else
300         {
301             return convertValue(value);
302         }
303     }
304 
305     private Object convertValue(Object value)
306     {
307         return this.converter.convert(this.target, value);
308     }
309 
310     private List convertList(Object val)
311     {
312         // we assume it is a string
313         String value = (String)val;
314         if (value == null || value.trim().length() == 0)
315         {
316             return null;
317         }
318         else
319         {
320             //TODO: make sure this works as expected...
321             List<String> list = Arrays.asList(value.split(","));
322             if (this.converter == null || this.target.equals(String.class))
323             {
324                 return list;
325             }
326             else
327             {
328                 List convertedList = new ArrayList();
329                 for (String item : list)
330                 {
331                     convertedList.add(convertValue(item));
332                 }
333                 return convertedList;
334             }
335         }
336     }
337 
338 
339 
340     // ------------- Subclasses -----------------
341 
342     /**
343      * Delineates the standard, known types and their
344      * associated target classes ({@link #setTargetClass} and
345      * converters ({@link #setConverter}).
346      */
347     protected static enum Type
348     {
349         AUTO(Object.class, new AutoConverter()),
350         BOOLEAN(Boolean.class, new BooleanConverter()),
351         CUSTOM(null, null),
352         FIELD(Object.class, new FieldConverter()),
353         NUMBER(Number.class, new NumberConverter()),
354         STRING(String.class, new StringConverter()),
355         LIST(List.class, null),
356         LIST_AUTO(List.class, AUTO.getConverter()),
357         LIST_BOOLEAN(List.class, BOOLEAN.getConverter()),
358         LIST_FIELD(List.class, FIELD.getConverter()),
359         LIST_NUMBER(List.class, NUMBER.getConverter()),
360         LIST_STRING(List.class, STRING.getConverter());
361 
362         private Class target;
363         private Converter converter;
364 
365         Type(Class t, Converter c)
366         {
367             this.target = t;
368             this.converter = c;
369         }
370 
371         public boolean isCustom()
372         {
373             // custom ones require the user to provide the converter
374             return (this.converter == null);
375         }
376 
377         public boolean isList()
378         {
379             // all list types return lists
380             return (this.target == List.class);
381         }
382 
383         public Class getTarget()
384         {
385             return this.target;
386         }
387 
388         public Converter getConverter()
389         {
390             return this.converter;
391         }
392 
393         public String value()
394         {
395             // make 'LIST_AUTO' into 'list.auto'
396             return name().replace('_','.').toLowerCase();
397         }
398 
399         public static Type get(String type)
400         {
401             if (type == null || type.length() == 0)
402             {
403                 return CUSTOM;
404             }
405             // make 'list.auto' eq 'LIST_AUTO'
406             return valueOf(type.replace('.','_').toUpperCase());
407         }
408     }
409 
410     protected static class FieldConverter implements Converter
411     {
412         public Object convert(Class type, Object value)
413         {
414             String fieldpath = (String)value;
415             try
416             {
417                 return ClassUtils.getFieldValue(fieldpath);
418             }
419             catch (Exception e)
420             {
421                 throw new IllegalArgumentException("Could not retrieve value for field at "+fieldpath, e);
422             }
423         }
424     }
425 
426     protected static class AutoConverter implements Converter
427     {
428         public Object convert(Class type, Object obj)
429         {
430             // only bother with strings for now
431             if (obj instanceof String)
432             {
433                 try
434                 {
435                     return convert((String)obj);
436                     
437                 }
438                 catch (Exception e)
439                 {
440                     return obj;
441                 }
442             }
443             return obj;
444         }
445 
446         public Object convert(String value)
447         {
448             // check if this looks like a typical boolean type
449             if (value.matches("true|false|yes|no|y|n|on|off"))
450             {
451                 return Type.BOOLEAN.getConverter().convert(Boolean.class, value);
452             }
453             // check if this looks like a typical number
454             else if (value.matches("-?[0-9]+(\\.[0-9]+)?"))
455             {
456                 return Type.NUMBER.getConverter().convert(Number.class, value);
457             }
458             // check if this looks like a typical field
459             else if (value.matches("(\\w+\\.)+\\w+"))
460             {
461                 return Type.FIELD.getConverter().convert(Object.class, value);
462             }
463             return value;
464         }
465     }
466 
467     protected static class NumberConverter implements Converter
468     {
469         public Object convert(Class type, Object obj)
470         {
471             Number num = ConversionUtils.toNumber(obj,"default",Locale.US);
472             if (num == null)
473             {
474                 throw new IllegalArgumentException("Could not convert "+obj+" to a number");
475             }
476             // now, let's return integers for integer values
477             else if (obj.toString().indexOf('.') < 0)
478             {
479                 // unless, of course, we need a long
480                 if (num.doubleValue() > Integer.MAX_VALUE ||
481                     num.doubleValue() < Integer.MIN_VALUE)
482                 {
483                     num = Long.valueOf(num.longValue());
484                 }
485                 else
486                 {
487                     num = Integer.valueOf(num.intValue());
488                 }
489             }
490             return num;
491         }
492     }
493 
494 }