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 trueif 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 trueif 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, longlonglong) 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, longlonglong) 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
894