001    package edu.nrao.sss.util;
002    
003    import java.io.InputStream;
004    import java.lang.reflect.Method;
005    import java.util.ArrayList;
006    import java.util.EnumSet;
007    import java.util.HashMap;
008    import java.util.List;
009    import java.util.Map;
010    import java.util.Properties;
011    
012    import org.apache.log4j.Logger;
013    
014    
015    /**
016     * A class that helps clients interact with enumeration classes.
017     * <p>
018     * Instances of this class will be used mainly, though not exclusively, by
019     * user interface classes.  This class helps with setting default values for
020     * enumerations and also with restricting the choices of enumeration values
021     * available to users.</p>
022     * <p>
023     * There are two ways to use this class to help manage enumerations, and these
024     * two ways can be used together.  The first is programmatic.  The
025     * {@link #setDefaultValue(Class, Enum)} method can be used to define a default
026     * enumeration element for an enumeration class.  The
027     * {@link #addToSelectableSet(Class, Enum)},
028     * {@link #removeFromSelectableSet(Class, Enum)}, and
029     * {@link #setSelectableSet(Class, EnumSet)} methods can be used to control
030     * the elements of an enumeration class that may be chosen by a user 
031     * (or another object).</p>
032     * <p>
033     * The second way to manage enumerations with this class is to use properties
034     * files.  The name of each properties file is
035     * <tt><i>simple-enumeration-class-name</i>.properties</tt>.  These files must
036     * reside on the classpath.  This class understands these two properties:
037     * <tt><b>default</b></tt> and <tt><b>nonselectable</b></tt>.  The first
038     * property takes a single value; the second takes a comma-separated list of
039     * values.  The text of the values must be identical to the name of the
040     * element, including capitalization.</p>
041     * <p>
042     * <b><u>Example Properties File:</u></b><br/>
043     * Name: <tt>DistanceUnits.properties</tt><br/>
044     * Contents:<pre>
045     * 
046     *   default = KILOMETER
047     *   
048     *   nonselectable = UNKNOWN, ANGSTROM, NANOMETER, MICROMETER, MILLIMETER,\
049     *                   CENTIMETER, MILE
050     * </pre>
051     * <p>
052     * <b>CVS Info:</b>
053     * <table style="margin-left:2em">
054     *   <tr><td>$Revision: 1707 $</td></tr>
055     *   <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr>
056     *   <tr><td>$Author: dharland $</td></tr>
057     * </table></p>
058     * 
059     * @author David M. Harland
060     * @since 2006-05-18
061     */
062    public class EnumerationUtility
063    {
064            private static Logger log = Logger.getLogger(EnumerationUtility.class);
065      
066      //============================================================================
067      // STATIC METHODS & INSTANCES
068      //============================================================================
069      
070      //An instance that could be shared by multiple clients.
071      private static EnumerationUtility sharedInst;
072      
073      /**
074       * Returns a pre-made instance of this utility.
075       * Every request to this method returns the same object.
076       * 
077       * @return a pre-made instance of this utility.
078       */
079      public static EnumerationUtility getSharedInstance()
080      {
081        if (sharedInst == null)
082          sharedInst = new EnumerationUtility();
083        
084        return sharedInst;
085      }
086    
087      //============================================================================
088      // INSTANCE VARIABLES & CONSTRUCTORS
089      //============================================================================
090    
091      //Holds information about various enumeration classes.
092      @SuppressWarnings("unchecked")
093      private Map<Class, Value> enums;
094      
095      /** Creates a new enumeration utility. */
096      @SuppressWarnings("unchecked")
097      public EnumerationUtility()
098      {
099        enums = new HashMap<Class, Value>();
100      }
101    
102      //============================================================================
103      // INSTANCE METHODS
104      //============================================================================
105      
106      /**
107       * Returns the complete set of enumeration elements for the given class.
108       * @param enumType an enumeration class.
109       * @return the complete set of enumeration elements for {@code enumType}.
110       * @throws NullPointerException if {@code enumType} is <i>null</i>.
111       */
112      public <E extends Enum<E>> EnumSet<E> getCompleteSetFor(Class<E> enumType)
113      {
114        return getOrMakeMapValueFor(enumType).complete.clone();
115      }
116      
117      /**
118       * Returns the set of enumeration elements for the given class from which
119       * a specific element may be selected.  This set is often, but not always,
120       * the complete set of elements.  Sometimes, though, an enumeration has
121       * an element such as <tt>UNKNOWN</tt> that should not be a client-selectable
122       * element.  The set returned by this method will typically exclude such
123       * elements.
124       * @param enumType an enumeration class.
125       * @return a set of selectable elements of type <tt>E</tt>.
126       * @throws NullPointerException if {@code enumType} is <i>null</i>.
127       */
128      public <E extends Enum<E>> EnumSet<E> getSelectableSetFor(Class<E> enumType)
129      {
130        return getOrMakeMapValueFor(enumType).selectable.clone();
131      }
132      
133      /**
134       * Returns a default element for the given enumeration type.
135       * A common use of this method is to initialize a user interface
136       * component to a default value.
137       * @param enumType an enumeration class.
138       * @return a default element for {@code enumType}.
139       * @throws NullPointerException if {@code enumType} is <i>null</i>.
140       */
141      public <E extends Enum<E>> E getDefaultValueFor(Class<E> enumType)
142      {
143        return getOrMakeMapValueFor(enumType).dfault;
144      }
145    
146      /**
147       * Returns a random element for the given enumeration type.
148       * @param enumType an enumeration class.
149       * @return a random element for the given enumeration type.
150       */
151      public <E extends Enum<E>> E getRandomValueFor(Class<E> enumType)
152      {
153        List<E> values = new ArrayList<E>(EnumSet.allOf(enumType));
154        
155        return values.get((int)(values.size() * Math.random())); 
156      }
157    
158      /**
159       * Sets the collection of selectable elements for the given enumeration type.
160       * @param enumType an enumeration class.
161       * @param selectableSet a set of selectable elements of type <tt>E</tt>.
162       * @throws NullPointerException if either parameter is <i>null</i>.
163       */
164      public <E extends Enum<E>> void setSelectableSet(Class<E> enumType,
165                                                       EnumSet<E> selectableSet)
166      {
167        getOrMakeMapValueFor(enumType).selectable = selectableSet.clone();
168      }
169      
170      /**
171       * Add {@code enumValue} to the set of selectable elements of class
172       * {@code enumType}.  If {@code enumType} is <i>null</i>, this method
173       * does nothing.
174       * @param enumType an enumeration class.
175       * @param enumValue a selectable element.
176       * @throws NullPointerException if {@code enumType} is <i>null</i>.
177       */
178      public <E extends Enum<E>> void addToSelectableSet(Class<E> enumType, E enumValue)
179      {
180        if (enumValue != null)
181          getOrMakeMapValueFor(enumType).selectable.add(enumValue);
182      }
183      
184      /**
185       * Removes {@code enumValue} from the set of selectable elements of class
186       * {@code enumType}.
187       * @param enumType an enumeration class.
188       * @param enumValue an element that is not selectable.
189       * @throws NullPointerException if {@code enumType} is <i>null</i>.
190       */
191      public <E extends Enum<E>> void removeFromSelectableSet(Class<E> enumType, E enumValue)
192      {
193        if (enumValue != null)
194          getOrMakeMapValueFor(enumType).selectable.remove(enumValue);
195      }
196    
197      /**
198       * Sets the default value for {@code enumType} to {@code enumValue}.
199       * @param enumType an enumeration class.
200       * @param enumValue a default value for {@code enumType}.
201       */
202      public <E extends Enum<E>> void setDefaultValue(Class<E> enumType, E enumValue)
203      {
204        if (enumValue != null)
205          getOrMakeMapValueFor(enumType).dfault = enumValue;
206      }
207    
208      /**
209       * Reloads the internally stored information for the given enumeration
210       * type.  This is useful if the client knows that a change has been
211       * made to an external properties file.  Unless forced to do so, this
212       * utility will not detect such external changes.
213       * 
214       * @param enumType an enumeration class.
215       */
216      public <E extends Enum<E>> void reload(Class<E> enumType)
217      {
218        enums.put(enumType, new Value<E>(enumType));
219      }
220      
221      /**
222       * Reloads all internally stored information.
223       * See {@link #reload(Class)} for details.
224       */
225      @SuppressWarnings("unchecked")
226      public void reload()
227      {
228        //The complier can't know what we know here: That the map is coordinated
229        //for Class<E> and Value<E>.  It sees only Class & Value.
230        //We suppress this warning.
231        for (Class enumType : enums.keySet())
232          reload(enumType);
233      }
234      
235      /**
236       * Returns a <tt>Value</tt> object for the given enumeration type.
237       * This method first looks at the internal map to see if a value
238       * object already exists for <tt>enumType</tt>.  If it does, it
239       * is returned.  Otherwise a new value object is created, placed
240       * in the map, and returned.
241       * @param enumType an enumeration class.
242       * @return a value object of the given enumeration type.
243       */
244      @SuppressWarnings("unchecked")
245      private <E extends Enum<E>> Value<E> getOrMakeMapValueFor(Class<E> enumType)
246      {
247        //The complier can't know what we know here: That the Value we get back
248        //for Class<E> really is Value<E>.  We suppress this warning.
249        Value<E> mapValue = enums.get(enumType);
250        
251        if (mapValue == null)
252        {
253          mapValue = new Value<E>(enumType);
254          enums.put(enumType, mapValue);
255        }
256        
257        return mapValue;
258      }
259    
260      //----------------------------------------------------------------------------
261      // Text Representation
262      //----------------------------------------------------------------------------
263    
264      /**
265       * Returns a text representation of {@code element}.
266       * <p>
267       * The returned string will be in lower case, except that the first letter of
268       * each word will be in upper case.  The string is based on the {@code name}
269       * of the element (as returned by {@code element.name()}).  Any underscores
270       * in the name are replaced with spaces.</p>
271       * <p>
272       * The main users of this method will be the enumeration classes themselves.</p>
273       * 
274       * @param element a member of an enumeration class.
275       * 
276       * @return a text representation of {@code element}.  If {@code element} is
277       *         <i>null</i>, the empty string (<tt>""</tt>) is returned.
278       */
279      public <E extends Enum<E>> String enumToString(E element)
280      {
281        if (element == null)
282          return "";
283        
284        String name = element.name().replaceAll("_", " ");
285        
286        return StringUtil.getInstance()
287                         .lowerAndCapitalizeFirstLetterOfEachWord(name);
288      }
289    
290      /**
291       * Returns a text representation of {@code element}.
292       * <p>
293       * The returned string will be in Upper case.  The string is based on the {@code name}
294       * of the element (as returned by {@code element.name()}).  Any underscores
295       * in the name are replaced with spaces.</p>
296       * <p>
297       * The main users of this method will be the enumeration classes themselves.</p>
298       * 
299       * @param element a member of an enumeration class.
300       * 
301       * @return a text representation of {@code element}.  If {@code element} is
302       *         <i>null</i>, the empty string (<tt>""</tt>) is returned.
303       */
304      public <E extends Enum<E>> String enumToUpperString(E element)
305      {
306        if (element == null)
307          return "";
308        
309        String name = element.name().replaceAll("_", " ");
310        
311        return name; 
312        
313      }
314      
315      
316      /**
317       * Returns an enumeration constant from {@code enumType} represented by
318       * {@code text}.
319       * <p>
320       * Leading and trailing whitespace is first stripped from {@code text}.
321       * A case-insensitive comparison against the {@code name} and
322       * {@code toString} methods of each constant in the enumeration is then
323       * performed.  If the enumeration class has a {@code getSymbol} method,
324       * it is also used in the comparison.  If no match is found, <i>null</i>
325       * is returned.</p>
326       * 
327       * @param enumType an enumeration class.
328       * 
329       * @param text a text representation of a constant in {@code enumType}.
330       * 
331       * @return an enumeration constant from {@code enumType}.
332       * 
333       * @see #enumFromStringOrDefault(Class, String)
334       */
335      public <E extends Enum<E>> E enumFromString(Class<E> enumType, String text)
336      {
337        //Quick exit if input class is null
338        if (enumType == null)
339          return null;
340        
341        E result = null;
342    
343        boolean hasSymbol = Symbolic.class.isAssignableFrom(enumType);
344        
345        //Remove leading & trailing whitespace from text and handle null
346        text = StringUtil.getInstance().normalizeString(text);
347        
348        E[] elements = enumType.getEnumConstants();
349        
350        //Loop through enumeration constants of input class
351        for (E element : elements)
352        {
353          //Try to match against standard name and toString methods
354          if (text.equalsIgnoreCase(element.name()) ||
355              text.equalsIgnoreCase(element.toString()))
356          {
357            result = element;
358            break;
359          }
360          //If no match, and if the class has the getSymbol method,
361          //see if text matches symbol.
362          else if (hasSymbol)
363          {
364            Symbolic symbolHolder = (Symbolic)element;
365    
366            //Cannot always ignore case for symbols (eg, milli-x vs Mega-x)
367            if ((symbolHolder.symbolsAreCaseSensitive() &&
368                 text.equals(symbolHolder.getSymbol()))
369                        ||
370                text.equalsIgnoreCase(symbolHolder.getSymbol()))
371            {
372              result = element;
373              break;
374            }
375          }
376        }
377        
378        return result;
379      }
380    
381      /**
382       * Returns an enumeration constant from {@code enumType} based on
383       * {@code text}.
384       * <p>
385       * This method is similar to {@link #enumFromString(Class, String)},
386       * except that if no match is found, a default constant from 
387       * {@code enumType} is returned.</p>
388       * <p>
389       * If {@code enumType}
390       * <ol>
391       *   <li>is non-null,</li>
392       *   <li>is an enumeration class, and</li>
393       *   <li>has at least one constant</li>
394       * </ol>
395       * then the return value is guaranteed to be non-null.</p>
396       * 
397       * @param enumType an enumeration class.
398       * 
399       * @param text a text representation of a constant in {@code enumType}.
400       * 
401       * @return an enumeration constant from {@code enumType}.
402       * 
403       * @see #enumFromString(Class, String)
404       * @see #getDefaultValueFor(Class)
405       */
406      public <E extends Enum<E>> E enumFromStringOrDefault(Class<E> enumType, String text)
407      {
408        E result = enumFromString(enumType, text);
409        
410        //If there was no match, use default
411        if (result == null)
412          result = getDefaultValueFor(enumType);
413        
414        return result;
415      }
416    
417      //============================================================================
418      // INTERNAL CLASSES
419      //============================================================================
420     
421      /**
422       * Holds those properties of an enumeration class that are dealt with by
423       * the enumeration utility.  Instances of this class are held in the
424       * utility's map, using enumeration classes as the keys.
425       */
426      class Value <E extends Enum<E>>
427      {
428        E          dfault;
429        EnumSet<E> selectable;
430        EnumSet<E> complete;
431        
432        /**
433         * Creates a new instance for the given enumeration type.
434         * This constructor ensures that all of the internal properties
435         * are set to non-null values.  This constructor first looks
436         * for a properties file for the enumertation type.  If one is
437         * found it is used to value some of the properties.  If it is
438         * not found, or if it is found but does not contain a given
439         * property, that property(ies) is given a default value.
440         */
441        Value(Class<E> enumType)
442        {
443          //See if we have a properties file for this class.
444          try
445          {
446            InputStream propFile = makePropFileFor(enumType);
447            Properties enumProps = new Properties();
448            enumProps.load(propFile);
449            setValuesFrom(enumProps, enumType);
450          }
451          //If not, set the default values of the properties
452          catch (Exception ex)
453          {
454            setDefaultEnumFor(enumType);
455            setDefaultSelectableFor(enumType);
456          }
457          
458          //Do not use the properties files for the complete set
459          complete = EnumSet.allOf(enumType);
460        }
461        
462        /**
463         * 
464         */
465        private void setValuesFrom(Properties properties, Class<E> enumType)
466        {
467          //Set the default element
468          try {
469            dfault = Enum.valueOf(enumType, properties.getProperty("default"));
470          }
471          catch (Exception ex) {
472            setDefaultEnumFor(enumType);
473          }
474          
475          //Set the selectable collection by first setting the default set
476          //and then removing from that collection any legal elements we
477          //find in the properties file.
478          setDefaultSelectableFor(enumType);
479          String nonselectables =
480            properties.getProperty("nonselectable").replaceAll("\\s", "");
481    
482          for (String nonselectable : nonselectables.split(","))
483          {
484            try {
485              selectable.remove(Enum.valueOf(enumType, nonselectable));
486            }
487            catch (Exception ex) {
488                    // Do nothing but log that an error happend.
489                    log.error("EnumerationUtility unable to match value. Possibly due to a typo in the properties file.",ex);
490            }
491          }
492        }
493    
494        /**
495         * 
496         */
497        @SuppressWarnings("unchecked")
498        private void setDefaultEnumFor(Class<E> enumType)
499        {
500          dfault = null;
501          
502          if (enumType != null)
503          {
504            //See if class has a (static) getDefault() method
505            try
506            {
507              Method getDefault = enumType.getDeclaredMethod("getDefault",
508                                                             (Class[])null);
509              dfault = (E)getDefault.invoke(enumType, (Object[])null);
510            }
511            //If trouble w/ getDefault, use first element in list 
512            catch (Exception ex)
513            {
514              E[] elements = enumType.getEnumConstants();
515              if (elements.length > 0)
516                dfault = elements[0];
517            }
518          }
519        }
520        
521        /**
522         * 
523         */
524        private void setDefaultSelectableFor(Class<E> enumType)
525        {
526          selectable = EnumSet.allOf(enumType);
527        }
528        
529        /**
530         * 
531         */
532        private InputStream makePropFileFor(Class<E> enumType)
533        {
534          return enumType.getResourceAsStream(makePropFileNameFor(enumType));
535        }
536        
537        /**
538         * 
539         */
540        private String makePropFileNameFor(Class<E> enumType)
541        {
542          return enumType.getSimpleName() + '.' + "properties";
543        }
544      }
545    
546      //============================================================================
547      // 
548      //============================================================================
549      
550    /*  public static void main(String[] args)
551      {
552        EnumerationUtility util = new EnumerationUtility();
553        
554        System.out.println("DistanceUnits: " + util.getSelectableSetFor(edu.nrao.sss.measure.DistanceUnits.class));
555        System.out.println("DistanceUnits.default: " + util.getDefaultValueFor(edu.nrao.sss.measure.DistanceUnits.class));
556        System.out.println("TimeUnits: " + util.getSelectableSetFor(edu.nrao.sss.measure.TimeUnits.class));
557        System.out.println("TimeUnits.default: " + util.getDefaultValueFor(Tedu.nrao.sss.measure.imeUnits.class));
558        
559        util.setDefaultValue(edu.nrao.sss.measure.TimeUnits.class, edu.nrao.sss.measure.TimeUnits.HOUR);
560        System.out.println("TimeUnits.default: " + util.getDefaultValueFor(edu.nrao.sss.measure.TimeUnits.class));
561      }*/
562      /*
563      public static void main(String[] args)
564      {
565        EnumerationUtility util = new EnumerationUtility();
566    
567        //Trigger loading of classes
568        util.getDefaultValueFor(edu.nrao.sss.measure.DistanceUnits.class);
569        util.getDefaultValueFor(edu.nrao.sss.measure.ArcUnits.class);
570        
571        System.out.println("Util has " + util.enums.size() + " mappings.");
572        System.out.println();
573        
574        for (Class c : util.enums.keySet())
575        {
576          System.out.println("Class " + c.getSimpleName());
577          Value v = util.enums.get(c);
578          System.out.println("  Default: " + v.dfault);
579          System.out.print("  Selectable:");
580          for (Object e : v.selectable)
581            System.out.print(" " + e + ",");
582          System.out.println();
583          System.out.println();
584        }
585    
586        System.out.println();
587        
588        for (edu.nrao.sss.model.project.scan.ScanIntent intent :
589             edu.nrao.sss.model.project.scan.ScanIntent.values())
590          System.out.println(intent.name() + " -> " + util.enumToString(intent));
591      }
592      
593      public static void main(String[] args)
594      {
595        EnumerationUtility util = EnumerationUtility.getSharedInstance(); 
596        Class<Epoch> enumType = Epoch.class;
597        int[] counts = new int[util.getCompleteSetFor(enumType).size()];
598        
599        for (int i=0; i < 100000; i++)
600        {
601          counts[util.getRandomValueFor(enumType).ordinal()]++;
602        }
603    
604        for (int c=0; c < counts.length; c++)
605          System.out.println("Count["+c+"] = " + counts[c]);
606      }
607      */
608    }