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′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 * "<samp>##</samp>". The entire string between the opening "<samp>##</samp>"
067 * and the closing "<samp>##</samp>" will be the treated as the separator. For instance,
068 * the line "<samp>##(*)##</samp>" defines the data values separator
069 * "<samp>(*)</samp>".<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 "<samp>,</samp>" (comma).</p>
074 *
075 * <p>4. <strong>Comment lines</strong>.<br />
076 * Any line where the first non-whitespace character is "<samp>#</samp>" (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 "<samp>{@literal @}</samp>" 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 "<samp>;</samp>" (semicolon).
107 * 4 data series are defined here: "<samp>ID</samp>", "<samp>Age</samp>",
108 * "<samp></samp>" and "<samp>Height</samp>" (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 "<samp>,</samp>"
111 * (comma) is used. Note that "<samp>;</samp>" is not a separator in this case. This
112 * gives 5 data series with the following labels: "<samp></samp>",
113 * "<samp>ID;Height</samp>", "<samp>Age</samp>", "<samp>weight</samp>"
114 * and "<samp></samp>". 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-2008 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 "AS IS", 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