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 }