001    /**
002     * Copyright 2007 Mike Kroutikov.
003     *
004     * This program is free software; you can redistribute it and/or modify
005     *   it under the terms of the Lesser GNU General Public License as 
006     *   published by the Free Software Foundation; either version 3 of
007     *   the License, or (at your option) any later version.
008     *
009     *   This program is distributed in the hope that it will be useful,
010     *   but WITHOUT ANY WARRANTY; without even the implied warranty of
011     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012     *   Lesser GNU General Public License for more details.
013     *
014     *   You should have received a copy of the Lesser GNU General Public License
015     *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
016     */
017    
018    package org.otfeed.support;
019    
020    import java.io.PrintWriter;
021    import java.lang.reflect.Method;
022    import java.util.Map;
023    import java.util.HashMap;
024    import java.util.List;
025    import java.util.LinkedList;
026    import java.util.TreeMap;
027    
028    /**
029     * Class that provides CSV formatting for Java POJO beans.
030     * <p/>
031     * Simplest usage is:
032     * <pre>
033     * IDataWriter writer = new CSVDataWriter(OTTrade.class);
034     * </pre>
035     * which creates a writer to output 
036     * {@link org.otfeed.event.OTTrade} properties in a CSV 
037     * format. Note that only objects of one class (used in constructor)
038     * can be written. Attempts to write a different object will
039     * yield a runtime exception.
040     * <p/>
041     * More advanced usage is:
042     * <pre>
043     * List<String> propertiesList = new LinkedList<String>();
044     * propertiesList.add("timestamp");
045     * propertiesList.add("openPrice");
046     * propertiesList.add("closePrice");
047     * propertiesList.add("volume");
048     * IDataWriter writer = new CSVDatWriter(OOHLC.class, propertiesList);
049     * </pre>
050     * which creates a writer that outputs only listed properties of 
051     * {@link org.otfeed.event.OTOHLC} object.
052     * <p/>
053     * Typically, this object will be used in conjunction with
054     * {@link org.otfeed.support.CommonDelegate CommonListener} or another class that implements an appropriate
055     * event listener using {@link IDataWriter} as the event sink.
056     */
057    public class CSVDataWriter implements IDataWriter {
058            
059            private static List<Prop> buildPropertyList(Class<?> cls, List<String> customPropertyList) {
060    
061                    Map<String,Prop> allProperties = introspect(cls);
062                    
063                    List<Prop> prop = new LinkedList<Prop>();
064                    
065                    if(customPropertyList != null) {
066                            // have custom list: include only properties
067                            // listed, in the order they are listed
068                            if(customPropertyList.size()== 0) {
069                                    throw new IllegalArgumentException("invalid property list: must contain at least one property name: [" + customPropertyList + "]");
070                            }
071                            for(String name : customPropertyList) {
072                                    if(name.length() == 0) {
073                                            throw new IllegalArgumentException("bad property name (can not have zero length!)");
074                                    }
075                                    Prop p = allProperties.get(name);
076                                    if(p == null) {
077                                            throw new IllegalArgumentException("requested property [" + name + "] not found. Following properties are available: " + allProperties.keySet());
078                                    }
079                                    prop.add(p);
080                            }
081                    } else {
082                            // add all properties (in alphabetical order)
083                            for(String name : allProperties.keySet()) {
084                                    prop.add(allProperties.get(name));
085                            }
086                    }
087                    
088                    return prop;
089            }
090            
091            /**
092             * Creates new CSVDataWriter to write listed properties of
093             * a given class.
094             * 
095             * @param cls type of the objects to be written.
096             * @param list list of property names. This is useful
097             *      if you want to get control over which properties
098             *      are included. It allows to skip some properties,
099             *      specify the exact order of properties in the CSV 
100             *      line, or output a single property more than once.
101             */
102            public CSVDataWriter(Class<?> cls, List<String> list) {
103                    dataClass = cls;
104                    propList = buildPropertyList(cls, list);
105            }
106            
107            /**
108             * Creates new CSVDataWriter to write objects of
109             * a given class. Order of the properties is not well-defined
110             * (actually depends on the JVM implementation, apparently).
111             * Therefore, it switching {@link #isHeaders() headers} property
112             * to OFF is not recommended.
113             * <p/>
114             * If you need full control over which properties are written out,
115             * and in what order, use {@link #CSVDataWriter(Class, List)}
116             * constructor.
117             * 
118             * @param cls type of the objects to be written.
119             */
120            public CSVDataWriter(Class<?> cls) {
121                    this(cls, null);
122            }
123    
124            private String delimeter = ", ";
125            
126            /**
127             * Delimeter, used to separate properties.
128             * <p/>
129             * Default value is ", ". Re-set this is you
130             * do not want blank character to follow comma.
131             * 
132             * @return delimeter string.
133             */
134            public String getDelimeter() { 
135                    return delimeter; 
136            }
137            
138            /**
139             * Sets delimeter.
140             * 
141             * @param val delimeter string.
142             */
143            public void setDelimeter(String val) { 
144                    delimeter = val;
145            }
146    
147            private Map<Class<?>,IFormat<Object>> customFormatters
148                    = new HashMap<Class<?>,IFormat<Object>>();
149    
150            /**
151             * Allows to customize how properties are being formatted.
152             * One particularly useful case is properties of
153             * java.util.Date type.
154             * <p/>
155             * Following code illustrates use of custom property 
156             * format:
157             * <pre>
158             * CSVDataWriter writer = ...;
159             * writer.getCustomPropertyFormatter().put(Date.class, new DateFormat("MM/dd/yyyy"));
160             * </pre>
161             *  
162             * @return map of custom formatters.
163             */
164            public Map<Class<?>,IFormat<Object>> getCustomPropertyFormatter() {
165                    return customFormatters;
166            }
167    
168            /**
169             * Sets dictionary of custom property formatters.
170             * 
171             * @param val formatters dictionary.
172             */
173            public void setCustomPropertyFormatter(Map<Class<?>,IFormat<Object>> val) {
174                    customFormatters = val;
175            }
176            
177            
178            private boolean needHeaders = true;
179    
180            /**
181             * Determines whether CVS output strats with list of properties.
182             * Default value is <code>true</code>.
183             * 
184             * @return headers flag.
185             */
186            public boolean isHeaders() {
187                    return needHeaders;
188            }
189            
190            /**
191             * Sets headers flag.
192             * 
193             * @param val headers flag value.
194             */
195            public void setHeaders(Boolean val) {
196                    needHeaders = val;
197            }
198            
199            private PrintWriter out = new PrintWriter(System.out, true);
200            /**
201             * Determines the output destination.
202             * Default destination is System.out.
203             * 
204             * @return output destination.
205             */
206            public PrintWriter getPrintWriter() {
207                    return out;
208            }
209            
210            /**
211             * Sets output destination.
212             * 
213             * @param val output destination.
214             */
215            public void setPrintWriter(PrintWriter val) {
216                    out = val;
217            }
218    
219            private static class Prop {
220                    public final String name;
221                    public final Method method;
222    
223                    private Prop(String name, Method method) {
224                            this.name = name;
225                            this.method = method;
226                    }
227            }
228    
229            private Class<?> dataClass;
230            private List<Prop> propList;
231            
232            private static String normalizeName(String name) {
233                    if(name.startsWith("get")) {
234                            name = name.substring(3);
235                    } else if(name.startsWith("is")) {
236                            name = name.substring(2);
237                    }
238    
239                    if(name.length() > 1 && Character.isLowerCase(name.charAt(1))) {
240                            return name.substring(0, 1).toLowerCase() + name.substring(1);
241                    } else {
242                            return name;
243                    }
244            }
245    
246            private static boolean isReadableProperty(Method method) {
247                    String name = method.getName();
248    
249                    if(method.getParameterTypes().length > 0) return false;
250    
251                    if(name.length() > 3 && name.startsWith("get")) {
252                            return true;
253                    } else if(name.length() > 2 && name.startsWith("is")) {
254                            // FIXME: check for boolean class on output??
255                            return true;
256                    }
257    
258                    return false;
259            }
260    
261            /**
262             * Returns dictionary of named readable properties.
263             * 
264             * @param cls object class.
265             * @return dictinary of named properties.
266             */
267            private static Map<String,Prop> introspect(Class<?> cls) {
268    
269                    Method[] method = cls.getDeclaredMethods();
270    
271                    Map<String,Prop> map = new TreeMap<String,Prop>();
272                    for(int i = 0; i < method.length; i++) {
273                            if(!isReadableProperty(method[i])) continue;
274    
275                            String name = normalizeName(method[i].getName());
276                            map.put(name, new Prop(name, method[i]));
277                    }
278    
279                    return map;
280            }
281            
282            private String header(Class<?> cls, List<Prop> prop, String delimeter) {
283    
284                    StringBuffer out = new StringBuffer();
285                    for(Prop p : prop) {
286                            if(out.length() > 0) out.append(delimeter);
287                            out.append(p.name);
288                    }
289    
290                    return out.toString();
291            }
292    
293            private String format(Object obj, List<Prop> prop, String delimeter) {
294                    StringBuffer out = new StringBuffer();
295                    for(Prop p : prop) {
296                            if(out.length() > 0) out.append(delimeter);
297                            try {
298                                    Object val = p.method.invoke(obj);
299                                    IFormat<Object> fmt = customFormatters.get(val.getClass());
300                                    if(fmt != null) {
301                                            out.append(fmt.format(val));
302                                    } else {
303                                            out.append(val);
304                                    }
305                            } catch(Exception ex) {
306                                    throw new AssertionError();
307                            }
308                    }
309    
310                    return out.toString();
311            }
312    
313            private boolean doneHeaders = false;
314            
315            public void writeData(String id, Object data) {
316                    
317                    if(data == null) {
318                            throw new NullPointerException("null data not allowed");
319                    }
320    
321                    if(!data.getClass().isAssignableFrom(dataClass)) {
322                            throw new IllegalStateException("incompatible object type: expected " + dataClass + ", received " + data.getClass());
323                    }
324    
325                    if(needHeaders && !doneHeaders) {
326                            doneHeaders = true;
327                            if(id != null) out.print("id" + delimeter);
328                            out.println(header(data.getClass(), propList, delimeter));
329                    }
330    
331                    if(id != null) out.print(id + delimeter);
332                    out.println(format(data, propList, delimeter));
333            }
334            
335            /**
336             * Closes the writer stream.
337             */
338            public void close() {
339                    out.flush(); // FIXME: maybe we should close() it?
340            }
341    }