001    package org.LiveGraph.dataFile.write;
002    
003    import java.io.BufferedWriter;
004    import java.io.IOException;
005    import java.io.OutputStream;
006    import java.io.OutputStreamWriter;
007    import java.util.ArrayList;
008    import java.util.HashMap;
009    import java.util.List;
010    import java.util.Map;
011    
012    import static org.LiveGraph.dataFile.common.DataFormatTools.*;
013    
014    /**
015     * {@code DataStreamWriter} objects are used for writing files in the LiveGraph file format.
016     * {@code DataStreamWriter} does not extend {@code java.io.Writer} because the structure
017     * of the data being written is different and the making use of the methods published by
018     * the standard API class would be counter-intuitive; however, {@code DataStreamWriter}
019     * objects should be used in much the same manner as a normal {@code Writer} in an
020     * application.<br /> 
021     * <br />
022     * The {@code DataStreamWriter} class provides methods for setting up the data file separator,
023     * adding information lines and comments to the data file, defining the number of and the
024     * labels for the data series and, eventually, for writing the data.<br />
025     * Before any data is sent to the writer the data series should be set up with a series of
026     * calls to {@link #addDataSeries(String)}. Once a dataset is written to the stream, no
027     * more data series may be added.<br />
028     * A dataset is written by a series of calls to one of the {@code setDataValue(...)}
029     * methods. Calls to those methods do not cause any data to be written. Instead, the values
030     * are associated with the appropriate data series and cached. In order to actually write the
031     * data to the underlying stream the method {@link #writeDataSet()} must be invoked. It flushes
032     * the cache to the data stream and prepares for the processing of the next dataset.<br />
033     * In order to allow for concise code when using this class in applications, no methods
034     * of {@code DataStreamWriter} throw any I/O exceptions. If an {@code IOException} is 
035     * thrown by the underlying stream, it is immediately caught by this class. In order to
036     * allow the application to nevertheless access and control the error handling, the methods
037     * {@link #hadIOException()}, {@link #getIOException()} and {@link #resetIOException()} are
038     * provided.<br/>
039     * <br />
040     * An example of how to use this class can be found in
041     * {@link org.LiveGraph.demoDataSource.LiveGraphDemo}.<br />
042     * 
043     * <p>Here is a formal description of the file format produced by this class:</p>
044     * <p>The LiveGraph API reads and stores data in text-based data files. The file format is
045     * based on the widely used comma-separated-values (CSV) format. LiveGraph&prime;s file format
046     * was defined in such a way that a standard CSV file will be accepted and correctly parsed
047     * by the application (except that the very first data line might be interpreted as column
048     * headings - see below).</p>
049     * 
050     * <p>The format definition is as follows:</p>
051     * 
052     * <p>1. <strong>File is character and line based</strong>.<br />
053     * LiveGraph data files are text-files (i.e. not binary files). Files are read (written) on
054     * a line-by-line basis. Only after a complete line was read and parsed (or written) will
055     * the next line be considered.</p>
056     * 
057     * <p>2. <strong>Empty lines are ignored</strong>.<br />
058     * Any empty line or a line containing only white spaces is ignored without any further
059     * consequences.</p>
060     * 
061     * <p>3. <strong>Data values separator definition line</strong>.<br />
062     * The first non-empty line in a LiveGraph data file may contain an <em>optional</em> data
063     * values separator definition. A data values separator is a string which will separate data
064     * values in data lines.<br />
065     * A data values separator definition line must start and finish with the tag
066     * &quot<samp>##</samp>&quot;. The entire string between the opening &quot<samp>##</samp>&quot;
067     * and the closing &quot<samp>##</samp>&quot; will be the treated as the separator. For instance,
068     * the line &quot<samp>##(*)##</samp>&quot; defines the data values separator
069     * &quot<samp>(*)</samp>&quot;.<br />
070     * A data values separator definition may not appear anywhere else than on the first non-empty
071     * line of the data file.<br />
072     * If the data values separator definition is omitted the default data values separator will be
073     * the string &quot<samp>,</samp>&quot; (comma).</p>
074     * 
075     * <p>4. <strong>Comment lines</strong>.<br />
076     * Any line where the first non-whitespace character is &quot<samp>#</samp>&quot; (except for
077     * the data values separator definition line) is treated as a comment and is ignored. Note that
078     * no comments may be placed before the optional data values separator definition line.</p>
079     * 
080     * <p>5. <strong>File information and description lines</strong>.<br />
081     * Any line where the first non-whitespace character is &quot<samp>{@literal @}</samp>&quot; is treated as
082     * a file information or description line. A file information line does not have any effect on
083     * the interpretation of the data contained in the file; however, it may be used by a
084     * processing application to provide information to the end-user.</p>
085     * 
086     * <p>6. <strong>Data series labels line</strong>.<br />
087     * The first non-empty line in a data file which is not a data separator definition line or a
088     * comment line or a file information line is treated as data series labels line. This line
089     * defines the number and the labels of the data columns in the file. The line is split in
090     * tokens using the data values separator. The number of tokens defines the number of data
091     * columns in the file and the tokens define the labels of the columns. Note that the labels
092     * might be empty strings. For example:</p>
093     * 
094     * <pre>
095     *     ##;##
096     *     {@literal @Example 1}
097     *     ID;Age;;Height
098     *     . . .
099     * </pre>
100     * <pre>
101     *     {@literal @Example 2}
102     *     ,ID;Height,Age,weight,
103     *     . . .
104     * </pre>
105     * 
106     * <p>In example 1 the data separator is defined to be &quot<samp>;</samp>&quot; (semicolon).
107     * 4 data series are defined here: &quot<samp>ID</samp>&quot;, &quot<samp>Age</samp>&quot;,
108     * &quot<samp></samp>&quot; and &quot<samp>Height</samp>&quot (note that the third series
109     * label here is an empty string).<br />
110     * In example 2 no data separator is defined, so the default separator &quot<samp>,</samp>&quot
111     * (comma) is used. Note that &quot<samp>;</samp>&quot is not a separator in this case. This
112     * gives 5 data series with the following labels: &quot<samp></samp>&quot,
113     * &quot<samp>ID;Height</samp>&quot, &quot<samp>Age</samp>&quot, &quot<samp>weight</samp>&quot
114     * and &quot<samp></samp>&quot. Note that the first and the last series labels are empty
115     * strings. They are separated from the following (preceding) labels by the data separator.</p>
116     * 
117     * <p>7. <strong>Data lines</strong>.<br />
118     * Any non-empty line after the series labels line which is not a comment line or a file
119     * information line is treated as data values line. Data lines contain the actual data. They
120     * are parsed into tokens in the same way as the data series labels line, which means that
121     * some tokens may be empty strings. The LiveGraph API allows any string to be used as a token.
122     * (Note that the plotter application converts each token to a double precision floating point
123     * value; if a token is not a character string representing a valid decimal number, it will be
124     * converted to a not-a-number floating point value. This is interpreted by the plotter as a
125     * gap in the data series.) All data values on the same line are considered to belong to the
126     * same dataset. The data series of each value in a given dataset is determined by comparing
127     * the position index of the corresponding data token in the line to the number of the series
128     * label token in the labels line.</p> 
129     * 
130     * <p>Here is an example data file:</p>
131     * 
132     * <pre>
133     *     ##|##
134     *     {@literal @File info for the user}
135     *     {@literal @More info}
136     *     #Comment
137     *     Seconds|Dead mosquitos|Hungry frogs
138     *     1|0|100
139     *     600|1000|50
140     *     1500|5000|0
141     *     #Another comment
142     * </pre>
143     * <p>Here is another example:</p>
144     * <pre>
145     *     Seconds,Dead mosquitos,Hungry frogs
146     *     1,0,100
147     *     600,1000,50
148     *     1500,5000,0
149     * </pre>
150     * 
151     * <p style="font-size:smaller;">This product includes software developed by the
152     *    <strong>LiveGraph</strong> project and its contributors.<br />
153     *    (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>)<br />
154     *    Copyright (c) 2007 G. Paperin.<br />
155     *    All rights reserved.
156     * </p>
157     * <p style="font-size:smaller;">File: DataStreamWriter.java</p> 
158     * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or
159     *    without modification, are permitted provided that the following terms and conditions are met:
160     * </p>
161     * <p style="font-size:smaller;">1. Redistributions of source code must retain the above
162     *    acknowledgement of the LiveGraph project and its web-site, the above copyright notice,
163     *    this list of conditions and the following disclaimer.<br />
164     *    2. Redistributions in binary form must reproduce the above acknowledgement of the
165     *    LiveGraph project and its web-site, the above copyright notice, this list of conditions
166     *    and the following disclaimer in the documentation and/or other materials provided with
167     *    the distribution.<br />
168     *    3. All advertising materials mentioning features or use of this software or any derived
169     *    software must display the following acknowledgement:<br />
170     *    <em>This product includes software developed by the LiveGraph project and its
171     *    contributors.<br />(http://www.live-graph.org)</em><br />
172     *    4. All advertising materials distributed in form of HTML pages or any other technology
173     *    permitting active hyper-links that mention features or use of this software or any
174     *    derived software must display the acknowledgment specified in condition 3 of this
175     *    agreement, and in addition, include a visible and working hyper-link to the LiveGraph
176     *    homepage (http://www.live-graph.org).
177     * </p>
178     * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY
179     *    OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
180     *    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT SHALL
181     *    THE AUTHORS, CONTRIBUTORS OR COPYRIGHT  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
182     *    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING  FROM, OUT OF OR
183     *    IN CONNECTION WITH THE SOFTWARE OR THE USE OR  OTHER DEALINGS IN THE SOFTWARE.
184     * </p>
185     * 
186     * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>)
187     * @version {@value org.LiveGraph.LiveGraph#version}
188     */
189    public class DataStreamWriter {
190    
191    /**
192     * Streat writer for printing to the output stream.
193     */
194    private BufferedWriter out = null;
195    
196    /**
197     * Whether the data separator can still be changed. 
198     */
199    private boolean canChangeSeparator = true;
200    
201    /**
202     * The currently used data values separator.
203     */
204    private String separator = DefaultSeparator;
205    
206    
207    /**
208     * Whether new data series can still be added. 
209     */
210    private boolean canAddDataSeries = true;
211    
212    /**
213     * Holds the data series labels.
214     */
215    private List<String> dataSeriesLabels = null;
216    
217    /**
218     * Holds the series index cursor within the current dataset. 
219     */
220    private int currentSeriesIndex = 0;
221    
222    
223    /**
224     * Values of the current dataset.
225     */
226    private Map<String, String> dataCache = null;
227    
228    
229    /**
230     * Raised IOException (if any).
231     */
232    private IOException ioException = null;
233    
234    
235    /**
236     * Creates a new data writer to write on the specified stream.
237     * 
238     * @param os The stream to the the data will be written.
239     */
240    public DataStreamWriter(OutputStream os) {
241            this.out = new BufferedWriter(new OutputStreamWriter(os));
242            this.canChangeSeparator = true;
243            this.separator = DefaultSeparator;
244            this.canAddDataSeries = true;
245            this.dataSeriesLabels = new ArrayList<String>();
246            this.currentSeriesIndex = 0;    
247            this.dataCache = new HashMap<String, String>();
248            this.ioException = null;        
249    }
250    
251    
252    /**
253     * Closes the underlying output stream.
254     * If any of the data values which have previously been cached by any of the
255     * {@code setDataValue(...)}-methods are not written yet, they wre written to the stream before it is closed.
256     * Once this method was invoken, no more data can be written. 
257     */
258    public void close() {
259            if (!dataCache.isEmpty())
260                    writeDataSet();
261            try {           
262                    out.close();
263            } catch (IOException e) {
264                    ioException = e;
265            }
266    }
267    
268    /**
269     * Sets the separator between data columns and values. (Note - if the separator ends up being a substring
270     * of any data series label or any data value, than the parsing will lead to undefined results including
271     * possible unstable system behaviour).
272     * 
273     * @param sep The new separator.
274     * @throws IllegalStateException If the separator cannot be changed because other data was already written.
275     * @throws IllegalArgumentException If the specified separator is not allowed.
276     * @see org.LiveGraph.dataFile.common.DataFormatTools#isValidSeparator(String)
277     */
278    public void setSeparator(String sep) {
279            if (!canChangeSeparator)
280                    throw new IllegalStateException("Separator cannot be changed any more");
281            
282            String problem = isValidSeparator(sep);
283            if (null != problem)
284                    throw new IllegalArgumentException(problem);
285            
286            separator = sep;
287    }
288    
289    /**
290     * If a non-default separator was set it is written to the output stream, unless other data
291     * was already written.
292     */
293    private void checkWriteSeparatorDefinition() {
294            
295            if (!canChangeSeparator)
296                    return;
297            
298            canChangeSeparator = false;
299            if (DefaultSeparator.equals(separator))
300                    return;
301            try {
302                    out.write(TAGSepDefinition);
303                    out.write(separator);
304                    out.write(TAGSepDefinition);
305                    out.newLine();
306                    out.flush();
307            } catch (IOException e) {
308                    ioException = e;
309            }
310    }
311    
312    /**
313     * Writes data series label information to the output stream.
314     */
315    private void checkWriteSeriesLabels() {
316            
317            if (!canAddDataSeries)
318                    return;
319            
320            canAddDataSeries = false;
321            try {
322                    String sep = "";
323                    for (String label : dataSeriesLabels) {
324                            out.write(sep);
325                            out.write(label);
326                            sep = this.separator;
327                    }
328                    out.newLine();
329                    out.flush();
330            } catch (IOException e) {
331                    ioException = e;
332            }       
333    }
334    
335    /**
336     * Writes the specified comment to the output stream.
337     * If a data values separator has been previously set, it is written to the stream before the comment line.
338     * A sepataror may not be set after invoking this method.
339     * 
340     * @param comm A comment line.
341     */
342    public void writeComment(String comm) {
343            checkWriteSeparatorDefinition();
344            try {
345                    out.write(TAGComment);
346                    out.write(comm.trim());
347                    out.newLine();
348                    out.flush();
349            } catch (IOException e) {
350                    ioException = e;
351            }
352    }
353    
354    /**
355     * Writes the specified information or file description line to the output stream.
356     * If a data values separator has been previously set, it is written to the stream before the information line.
357     * A sepataror may not be set after invoking this method.
358     * 
359     * @param info An information or file description line.
360     */
361    public void writeFileInfo(String info) {
362            checkWriteSeparatorDefinition();
363            try {
364                    out.write(TAGFileInfo);
365                    out.write(info.trim());
366                    out.newLine();
367                    out.flush();
368            } catch (IOException e) {
369                    ioException = e;
370            }
371    }
372    
373    /**
374     * Checks whether this writer knows a data series with the specified label.
375     * 
376     * @param label A data series label.
377     * @return {@code true} if a data series has been defined on this writer,
378     * {@code false} otherwise.
379     */
380    public boolean dataSeriesExists(String label) {
381            return dataSeriesLabels.contains(label);
382    }
383    
384    
385    /**
386     * Defines a new data series with the specified label for this writer. The data columns
387     * representing the data series are placed in the order in which the data series have
388     * been defined.
389     *  
390     * @param label Label for the new data series.
391     * @throws NullPointerException If the label is {@code null}.
392     * @throws IllegalStateException If no more data series may be defined because datasets
393     * have already been written to the output stream.
394     */
395    public void addDataSeries(String label) {
396            if (null == label)
397                    throw new NullPointerException("Data series label may not be null");
398            
399            if (!canAddDataSeries)
400                    throw new IllegalStateException("Cannot add new data series at any more");
401            
402            label = label.trim();
403            
404            if (dataSeriesExists(label))
405                    return;
406            
407            dataSeriesLabels.add(label);
408    }
409    
410    /**
411     * Assigns the specified value to the specified data series in the current dataset.
412     * 
413     * @param seriesLabel Label of the series to which {@code value} is to be assigned. 
414     * @param value A value to include in the current dataset.
415     */
416    public void setDataValue(String seriesLabel, double value) {
417            if (null == seriesLabel)
418                    throw new NullPointerException("Data series label may not be null");
419            dataCache.put(seriesLabel.trim(), Double.toString(value));
420    }
421    
422    /**
423     * Assigns the specified value to the data series at the specified index in the
424     * current dataset.
425     * 
426     * @param seriesIndex Column index of the series to which {@code value} is to be assigned. 
427     * @param value A value to include in the current dataset.
428     * @throws IllegalArgumentException If {@code seriesIndex < 0} or if
429     * {@code seriesIndex >= (number of data-series defined for this writer)}. 
430     */
431    public void setDataValue(int seriesIndex, double value) {
432            if (0 > seriesIndex)
433                    throw new IllegalArgumentException("Series index may not be negative");
434            if (dataSeriesLabels.size() <= seriesIndex)
435                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
436                                                                                       seriesIndex + " >= " + dataSeriesLabels.size()+")");
437            dataCache.put(dataSeriesLabels.get(seriesIndex), Double.toString(value));
438    }
439    
440    /**
441     * Assigns the specified value to the next data series in the current dataset.
442     * The "next"-pointer is reset each time a dataset is written to the stream.
443     *  
444     * @param value A value to include in the current dataset.
445     * @throws IllegalArgumentException If there are no more data series defined for this writer.
446     */
447    public void setDataValue(double value) {
448            setDataValue(currentSeriesIndex++, value);      
449    }
450    
451    /**
452     * Assigns the specified value to the specified data series in the current dataset.
453     * 
454     * @param seriesLabel Label of the series to which {@code value} is to be assigned. 
455     * @param value A value to include in the current dataset.
456     */
457    public void setDataValue(String seriesLabel, float value) {
458            if (null == seriesLabel)
459                    throw new NullPointerException("Data series label may not be null");
460            dataCache.put(seriesLabel.trim(), Float.toString(value));
461    }
462    
463    /**
464     * Assigns the specified value to the data series at the specified index in the
465     * current dataset.
466     * 
467     * @param seriesIndex Column index of the series to which {@code value} is to be assigned. 
468     * @param value A value to include in the current dataset.
469     * @throws IllegalArgumentException If {@code seriesIndex < 0} or if
470     * {@code seriesIndex >= (number of data-series defined for this writer)}. 
471     */
472    public void setDataValue(int seriesIndex, float value) {
473            if (0 > seriesIndex)
474                    throw new IllegalArgumentException("Series index may not be negative");
475            if (dataSeriesLabels.size() <= seriesIndex)
476                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
477                                                                                       seriesIndex + " >=" + dataSeriesLabels.size()+")");
478            dataCache.put(dataSeriesLabels.get(seriesIndex), Float.toString(value));
479    }
480    
481    /**
482     * Assigns the specified value to the next data series in the current dataset.
483     * The "next"-pointer is reset each time a dataset is written to the stream.
484     *  
485     * @param value A value to include in the current dataset.
486     * @throws IllegalArgumentException If there are no more data series defined for this writer.
487     */
488    public void setDataValue(float value) {
489            setDataValue(currentSeriesIndex++, value);      
490    }
491    
492    /**
493     * Assigns the specified value to the specified data series in the current dataset.
494     * 
495     * @param seriesLabel Label of the series to which {@code value} is to be assigned. 
496     * @param value A value to include in the current dataset.
497     */
498    public void setDataValue(String seriesLabel, long value) {
499            if (null == seriesLabel)
500                    throw new NullPointerException("Data series label may not be null");
501            dataCache.put(seriesLabel.trim(), Long.toString(value));
502    }
503    
504    /**
505     * Assigns the specified value to the data series at the specified index in the
506     * current dataset.
507     * 
508     * @param seriesIndex Column index of the series to which {@code value} is to be assigned. 
509     * @param value A value to include in the current dataset.
510     * @throws IllegalArgumentException If {@code seriesIndex < 0} or if
511     * {@code seriesIndex >= (number of data-series defined for this writer)}. 
512     */
513    public void setDataValue(int seriesIndex, long value) {
514            if (0 > seriesIndex)
515                    throw new IllegalArgumentException("Series index may not be negative");
516            if (dataSeriesLabels.size() <= seriesIndex)
517                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
518                                                                                       seriesIndex + " >=" + dataSeriesLabels.size()+")");
519            dataCache.put(dataSeriesLabels.get(seriesIndex), Long.toString(value));
520    }
521    
522    /**
523     * Assigns the specified value to the next data series in the current dataset.
524     * The "next"-pointer is reset each time a dataset is written to the stream.
525     *  
526     * @param value A value to include in the current dataset.
527     * @throws IllegalArgumentException If there are no more data series defined for this writer.
528     */
529    public void setDataValue(long value) {
530            setDataValue(currentSeriesIndex++, value);      
531    }
532    
533    /**
534     * Assigns the specified value to the specified data series in the current dataset.
535     * 
536     * @param seriesLabel Label of the series to which {@code value} is to be assigned. 
537     * @param value A value to include in the current dataset.
538     */
539    public void setDataValue(String seriesLabel, int value) {
540            if (null == seriesLabel)
541                    throw new NullPointerException("Data series label may not be null");
542            dataCache.put(seriesLabel.trim(), Integer.toString(value));
543    }
544    
545    /**
546     * Assigns the specified value to the data series at the specified index in the
547     * current dataset.
548     * 
549     * @param seriesIndex Column index of the series to which {@code value} is to be assigned. 
550     * @param value A value to include in the current dataset.
551     * @throws IllegalArgumentException If {@code seriesIndex < 0} or if
552     * {@code seriesIndex >= (number of data-series defined for this writer)}. 
553     */
554    public void setDataValue(int seriesIndex, int value) {
555            if (0 > seriesIndex)
556                    throw new IllegalArgumentException("Series index may not be negative");
557            if (dataSeriesLabels.size() <= seriesIndex)
558                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
559                                                                                       seriesIndex + " >=" + dataSeriesLabels.size()+")");
560            dataCache.put(dataSeriesLabels.get(seriesIndex), Integer.toString(value));
561    }
562    
563    /**
564     * Assigns the specified value to the next data series in the current dataset.
565     * The "next"-pointer is reset each time a dataset is written to the stream.
566     *  
567     * @param value A value to include in the current dataset.
568     * @throws IllegalArgumentException If there are no more data series defined for this writer.
569     */
570    public void setDataValue(int value) {
571            setDataValue(currentSeriesIndex++, value);      
572    }
573    
574    /**
575     * Assigns the specified value to the specified data series in the current dataset.
576     * 
577     * @param seriesLabel Label of the series to which {@code value} is to be assigned. 
578     * @param value A value to include in the current dataset ({@code true} will be
579     * converted to {@code 1} and {@code false} will be converted to {@code 0}).
580     */
581    public void setDataValue(String seriesLabel, boolean value) {
582            if (null == seriesLabel)
583                    throw new NullPointerException("Data series label may not be null");
584            dataCache.put(seriesLabel.trim(), value ? "1" : "0");
585    }
586    
587    /**
588     * Assigns the specified value to the data series at the specified index in the
589     * current dataset.
590     * 
591     * @param seriesIndex Column index of the series to which {@code value} is to be assigned. 
592     * @param value A value to include in the current dataset ({@code true} will be
593     * converted to {@code 1} and {@code false} will be converted to {@code 0}).
594     * @throws IllegalArgumentException If {@code seriesIndex < 0} or if
595     * {@code seriesIndex >= (number of data-series defined for this writer)}. 
596     */
597    public void setDataValue(int seriesIndex, boolean value) {
598            if (0 > seriesIndex)
599                    throw new IllegalArgumentException("Series index may not be negative");
600            if (dataSeriesLabels.size() <= seriesIndex)
601                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
602                                                                                       seriesIndex + " >=" + dataSeriesLabels.size()+")");
603            dataCache.put(dataSeriesLabels.get(seriesIndex), value ? "1" : "0");
604    }
605    
606    /**
607     * Assigns the specified value to the next data series in the current dataset.
608     * The "next"-pointer is reset each time a dataset is written to the stream.
609     *  
610     * @param value A value to include in the current dataset ({@code true} will be
611     * converted to {@code 1} and {@code false} will be converted to {@code 0}).
612     * @throws IllegalArgumentException If there are no more data series defined for this writer.
613     */
614    public void setDataValue(boolean value) {
615            setDataValue(currentSeriesIndex++, value);      
616    }
617    
618    /**
619     * Assigns the specified value to the specified data series in the current dataset.
620     * 
621     * @param seriesLabel Label of the series to which {@code value} is to be assigned. 
622     * @param value A value to include in the current dataset ({@code null} will be
623     * converted to the empty string {@code ""}).
624     */
625    public void setDataValue(String seriesLabel, String value) {
626            if (null == seriesLabel)
627                    throw new NullPointerException("Data series label may not be null");
628            dataCache.put(seriesLabel.trim(), null == value ? "" : value);
629    }
630    
631    /**
632     * Assigns the specified value to the data series at the specified index in the
633     * current dataset.
634     * 
635     * @param seriesIndex Column index of the series to which {@code value} is to be assigned. 
636     * @param value A value to include in the current dataset ({@code null} will be
637     * converted to the empty string {@code ""}).
638     * @throws IllegalArgumentException If {@code seriesIndex < 0} or if
639     * {@code seriesIndex >= (number of data-series defined for this writer)}. 
640     */
641    public void setDataValue(int seriesIndex, String value) {
642            if (0 > seriesIndex)
643                    throw new IllegalArgumentException("Series index may not be negative");
644            if (dataSeriesLabels.size() <= seriesIndex)
645                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
646                                                                                       seriesIndex + " >=" + dataSeriesLabels.size()+")");
647            dataCache.put(dataSeriesLabels.get(seriesIndex), null == value ? "" : value);
648    }
649    
650    /**
651     * Assigns the specified value to the next data series in the current dataset.
652     * The "next"-pointer is reset each time a dataset is written to the stream.
653     *  
654     * @param value A value to include in the current dataset ({@code null} will be
655     * converted to the empty string {@code ""}).
656     * @throws IllegalArgumentException If there are no more data series defined for this writer.
657     */
658    public void setDataValue(String value) {
659            setDataValue(currentSeriesIndex++, value);      
660    }
661    
662    /**
663     * Gets the data value which has been previously associated with the specified data series in the
664     * current dataset.
665     * 
666     * @param seriesLabel The label of the data series to query.
667     * @return The data value which has been previously associated with the specified series in the
668     * current dataset as a {@code String} or {@code null} if no value was associated with the specified
669     * data series. 
670     */
671    public String getDataValue(String seriesLabel) {
672            if (null == seriesLabel)
673                    throw new NullPointerException("Data series label may not be null");
674            return dataCache.get(seriesLabel.trim());
675    }
676    
677    /**
678     * Gets the data value which has been previously associated with the specified data series in the
679     * current dataset.
680     * 
681     * @param seriesIndex Column index of the data series to query.
682     * @return The data value which has been previously associated with the specified series in the
683     * current dataset as a {@code String} or {@code null} if no value was associated with the specified
684     * data series. 
685     */
686    public String getDataValue(int seriesIndex) {
687            if (0 > seriesIndex)
688                    throw new IllegalArgumentException("Series index may not be negative");
689            if (dataSeriesLabels.size() <= seriesIndex)
690                    throw new IllegalArgumentException("Series index may not be >= number of data series (" + 
691                                                                                       seriesIndex + " >=" + dataSeriesLabels.size()+")");
692            
693            return dataCache.get(dataSeriesLabels.get(seriesIndex));
694    }
695    
696    /**
697     * Writes the current dataset to the output stream.
698     * If a data separator was explicitly defined and not yet written, it is written to the output stream
699     * before the data.
700     * If the data series (column) labels were not yet written, they are written to the output stream
701     * before the data.
702     * After invoking this method the data separator cannot be changed and no more new data series can be defined.
703     */
704    public void writeDataSet() {
705            checkWriteSeparatorDefinition();
706            checkWriteSeriesLabels();
707            try {
708                    String sep = "";
709                    String val = null;
710                    for (String label : dataSeriesLabels) {
711                            val = dataCache.get(label);
712                            if (null == val)
713                                    val = "";
714                            out.write(sep);                 
715                            out.write(val);
716                            sep = this.separator;
717                    }
718                    out.newLine();
719                    out.flush();
720            } catch (IOException e) {
721                    ioException = e;
722            }
723            dataCache.clear();
724            currentSeriesIndex = 0;
725    }
726    
727    /**
728     * Check whether a recent operation caused an {@code IOException}. 
729     * @return {@code true} if an {@code IOException} was encountered after this writer was created or after
730     * the last call to {@link #resetIOException()}, {@code false} otherwise.
731     */
732    public boolean hadIOException() {
733            return (null != ioException);
734    }
735    
736    /**
737     * Gets the last {@code IOException} encountered by this writer.
738     * 
739     * @return If {@link #hadIOException()} returns {@code true} - the last {@code IOException} encountered
740     * by this writer, otherwise - {@code null}.
741     */
742    public IOException getIOException() {
743            return ioException;
744    }
745    
746    /**
747     * Deletes any internal state concerned with previously encountered {@code IOException}s.
748     *
749     */
750    public void resetIOException() {
751            ioException = null;
752    }
753    
754    } // public class DataStreamWriter