001    package com.softnetConsult.utils.attributes;
002    
003    import java.io.FileInputStream;
004    import java.io.FileOutputStream;
005    import java.io.IOException;
006    import java.lang.reflect.Array;
007    import java.lang.reflect.Constructor;
008    import java.lang.reflect.Field;
009    import java.lang.reflect.InvocationTargetException;
010    import java.util.ArrayList;
011    import java.util.HashMap;
012    import java.util.Iterator;
013    import java.util.List;
014    import java.util.Map;
015    import java.util.Properties;
016    import java.util.StringTokenizer;
017    
018    import com.softnetConsult.utils.collections.ArrayTools;
019    import com.softnetConsult.utils.reflect.ClassTools;
020    
021    
022    /**
023     * This class allows creating a behaviour similar to {@code java.util.Properties} for
024     * a Java-compilable collection of values; in other words it provides an interface
025     * between the {@code java.util.Properties}-API and a collection of attributes
026     * implemented as a set of public variables of a class.
027     * Assume, a program uses a number of configuration attributes, e.g. {@code width} and
028     * {@code height}. The values could be stored in a common Java properties file and loaded
029     * as needed. However, every time a variable value is needed, a request to the {@code Map}
030     * underlying the {@code Properties} object must be generated. In addition, a conversion
031     * from {@code String} is often necessary (e.g.
032     * {@code int height = Integer.parseString(myProperties.getProperty(“colour”))) }.
033     * This results in an ugly and slow code. Instead, one may have a class
034     * {@code MySettings implements Attributes} that contains two <strong>public static</strong> variables:
035     * {@code width} and {@code height}. AttributeManager provides an API for an <em>automatic</em>
036     * conversion between such a container of public attributes and a {@code Properties} object.
037     * Note that properties congregated in a Properties object are freely accessible (public equivalent).
038     * If full-scale encapsulation to private level is required, a purpose-build class must be created.
039     * 
040     * <p style="font-size:smaller;">This product includes software developed by the
041     *    <strong>SoftNet-Consult Java Utility Library</strong> project and its contributors.<br />
042     *    (<a href="http://java-tools.sourceforge.net" target="_blank">http://java-tools.sourceforge.net</a>)<br />
043     *    Copyright (c) 2007-2008 SoftNet-Consult.<br />
044     *    Copyright (c) 2007-2008 G. Paperin.<br />
045     *    All rights reserved.
046     * </p>
047     * <p style="font-size:smaller;">File: AttributeManager.java<br />
048     *    Library API version: {@value com.softnetConsult.utils.APIProperties#apiVersion}<br />
049     *    Java compliance version: {@value com.softnetConsult.utils.APIProperties#javaComplianceVersion}
050     * </p>
051     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
052     *    without modification, are permitted provided that the following terms and conditions are met:
053     * </p>
054     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
055     *    acknowledgement of the SoftNet-Consult Java Utility Library project, the above copyright
056     *    notice, this list of conditions and the following disclaimer.<br />
057     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
058     *    SoftNet-Consult Java Utility Library project, the above copyright notice, this list of
059     *    conditions and the following disclaimer in the documentation and/or other materials
060     *    provided with the distribution.<br />
061     *    3. All advertising materials mentioning features or use of this software or any derived
062     *    software must display the following acknowledgement:<br />
063     *    <em>This product includes software developed by the SoftNet-Consult Java Utility Library
064     *    project and its contributors.</em>
065     * </p>
066     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
067     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
068     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
069     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
070     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
071     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
072     * </p> 
073     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
074     * @version {@value com.softnetConsult.utils.APIProperties#apiVersion}
075     *
076     */
077    public final class AttributeManager {
078    
079    /**
080     * Prevents instances of this class from being created
081     * as this class contains only static utility methods.
082     */
083    private AttributeManager() {}
084    
085    
086    /**
087     * Separator used when printing a list of attributes.
088     */
089    public static final char LIST_SEPARATOR = ';';
090    
091    
092    /**
093     * Takes an attribute class object and returns a mapping from the names of variables
094     * defined for the specified class (not object of that class) and their current
095     * (static) values.
096     * 
097     * @param attributesClass An attribute class.
098     * @return A mapping from the names of variables defined for the specified class and
099     * their current (static) values represented as Strings.
100     */
101    public static Map<String, String> getNameValueMap(Class<? extends Attributes> attributesClass) {
102            
103            Map<String, String> fieldsValues = new HashMap<String, String>();
104            
105            for (Field field : attributesClass.getFields()) {
106                    
107                    String name = field.getName();
108                    
109                    Object fval = null;
110                    try {
111                            fval = field.get(null);
112                    } catch(IllegalAccessException e) {
113                            fval = "<" + e.toString() + ">";
114                    }
115                            
116                    String value = null;
117                    if (null == fval)                       
118                            value = "null";
119                            
120                    else if (!field.getType().isArray())                    
121                            value = fval.toString();
122                            
123                    else  // if non-null array:                     
124                            value = ArrayTools.arrayToString(fval, LIST_SEPARATOR);                 
125                    
126                    fieldsValues.put(name, value);
127            }
128            
129            return fieldsValues;
130    }
131    
132    /**
133     * Sets the values of the public static variables of an attribute class to the values
134     * loaded from the specified properties file.
135     * 
136     * @param attributesClass An attributes collection.
137     * @param fileName A file name.
138     * @return {@code false} if an IOException occured while reading the file, {@code true} otherwise.
139     */
140    public static boolean load(Class<? extends Attributes> attributesClass, String fileName) {
141            
142            // Load from disk:
143            
144            Properties props = new Properties();    
145            try {
146                    FileInputStream is = new FileInputStream(fileName);
147                    try { props.loadFromXML(is); }
148                    finally { is.close(); }
149            } catch(IOException e) {
150                    e.printStackTrace();
151                    return false;
152            }       
153                    
154            // For all fields of the attributes class:
155            for (Field field : attributesClass.getFields()) {
156                    
157                    // Get field name:
158                    String fname = field.getName();         
159                    
160                    // Get loaded field value:
161                    String fsval = props.getProperty(fname);
162                    if (null == fsval)
163                            continue;
164            
165                    // Get the type of the field:
166                    Class<?> fc = field.getType();
167                    
168                    // If field is not an array:
169                    if (!fc.isArray()) {
170                            
171                            // Get warpper for primitive types:
172                            if (fc.isPrimitive())
173                                    fc = ClassTools.getWrapperClass(fc);
174                            
175                            // Obtain a constructor which takes string:
176                            Constructor<?> constr = null;
177                            try {
178                                    constr = fc.getConstructor(String.class);
179                            } catch(NoSuchMethodException e) {
180                                    throw new NoSuchMethodError("The AttributeManager can only initialise fields of a type"
181                                                                                      + " which has a constructor taking a single String argument;"
182                                                                                      + " the attribute class \"" + attributesClass.getName()
183                                                                                      + "\" contains the field \"" + field.getName() + "\" of type \""
184                                                                                      + fc.getName() + "\" that does not have such a constructor");
185                            }                       
186                            
187                            Object val = null;
188                            try {
189                                    val = constr.newInstance(fsval);
190                            } catch (InstantiationException ie) {
191                                    throw new InstantiationError("Problem while constructing field value for "
192                                                                                       + field.getName() + " was indicated by " + ie);
193                            } catch (IllegalAccessException ae) {
194                                    throw new IllegalAccessError("Problem while constructing field value for "
195                                                                                       + field.getName() + " was indicated by " + ae);
196                            } catch (InvocationTargetException te) {
197                                    throw new RuntimeException("Problem while constructing field value for "
198                                                                                     + field.getName() + " was indicated by " + te.getCause());
199                            }
200                            
201                            try {
202                                    field.set(null, val);
203                            } catch (IllegalAccessException e) {
204                                    throw new IllegalAccessError("Problem while setting field value for "
205                                                                                       + field.getName() + " was indicated by " + e);
206                            }                       
207                            
208                    // If field IS an array:
209                    } else {
210                    
211                            // Get array component type:
212                            Class<?> ct = fc.getComponentType();                      
213                            
214                            // Strip '[' and ']':
215                            if (fsval.length() > 0 && fsval.charAt(0) == '[')
216                                    fsval = fsval.substring(1);
217                            
218                            if (fsval.length() > 0 && fsval.charAt(fsval.length() - 1) == ']')
219                                    fsval = fsval.substring(0, fsval.length() - 1);
220                            
221                            // Parse array string:
222                            List<String> svals = new ArrayList<String>();
223                            StringTokenizer tok = new StringTokenizer(fsval, new String(new char[] {LIST_SEPARATOR}));
224                            while(tok.hasMoreTokens()) {
225                                    svals.add(tok.nextToken().trim());
226                            }
227                            
228                            // Allocate array:
229                            Object arr = Array.newInstance(ct, svals.size());                       
230                            
231                            // Get the warapper of the array component type:
232                            if (ct.isPrimitive())
233                                    ct = ClassTools.getWrapperClass(ct);
234                            
235                            // Obtain a constructor which takes string:
236                            Constructor<?> constr = null;
237                            try {
238                                    constr = ct.getConstructor(String.class);
239                            } catch(NoSuchMethodException e) {
240                                    throw new NoSuchMethodError("The AttributeManager can only initialise arrays of a"
241                                                                                      + " component-type which has a constructor taking a single "
242                                                                                      + "String argument; the attribute class \""
243                                                                                      + attributesClass.getName() + "\" contains the field \""
244                                                                                      + field.getName() + "\" of type \"" + fc.getName()
245                                                                                      + "\" that does not have such a constructor");
246                            }
247                            
248                            // Fill the array:
249                            Object aval = null;
250                            for(int i = 0; i < svals.size(); i++) {
251                                                                    
252                                    try {
253                                            aval = constr.newInstance(svals.get(i));
254                                    } catch (InstantiationException ie) {
255                                            throw new InstantiationError("Problem while constructing value for element "
256                                                                                               + i + " of field " + field.getName()
257                                                                                               + " was indicated by " + ie);
258                                    } catch (IllegalAccessException ae) {
259                                            throw new IllegalAccessError("Problem while constructing value for element "
260                                                                                               + i + " of field " + field.getName()
261                                                                                               + " was indicated by " + ae);
262                                    } catch (InvocationTargetException te) {
263                                            throw new RuntimeException("Problem while constructing value for element "
264                                                                                             + i + " of field " + field.getName()
265                                                                                             + " was indicated by " + te.getCause());
266                                    }
267                                    
268                                    Array.set(arr, i, aval);
269                                    
270                            } // FOR i=0 TO svals.size()
271                            
272                            try {
273                                    field.set(null, arr);
274                            } catch (IllegalAccessException e) {
275                                    throw new IllegalAccessError("Problem while setting field value for "
276                                                                                       + field.getName() + " was indicated by " + e);
277                            }
278                            
279                    }  // if field IS an array              
280                    
281            } // for (Field field : attributesClass.getFields())
282            
283            return true;
284    }
285    
286    /**
287     * Saves the values of static public variables of an attribute collection class to
288     * a Java properties file.
289     * 
290     * @param attributesClass An attribute collection.
291     * @param fileName A properties file name.
292     * @return {@code false} if an IOException occured while writing the file, {@code true} otherwise.
293     */
294    public static boolean save(Class<? extends Attributes> attributesClass, String fileName) {
295    
296            Properties props = asProperties(attributesClass);
297            try {
298                    FileOutputStream os = new FileOutputStream(fileName);
299                    try { props.storeToXML(os, "Attribute values for " + attributesClass.getName() + ".", "UTF-8"); }
300                    finally { os.close(); }
301                    return true;
302            } catch(IOException e) {
303                    e.printStackTrace();
304                    return false;
305            }       
306    }
307    
308    /**
309     * Converts an attribute collection to a set of properties.
310     * 
311     * @param attributesClass An attribute collection class.
312     * @return A set of properties representing the current values of the attributes as Strings.
313     */
314    public static final Properties asProperties(Class<? extends Attributes> attributesClass) {
315            Map<String, String> fieldsValues = getNameValueMap(attributesClass);
316            Properties props = new Properties();
317            props.putAll(fieldsValues);
318            return props;
319    }
320    
321    /**
322     * Converts an attribute collection to a human readable form.
323     *  
324     * @param attributesClass An attribute collection class.
325     * @return An array of strings containing for each attribute of the collection a
326     * string of form {@code name=value}.
327     */
328    public static final String[] asStrings(Class<? extends Attributes> attributesClass) {
329            
330            Map<String, String> fieldsValues = getNameValueMap(attributesClass);
331            String fieldsValuesStrings[] = new String[fieldsValues.size()];
332            
333            Iterator<String> fieldNames = fieldsValues.keySet().iterator();
334            for (int i = 0; i < fieldsValuesStrings.length; i++) {
335                    
336                    String n = fieldNames.next();
337                    String v = fieldsValues.get(n);
338                    fieldsValuesStrings[i] = n + "=" + v;
339            }
340            return fieldsValuesStrings;
341    }
342    
343    /**
344     * Converts an attribute collection to a human readable form.
345     * 
346     * @param separator Separator between name/value pairs.
347     * @param attributesClass An attribute collection class.
348     * @return A String containing containing for each attribute of the collection a
349     * sub-string of form {@code name=value}, with all such pairs separated by the
350     * specified separator.
351     */
352    public static final String asString(String separator, Class<? extends Attributes> attributesClass) {
353            
354            StringBuffer buff = new StringBuffer();
355            String[] fieldsStrings = asStrings(attributesClass);
356            
357            for (String fs : fieldsStrings) {
358                    
359                    if (buff.length() > 0)
360                            buff.append(separator);
361                    
362                    buff.append(fs);
363            }
364            
365            return buff.toString();
366    }
367    
368    /**
369     * Converts an attribute collection to a human readable form.
370     * 
371     * @param attributesClass An attribute collection class.
372     * @return A String containing containing for each attribute of the collection a
373     * sub-string of form {@code name=value}, with all such pairs separated by the
374     * default separator.
375     */
376    public static final String asLine(Class<? extends Attributes> attributesClass) {
377            return asString(LIST_SEPARATOR + " ", attributesClass); 
378    }
379    
380    } // public class AttributeManager