View Javadoc

1   /* ============================================================
2    * JRobin : Pure java implementation of RRDTool's functionality
3    * ============================================================
4    *
5    * Project Info:  http://www.jrobin.org
6    * Project Lead:  Sasa Markovic (saxon@jrobin.org);
7    *
8    * (C) Copyright 2003-2005, by Sasa Markovic.
9    *
10   * Developers:    Sasa Markovic (saxon@jrobin.org)
11   *
12   *
13   * This library is free software; you can redistribute it and/or modify it under the terms
14   * of the GNU Lesser General Public License as published by the Free Software Foundation;
15   * either version 2.1 of the License, or (at your option) any later version.
16   *
17   * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
18   * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19   * See the GNU Lesser General Public License for more details.
20   *
21   * You should have received a copy of the GNU Lesser General Public License along with this
22   * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
23   * Boston, MA 02111-1307, USA.
24   */
25  
26  package org.jrobin.data;
27  
28  import org.jrobin.core.*;
29  
30  import java.io.IOException;
31  import java.util.*;
32  
33  /**
34   * Class which should be used for all calculations based on the data fetched from RRD files. This class
35   * supports ordinary DEF datasources (defined in RRD files), CDEF datasources (RPN expressions evaluation),
36   * SDEF (static datasources - extension of JRobin) and PDEF (plottables, see
37   * {@link Plottable Plottable} for more information.<p>
38   * <p/>
39   * Typical class usage:<p>
40   * <pre>
41   * final long t1 = ...
42   * final long t2 = ...
43   * DataProcessor dp = new DataProcessor(t1, t2);
44   * // DEF datasource
45   * dp.addDatasource("x", "demo.rrd", "some_source", "AVERAGE");
46   * // DEF datasource
47   * dp.addDatasource("y", "demo.rrd", "some_other_source", "AVERAGE");
48   * // CDEF datasource, z = (x + y) / 2
49   * dp.addDatasource("z", "x,y,+,2,/");
50   * // ACTION!
51   * dp.processData();
52   * // Dump calculated values
53   * System.out.println(dp.dump());
54   * </pre>
55   */
56  public class DataProcessor implements ConsolFuns {
57  	/**
58  	 * Constant representing the default number of pixels on a JRobin graph (will be used if
59  	 * no other value is specified with {@link #setStep(long) setStep()} method.
60  	 */
61  	public static final int DEFAULT_PIXEL_COUNT = 600;
62  	private static final double DEFAULT_PERCENTILE = 95.0; // %
63  
64  	private int pixelCount = DEFAULT_PIXEL_COUNT;
65  
66  	/**
67  	 * Constant that defines the default {@link RrdDbPool} usage policy. Defaults to <code>false</code>
68  	 * (i.e. the pool will not be used to fetch data from RRD files)
69  	 */
70  	public static final boolean DEFAULT_POOL_USAGE_POLICY = false;
71  	private boolean poolUsed = DEFAULT_POOL_USAGE_POLICY;
72  
73  	private final long tStart;
74  	private long tEnd, timestamps[];
75  	private long lastRrdArchiveUpdateTime = 0;
76  	// this will be adjusted later
77  	private long step = 0;
78  	// resolution to be used for RRD fetch operation
79  	private long fetchRequestResolution = 1;
80  
81  	// the order is important, ordinary HashMap is unordered
82  	private Map<String, Source> sources = new LinkedHashMap<String, Source>();
83  
84  	private Def[] defSources;
85  
86  	/**
87  	 * Creates new DataProcessor object for the given time span. Ending timestamp may be set to zero.
88  	 * In that case, the class will try to find the optimal ending timestamp based on the last update time of
89  	 * RRD files processed with the {@link #processData()} method.
90  	 *
91  	 * @param t1 Starting timestamp in seconds without milliseconds
92  	 * @param t2 Ending timestamp in seconds without milliseconds
93  	 * @throws RrdException Thrown if invalid timestamps are supplied
94  	 */
95  	public DataProcessor(long t1, long t2) throws RrdException {
96  		if ((t1 < t2 && t1 > 0 && t2 > 0) || (t1 > 0 && t2 == 0)) {
97  			this.tStart = t1;
98  			this.tEnd = t2;
99  		}
100 		else {
101 			throw new RrdException("Invalid timestamps specified: " + t1 + ", " + t2);
102 		}
103 	}
104 
105 	/**
106 	 * Creates new DataProcessor object for the given time span. Ending date may be set to null.
107 	 * In that case, the class will try to find optimal ending date based on the last update time of
108 	 * RRD files processed with the {@link #processData()} method.
109 	 *
110 	 * @param d1 Starting date
111 	 * @param d2 Ending date
112 	 * @throws RrdException Thrown if invalid timestamps are supplied
113 	 */
114 	public DataProcessor(Date d1, Date d2) throws RrdException {
115 		this(Util.getTimestamp(d1), d2 != null ? Util.getTimestamp(d2) : 0);
116 	}
117 
118 	/**
119 	 * Creates new DataProcessor object for the given time span. Ending date may be set to null.
120 	 * In that case, the class will try to find optimal ending date based on the last update time of
121 	 * RRD files processed with the {@link #processData()} method.
122 	 *
123 	 * @param gc1 Starting Calendar date
124 	 * @param gc2 Ending Calendar date
125 	 * @throws RrdException Thrown if invalid timestamps are supplied
126 	 */
127 	public DataProcessor(Calendar gc1, Calendar gc2) throws RrdException {
128 		this(Util.getTimestamp(gc1), gc2 != null ? Util.getTimestamp(gc2) : 0);
129 	}
130 
131 	/**
132 	 * Returns boolean value representing {@link org.jrobin.core.RrdDbPool RrdDbPool} usage policy.
133 	 *
134 	 * @return true, if the pool will be used internally to fetch data from RRD files, false otherwise.
135 	 */
136 	public boolean isPoolUsed() {
137 		return poolUsed;
138 	}
139 
140 	/**
141 	 * Sets the {@link org.jrobin.core.RrdDbPool RrdDbPool} usage policy.
142 	 *
143 	 * @param poolUsed true, if the pool should be used to fetch data from RRD files, false otherwise.
144 	 */
145 	public void setPoolUsed(boolean poolUsed) {
146 		this.poolUsed = poolUsed;
147 	}
148 
149 	/**
150 	 * Sets the number of pixels (target graph width). This number is used only to calculate pixel coordinates
151 	 * for JRobin graphs (methods {@link #getValuesPerPixel(String)} and {@link #getTimestampsPerPixel()}),
152 	 * but has influence neither on datasource values calculated with the
153 	 * {@link #processData()} method nor on aggregated values returned from {@link #getAggregates(String)}
154 	 * and similar methods. In other words, aggregated values will not change once you decide to change
155 	 * the dimension of your graph.<p>
156 	 * <p/>
157 	 * The default number of pixels is defined by constant {@link #DEFAULT_PIXEL_COUNT}
158 	 * and can be changed with a {@link #setPixelCount(int)} method.
159 	 *
160 	 * @param pixelCount The number of pixels. If you process RRD data in order to display it on the graph,
161 	 *                   this should be the width of your graph.
162 	 */
163 	public void setPixelCount(int pixelCount) {
164 		this.pixelCount = pixelCount;
165 	}
166 
167 	/**
168 	 * Returns the number of pixels (target graph width). See {@link #setPixelCount(int)} for more information.
169 	 *
170 	 * @return Target graph width
171 	 */
172 	public int getPixelCount() {
173 		return pixelCount;
174 	}
175 
176 	/**
177 	 * Roughly corresponds to the --step option in RRDTool's graph/xport commands. Here is an explanation borrowed
178 	 * from RRDTool:<p>
179 	 * <p/>
180 	 * <i>"By default rrdgraph calculates the width of one pixel in the time
181 	 * domain and tries to get data at that resolution from the RRD. With
182 	 * this switch you can override this behavior. If you want rrdgraph to
183 	 * get data at 1 hour resolution from the RRD, then you can set the
184 	 * step to 3600 seconds. Note, that a step smaller than 1 pixel will
185 	 * be silently ignored."</i><p>
186 	 * <p/>
187 	 * I think this option is not that useful, but it's here just for compatibility.<p>
188 	 *
189 	 * @param step Time step at which data should be fetched from RRD files. If this method is not used,
190 	 *             the step will be equal to the smallest RRD step of all processed RRD files. If no RRD file is processed,
191 	 *             the step will be roughly equal to the with of one graph pixel (in seconds).
192 	 */
193 	public void setStep(long step) {
194 		this.step = step;
195 	}
196 
197 	/**
198 	 * Returns the time step used for data processing. Initially, this method returns zero.
199 	 * Once {@link #processData()} is finished, the method will return the real value used for
200 	 * all internal computations. Roughly corresponds to the --step option in RRDTool's graph/xport commands.
201 	 *
202 	 * @return Step used for data processing.
203 	 */
204 	public long getStep() {
205 		return step;
206 	}
207 
208 	/**
209 	 * Returns desired RRD archive step (reslution) in seconds to be used while fetching data
210 	 * from RRD files. In other words, this value will used as the last parameter of
211 	 * {@link RrdDb#createFetchRequest(String, long, long, long) RrdDb.createFetchRequest()} method
212 	 * when this method is called internally by this DataProcessor.
213 	 *
214 	 * @return Desired archive step (fetch resolution) in seconds.
215 	 */
216 	public long getFetchRequestResolution() {
217 		return fetchRequestResolution;
218 	}
219 
220 	/**
221 	 * Sets desired RRD archive step in seconds to be used internally while fetching data
222 	 * from RRD files. In other words, this value will used as the last parameter of
223 	 * {@link RrdDb#createFetchRequest(String, long, long, long) RrdDb.createFetchRequest()} method
224 	 * when this method is called internally by this DataProcessor. If this method is never called, fetch
225 	 * request resolution defaults to 1 (smallest possible archive step will be chosen automatically).
226 	 *
227 	 * @param fetchRequestResolution Desired archive step (fetch resoltuion) in seconds.
228 	 */
229 	public void setFetchRequestResolution(long fetchRequestResolution) {
230 		this.fetchRequestResolution = fetchRequestResolution;
231 	}
232 
233 	/**
234 	 * Returns ending timestamp. Basically, this value is equal to the ending timestamp
235 	 * specified in the constructor. However, if the ending timestamps was zero, it
236 	 * will be replaced with the real timestamp when the {@link #processData()} method returns. The real
237 	 * value will be calculated from the last update times of processed RRD files.
238 	 *
239 	 * @return Ending timestamp in seconds
240 	 */
241 	public long getEndingTimestamp() {
242 		return tEnd;
243 	}
244 
245 	/**
246 	 * Returns consolidated timestamps created with the {@link #processData()} method.
247 	 *
248 	 * @return array of timestamps in seconds
249 	 * @throws RrdException thrown if timestamps are not calculated yet
250 	 */
251 	public long[] getTimestamps() throws RrdException {
252 		if (timestamps == null) {
253 			throw new RrdException("Timestamps not calculated yet");
254 		}
255 		else {
256 			return timestamps;
257 		}
258 	}
259 
260 	/**
261 	 * Returns calculated values for a single datasource. Corresponding timestamps can be obtained from
262 	 * the {@link #getTimestamps()} method.
263 	 *
264 	 * @param sourceName Datasource name
265 	 * @return an array of datasource values
266 	 * @throws RrdException Thrown if invalid datasource name is specified,
267 	 *                      or if datasource values are not yet calculated (method {@link #processData()}
268 	 *                      was not called)
269 	 */
270 	public double[] getValues(String sourceName) throws RrdException {
271 		Source source = getSource(sourceName);
272 		double[] values = source.getValues();
273 		if (values == null) {
274 			throw new RrdException("Values not available for source [" + sourceName + "]");
275 		}
276 		return values;
277 	}
278 
279 	/**
280 	 * Returns single aggregated value for a single datasource.
281 	 *
282 	 * @param sourceName Datasource name
283 	 * @param consolFun  Consolidation function to be applied to fetched datasource values.
284 	 *                   Valid consolidation functions are MIN, MAX, LAST, FIRST, AVERAGE and TOTAL
285 	 *                   (these string constants are conveniently defined in the {@link ConsolFuns} class)
286 	 * @return MIN, MAX, LAST, FIRST, AVERAGE or TOTAL value calculated from the data
287 	 *         for the given datasource name
288 	 * @throws RrdException Thrown if invalid datasource name is specified,
289 	 *                      or if datasource values are not yet calculated (method {@link #processData()}
290 	 *                      was not called)
291 	 */
292 	public double getAggregate(String sourceName, String consolFun) throws RrdException {
293 		Source source = getSource(sourceName);
294 		return source.getAggregates(tStart, tEnd).getAggregate(consolFun);
295 	}
296 
297 	/**
298 	 * Returns all (MIN, MAX, LAST, FIRST, AVERAGE and TOTAL) aggregated values for a single datasource.
299 	 *
300 	 * @param sourceName Datasource name
301 	 * @return Object containing all aggregated values
302 	 * @throws RrdException Thrown if invalid datasource name is specified,
303 	 *                      or if datasource values are not yet calculated (method {@link #processData()}
304 	 *                      was not called)
305 	 */
306 	public Aggregates getAggregates(String sourceName) throws RrdException {
307 		Source source = getSource(sourceName);
308 		return source.getAggregates(tStart, tEnd);
309 	}
310 
311 	/**
312 	 * This method is just an alias for {@link #getPercentile(String)} method.
313 	 * <p/>
314 	 * Used by ISPs which charge for bandwidth utilization on a "95th percentile" basis.<p>
315 	 * <p/>
316 	 * The 95th percentile is the highest source value left when the top 5% of a numerically sorted set
317 	 * of source data is discarded. It is used as a measure of the peak value used when one discounts
318 	 * a fair amount for transitory spikes. This makes it markedly different from the average.<p>
319 	 * <p/>
320 	 * Read more about this topic at
321 	 * <a href="http://www.red.net/support/resourcecentre/leasedline/percentile.php">Rednet</a> or
322 	 * <a href="http://www.bytemark.co.uk/support/tech/95thpercentile.html">Bytemark</a>.
323 	 *
324 	 * @param sourceName Datasource name
325 	 * @return 95th percentile of fetched source values
326 	 * @throws RrdException Thrown if invalid source name is supplied
327 	 */
328 	public double get95Percentile(String sourceName) throws RrdException {
329 		return getPercentile(sourceName);
330 	}
331 
332 	/**
333 	 * Used by ISPs which charge for bandwidth utilization on a "95th percentile" basis.<p>
334 	 * <p/>
335 	 * The 95th percentile is the highest source value left when the top 5% of a numerically sorted set
336 	 * of source data is discarded. It is used as a measure of the peak value used when one discounts
337 	 * a fair amount for transitory spikes. This makes it markedly different from the average.<p>
338 	 * <p/>
339 	 * Read more about this topic at
340 	 * <a href="http://www.red.net/support/resourcecentre/leasedline/percentile.php">Rednet</a> or
341 	 * <a href="http://www.bytemark.co.uk/support/tech/95thpercentile.html">Bytemark</a>.
342 	 *
343 	 * @param sourceName Datasource name
344 	 * @return 95th percentile of fetched source values
345 	 * @throws RrdException Thrown if invalid source name is supplied
346 	 */
347 	public double getPercentile(String sourceName) throws RrdException {
348 		return getPercentile(sourceName, DEFAULT_PERCENTILE);
349 	}
350 
351 	/**
352 	 * The same as {@link #getPercentile(String)} but with a possibility to define custom percentile boundary
353 	 * (different from 95).
354 	 *
355 	 * @param sourceName Datasource name.
356 	 * @param percentile Boundary percentile. Value of 95 (%) is suitable in most cases, but you are free
357 	 *                   to provide your own percentile boundary between zero and 100.
358 	 * @return Requested percentile of fetched source values
359 	 * @throws RrdException Thrown if invalid sourcename is supplied, or if the percentile value makes no sense.
360 	 */
361 	public double getPercentile(String sourceName, double percentile) throws RrdException {
362 		if (percentile <= 0.0 || percentile > 100.0) {
363 			throw new RrdException("Invalid percentile [" + percentile + "], should be between 0 and 100");
364 		}
365 		Source source = getSource(sourceName);
366 		return source.getPercentile(tStart, tEnd, percentile);
367 	}
368 
369 	/**
370 	 * Returns array of datasource names defined in this DataProcessor.
371 	 *
372 	 * @return array of datasource names
373 	 */
374 	public String[] getSourceNames() {
375 		return sources.keySet().toArray(new String[0]);
376 	}
377 
378 	/**
379 	 * Returns an array of all datasource values for all datasources. Each row in this two-dimensional
380 	 * array represents an array of calculated values for a single datasource. The order of rows is the same
381 	 * as the order in which datasources were added to this DataProcessor object.
382 	 *
383 	 * @return All datasource values for all datasources. The first index is the index of the datasource,
384 	 *         the second index is the index of the datasource value. The number of datasource values is equal
385 	 *         to the number of timestamps returned with {@link #getTimestamps()}  method.
386 	 * @throws RrdException Thrown if invalid datasource name is specified,
387 	 *                      or if datasource values are not yet calculated (method {@link #processData()}
388 	 *                      was not called)
389 	 */
390 	public double[][] getValues() throws RrdException {
391 		String[] names = getSourceNames();
392 		double[][] values = new double[names.length][];
393 		for (int i = 0; i < names.length; i++) {
394 			values[i] = getValues(names[i]);
395 		}
396 		return values;
397 	}
398 
399 	private Source getSource(String sourceName) throws RrdException {
400 		Source source = sources.get(sourceName);
401 		if (source != null) {
402 			return source;
403 		}
404 		throw new RrdException("Unknown source: " + sourceName);
405 	}
406 
407 	/////////////////////////////////////////////////////////////////
408 	// DATASOURCE DEFINITIONS
409 	/////////////////////////////////////////////////////////////////
410 
411 	/**
412 	 * <p>Adds a custom, {@link org.jrobin.data.Plottable plottable} datasource (<b>PDEF</b>).
413 	 * The datapoints should be made available by a class extending
414 	 * {@link org.jrobin.data.Plottable Plottable} class.</p>
415 	 *
416 	 * @param name	  source name.
417 	 * @param plottable class that extends Plottable class and is suited for graphing.
418 	 */
419 	public void addDatasource(String name, Plottable plottable) {
420 		PDef pDef = new PDef(name, plottable);
421 		sources.put(name, pDef);
422 	}
423 
424 	/**
425 	 * <p>Adds complex source (<b>CDEF</b>).
426 	 * Complex sources are evaluated using the supplied <code>RPN</code> expression.</p>
427 	 * <p/>
428 	 * <p>Complex source <code>name</code> can be used:</p>
429 	 * <ul>
430 	 * <li>To specify sources for line, area and stack plots.</li>
431 	 * <li>To define other complex sources.</li>
432 	 * </ul>
433 	 * <p/>
434 	 * <p>JRobin supports the following RPN functions, operators and constants: +, -, *, /,
435 	 * %, SIN, COS, LOG, EXP, FLOOR, CEIL, ROUND, POW, ABS, SQRT, RANDOM, LT, LE, GT, GE, EQ,
436 	 * IF, MIN, MAX, LIMIT, DUP, EXC, POP, UN, UNKN, NOW, TIME, PI, E,
437 	 * AND, OR, XOR, PREV, PREV(sourceName), INF, NEGINF, STEP, YEAR, MONTH, DATE,
438 	 * HOUR, MINUTE, SECOND, WEEK, SIGN and RND.</p>
439 	 * <p/>
440 	 * <p>JRobin does not force you to specify at least one simple source name as RRDTool.</p>
441 	 * <p/>
442 	 * <p>For more details on RPN see RRDTool's
443 	 * <a href="http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/manual/rrdgraph.html" target="man">
444 	 * rrdgraph man page</a>.</p>
445 	 *
446 	 * @param name		  source name.
447 	 * @param rpnExpression RPN expression containig comma (or space) delimited simple and complex
448 	 *                      source names, RPN constants, functions and operators.
449 	 */
450 	public void addDatasource(String name, String rpnExpression) {
451 		CDef cDef = new CDef(name, rpnExpression);
452 		sources.put(name, cDef);
453 	}
454 
455 	/**
456 	 * <p>Adds static source (<b>SDEF</b>). Static sources are the result of a consolidation function applied
457 	 * to *any* other source that has been defined previously.</p>
458 	 *
459 	 * @param name	  source name.
460 	 * @param defName   Name of the datasource to calculate the value from.
461 	 * @param consolFun Consolidation function to use for value calculation
462 	 */
463 	public void addDatasource(String name, String defName, String consolFun) {
464 		SDef sDef = new SDef(name, defName, consolFun);
465 		sources.put(name, sDef);
466 	}
467 
468 	/**
469 	 * <p>Adds simple datasource (<b>DEF</b>). Simple source <code>name</code>
470 	 * can be used:</p>
471 	 * <ul>
472 	 * <li>To specify sources for line, area and stack plots.</li>
473 	 * <li>To define complex sources
474 	 * </ul>
475 	 *
476 	 * @param name	   source name.
477 	 * @param file	   Path to RRD file.
478 	 * @param dsName	 Datasource name defined in the RRD file.
479 	 * @param consolFunc Consolidation function that will be used to extract data from the RRD
480 	 *                   file ("AVERAGE", "MIN", "MAX" or "LAST" - these string constants are conveniently defined
481 	 *                   in the {@link org.jrobin.core.ConsolFuns ConsolFuns} class).
482 	 */
483 	public void addDatasource(String name, String file, String dsName, String consolFunc) {
484 		Def def = new Def(name, file, dsName, consolFunc);
485 		sources.put(name, def);
486 	}
487 
488 	/**
489 	 * <p>Adds simple source (<b>DEF</b>). Source <code>name</code> can be used:</p>
490 	 * <ul>
491 	 * <li>To specify sources for line, area and stack plots.</li>
492 	 * <li>To define complex sources
493 	 * </ul>
494 	 *
495 	 * @param name	   Source name.
496 	 * @param file	   Path to RRD file.
497 	 * @param dsName	 Data source name defined in the RRD file.
498 	 * @param consolFunc Consolidation function that will be used to extract data from the RRD
499 	 *                   file ("AVERAGE", "MIN", "MAX" or "LAST" - these string constants are conveniently defined
500 	 *                   in the {@link org.jrobin.core.ConsolFuns ConsolFuns} class).
501 	 * @param backend	Name of the RrdBackendFactory that should be used for this RrdDb.
502 	 */
503 	public void addDatasource(String name, String file, String dsName, String consolFunc, String backend) {
504 		Def def = new Def(name, file, dsName, consolFunc, backend);
505 		sources.put(name, def);
506 	}
507 
508 	/**
509 	 * Adds DEF datasource with datasource values already available in the FetchData object. This method is
510 	 * used internally by JRobin and probably has no purpose outside of it.
511 	 *
512 	 * @param name	  Source name.
513 	 * @param fetchData Fetched data containing values for the given source name.
514 	 */
515 	public void addDatasource(String name, FetchData fetchData) {
516 		Def def = new Def(name, fetchData);
517 		sources.put(name, def);
518 	}
519 
520 	/////////////////////////////////////////////////////////////////
521 	// CALCULATIONS
522 	/////////////////////////////////////////////////////////////////
523 
524 	/**
525 	 * Method that should be called once all datasources are defined. Data will be fetched from
526 	 * RRD files, RPN expressions will be calculated, etc.
527 	 *
528 	 * @throws IOException  Thrown in case of I/O error (while fetching data from RRD files)
529 	 * @throws RrdException Thrown in case of JRobin specific error
530 	 */
531 	public void processData() throws IOException, RrdException {
532 		extractDefs();
533 		fetchRrdData();
534 		fixZeroEndingTimestamp();
535 		chooseOptimalStep();
536 		createTimestamps();
537 		assignTimestampsToSources();
538 		normalizeRrdValues();
539 		calculateNonRrdSources();
540 	}
541 
542 	/**
543 	 * Method used to calculate datasource values which should be presented on the graph
544 	 * based on the desired graph width. Each value returned represents a single pixel on the graph.
545 	 * Corresponding timestamp can be found in the array returned from {@link #getTimestampsPerPixel()}
546 	 * method.
547 	 *
548 	 * @param sourceName Datasource name
549 	 * @param pixelCount Graph width
550 	 * @return Per-pixel datasource values
551 	 * @throws RrdException Thrown if datasource values are not yet calculated (method {@link #processData()}
552 	 *                      was not called)
553 	 */
554 	public double[] getValuesPerPixel(String sourceName, int pixelCount) throws RrdException {
555 		setPixelCount(pixelCount);
556 		return getValuesPerPixel(sourceName);
557 	}
558 
559 	/**
560 	 * Method used to calculate datasource values which should be presented on the graph
561 	 * based on the graph width set with a {@link #setPixelCount(int)} method call.
562 	 * Each value returned represents a single pixel on the graph. Corresponding timestamp can be
563 	 * found in the array returned from {@link #getTimestampsPerPixel()} method.
564 	 *
565 	 * @param sourceName Datasource name
566 	 * @return Per-pixel datasource values
567 	 * @throws RrdException Thrown if datasource values are not yet calculated (method {@link #processData()}
568 	 *                      was not called)
569 	 */
570 	public double[] getValuesPerPixel(String sourceName) throws RrdException {
571 		double[] values = getValues(sourceName);
572 		double[] pixelValues = new double[pixelCount];
573 		Arrays.fill(pixelValues, Double.NaN);
574 		long span = tEnd - tStart;
575 		// this is the ugliest nested loop I have ever made
576 		for (int pix = 0, ref = 0; pix < pixelCount; pix++) {
577 			double t = tStart + (double) (span * pix) / (double) (pixelCount - 1);
578 			while (ref < timestamps.length) {
579 				if (t <= timestamps[ref] - step) {
580 					// too left, nothing to do, already NaN
581 					break;
582 				}
583 				else if (t <= timestamps[ref]) {
584 					// in brackets, get this value
585 					pixelValues[pix] = values[ref];
586 					break;
587 				}
588 				else {
589 					// too right
590 					ref++;
591 				}
592 			}
593 		}
594 		return pixelValues;
595 	}
596 
597 	/**
598 	 * Calculates timestamps which correspond to individual pixels on the graph.
599 	 *
600 	 * @param pixelCount Graph width
601 	 * @return Array of timestamps
602 	 */
603 	public long[] getTimestampsPerPixel(int pixelCount) {
604 		setPixelCount(pixelCount);
605 		return getTimestampsPerPixel();
606 	}
607 
608 	/**
609 	 * Calculates timestamps which correspond to individual pixels on the graph
610 	 * based on the graph width set with a {@link #setPixelCount(int)} method call.
611 	 *
612 	 * @return Array of timestamps
613 	 */
614 	public long[] getTimestampsPerPixel() {
615 		long[] times = new long[pixelCount];
616 		long span = tEnd - tStart;
617 		for (int i = 0; i < pixelCount; i++) {
618 			times[i] = Math.round(tStart + (double) (span * i) / (double) (pixelCount - 1));
619 		}
620 		return times;
621 	}
622 
623 	/**
624 	 * Dumps timestamps and values of all datasources in a tabelar form. Very useful for debugging.
625 	 *
626 	 * @return Dumped object content.
627 	 * @throws RrdException Thrown if nothing is calculated so far (the method {@link #processData()}
628 	 *                      was not called).
629 	 */
630 	public String dump() throws RrdException {
631 		String[] names = getSourceNames();
632 		double[][] values = getValues();
633 		StringBuffer buffer = new StringBuffer();
634 		buffer.append(format("timestamp", 12));
635 		for (String name : names) {
636 			buffer.append(format(name, 20));
637 		}
638 		buffer.append("\n");
639 		for (int i = 0; i < timestamps.length; i++) {
640 			buffer.append(format("" + timestamps[i], 12));
641 			for (int j = 0; j < names.length; j++) {
642 				buffer.append(format(Util.formatDouble(values[j][i]), 20));
643 			}
644 			buffer.append("\n");
645 		}
646 		return buffer.toString();
647 	}
648 
649 	/**
650 	 * Returns time when last RRD archive was updated (all RRD files are considered).
651 	 *
652 	 * @return Last archive update time for all RRD files in this DataProcessor
653 	 */
654 	public long getLastRrdArchiveUpdateTime() {
655 		return lastRrdArchiveUpdateTime;
656 	}
657 
658 	// PRIVATE METHODS
659 
660 	private void extractDefs() {
661 		List<Def> defList = new ArrayList<Def>();
662 		for (Source source : sources.values()) {
663 			if (source instanceof Def) {
664 				defList.add((Def) source);
665 			}
666 		}
667 		defSources = defList.toArray(new Def[defList.size()]);
668 	}
669 
670 	private void fetchRrdData() throws IOException, RrdException {
671 		long tEndFixed = (tEnd == 0) ? Util.getTime() : tEnd;
672 		for (int i = 0; i < defSources.length; i++) {
673 			if (!defSources[i].isLoaded()) {
674 				// not fetched yet
675 				Set<String> dsNames = new HashSet<String>();
676 				dsNames.add(defSources[i].getDsName());
677 				// look for all other datasources with the same path and the same consolidation function
678 				for (int j = i + 1; j < defSources.length; j++) {
679 					if (defSources[i].isCompatibleWith(defSources[j])) {
680 						dsNames.add(defSources[j].getDsName());
681 					}
682 				}
683 				// now we have everything
684 				RrdDb rrd = null;
685 				try {
686 					rrd = getRrd(defSources[i]);
687 					lastRrdArchiveUpdateTime = Math.max(lastRrdArchiveUpdateTime, rrd.getLastArchiveUpdateTime());
688 					FetchRequest req = rrd.createFetchRequest(defSources[i].getConsolFun(),
689 							tStart, tEndFixed, fetchRequestResolution);
690 					req.setFilter(dsNames);
691 					FetchData data = req.fetchData();
692 					defSources[i].setFetchData(data);
693 					for (int j = i + 1; j < defSources.length; j++) {
694 						if (defSources[i].isCompatibleWith(defSources[j])) {
695 							defSources[j].setFetchData(data);
696 						}
697 					}
698 				}
699 				finally {
700 					if (rrd != null) {
701 						releaseRrd(rrd, defSources[i]);
702 					}
703 				}
704 			}
705 		}
706 	}
707 
708 	private void fixZeroEndingTimestamp() throws RrdException {
709 		if (tEnd == 0) {
710 			if (defSources.length == 0) {
711 				throw new RrdException("Could not adjust zero ending timestamp, no DEF source provided");
712 			}
713 			tEnd = defSources[0].getArchiveEndTime();
714 			for (int i = 1; i < defSources.length; i++) {
715 				tEnd = Math.min(tEnd, defSources[i].getArchiveEndTime());
716 			}
717 			if (tEnd <= tStart) {
718 				throw new RrdException("Could not resolve zero ending timestamp.");
719 			}
720 		}
721 	}
722 
723 	// Tricky and ugly. Should be redesigned some time in the future
724 	private void chooseOptimalStep() {
725 		long newStep = Long.MAX_VALUE;
726 		for (Def defSource : defSources) {
727 			long fetchStep = defSource.getFetchStep(), tryStep = fetchStep;
728 			if (step > 0) {
729 				tryStep = Math.min(newStep, (((step - 1) / fetchStep) + 1) * fetchStep);
730 			}
731 			newStep = Math.min(newStep, tryStep);
732 		}
733 		if (newStep != Long.MAX_VALUE) {
734 			// step resolved from a RRD file
735 			step = newStep;
736 		}
737 		else {
738 			// choose step based on the number of pixels (useful for plottable datasources)
739 			step = Math.max((tEnd - tStart) / pixelCount, 1);
740 		}
741 	}
742 
743 	private void createTimestamps() {
744 		long t1 = Util.normalize(tStart, step);
745 		long t2 = Util.normalize(tEnd, step);
746 		if (t2 < tEnd) {
747 			t2 += step;
748 		}
749 		int count = (int) (((t2 - t1) / step) + 1);
750 		timestamps = new long[count];
751 		for (int i = 0; i < count; i++) {
752 			timestamps[i] = t1;
753 			t1 += step;
754 		}
755 	}
756 
757 	private void assignTimestampsToSources() {
758 		for (Source src : sources.values()) {
759 			src.setTimestamps(timestamps);
760 		}
761 	}
762 
763 	private void normalizeRrdValues() throws RrdException {
764 		Normalizer normalizer = new Normalizer(timestamps);
765 		for (Def def : defSources) {
766 			long[] rrdTimestamps = def.getRrdTimestamps();
767 			double[] rrdValues = def.getRrdValues();
768 			double[] values = normalizer.normalize(rrdTimestamps, rrdValues);
769 			def.setValues(values);
770 		}
771 	}
772 
773 	private void calculateNonRrdSources() throws RrdException {
774 		for (Source source : sources.values()) {
775 			if (source instanceof SDef) {
776 				calculateSDef((SDef) source);
777 			}
778 			else if (source instanceof CDef) {
779 				calculateCDef((CDef) source);
780 			}
781 			else if (source instanceof PDef) {
782 				calculatePDef((PDef) source);
783 			}
784 		}
785 	}
786 
787 	private void calculatePDef(PDef pdef) {
788 		pdef.calculateValues();
789 	}
790 
791 	private void calculateCDef(CDef cDef) throws RrdException {
792 		RpnCalculator calc = new RpnCalculator(cDef.getRpnExpression(), cDef.getName(), this);
793 		cDef.setValues(calc.calculateValues());
794 	}
795 
796 	private void calculateSDef(SDef sDef) throws RrdException {
797 		String defName = sDef.getDefName();
798 		String consolFun = sDef.getConsolFun();
799 		Source source = getSource(defName);
800 		double value = source.getAggregates(tStart, tEnd).getAggregate(consolFun);
801 		sDef.setValue(value);
802 	}
803 
804 	private RrdDb getRrd(Def def) throws IOException, RrdException {
805 		String path = def.getPath(), backend = def.getBackend();
806 		if (poolUsed && backend == null) {
807 			return RrdDbPool.getInstance().requestRrdDb(path);
808 		}
809 		else if (backend != null) {
810 			return new RrdDb(path, true, RrdBackendFactory.getFactory(backend));
811 		}
812 		else {
813 			return new RrdDb(path, true);
814 		}
815 	}
816 
817 	private void releaseRrd(RrdDb rrd, Def def) throws IOException, RrdException {
818 		String backend = def.getBackend();
819 		if (poolUsed && backend == null) {
820 			RrdDbPool.getInstance().release(rrd);
821 		}
822 		else {
823 			rrd.close();
824 		}
825 	}
826 
827 	private static String format(String s, int length) {
828 		StringBuffer b = new StringBuffer(s);
829 		for (int i = 0; i < length - s.length(); i++) {
830 			b.append(' ');
831 		}
832 		return b.toString();
833 	}
834 
835 	/**
836 	 * Cute little demo. Uses demo.rrd file previously created by basic JRobin demo.
837 	 *
838 	 * @param args Not used
839 	 * @throws IOException
840 	 * @throws RrdException
841 	 */
842 	public static void main(String[] args) throws IOException, RrdException {
843 		// time span
844 		long t1 = Util.getTimestamp(2003, 4, 1);
845 		long t2 = Util.getTimestamp(2003, 5, 1);
846 		System.out.println("t1 = " + t1);
847 		System.out.println("t2 = " + t2);
848 
849 		// RRD file to use
850 		String rrdPath = Util.getJRobinDemoPath("demo.rrd");
851 
852 		// constructor
853 		DataProcessor dp = new DataProcessor(t1, t2);
854 
855 		// uncomment and run again
856 		//dp.setFetchRequestResolution(86400);
857 
858 		// uncomment and run again
859 		//dp.setStep(86500);
860 
861 		// datasource definitions
862 		dp.addDatasource("X", rrdPath, "sun", "AVERAGE");
863 		dp.addDatasource("Y", rrdPath, "shade", "AVERAGE");
864 		dp.addDatasource("Z", "X,Y,+,2,/");
865 		dp.addDatasource("DERIVE[Z]", "Z,PREV(Z),-,STEP,/");
866 		dp.addDatasource("TREND[Z]", "DERIVE[Z],SIGN");
867 		dp.addDatasource("AVG[Z]", "Z", "AVERAGE");
868 		dp.addDatasource("DELTA", "Z,AVG[Z],-");
869 
870 		// action
871 		long laptime = System.currentTimeMillis();
872 		//dp.setStep(86400);
873 		dp.processData();
874 		System.out.println("Data processed in " + (System.currentTimeMillis() - laptime) + " milliseconds\n---");
875 		System.out.println(dp.dump());
876 
877 		// aggregates
878 		System.out.println("\nAggregates for X");
879 		Aggregates agg = dp.getAggregates("X");
880 		System.out.println(agg.dump());
881 		System.out.println("\nAggregates for Y");
882 		agg = dp.getAggregates("Y");
883 		System.out.println(agg.dump());
884 
885 		// 95-percentile
886 		System.out.println("\n95-percentile for X: " + Util.formatDouble(dp.get95Percentile("X")));
887 		System.out.println("95-percentile for Y: " + Util.formatDouble(dp.get95Percentile("Y")));
888 
889 		// lastArchiveUpdateTime
890 		System.out.println("\nLast archive update time was: " + dp.getLastRrdArchiveUpdateTime());
891 	}
892 }
893