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 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