001    /* ============================================================
002     * JRobin : Pure java implementation of RRDTool's functionality
003     * ============================================================
004     *
005     * Project Info:  http://www.jrobin.org
006     * Project Lead:  Sasa Markovic (saxon@jrobin.org);
007     *
008     * (C) Copyright 2003-2005, by Sasa Markovic.
009     *
010     * Developers:    Sasa Markovic (saxon@jrobin.org)
011     *
012     *
013     * This library is free software; you can redistribute it and/or modify it under the terms
014     * of the GNU Lesser General Public License as published by the Free Software Foundation;
015     * either version 2.1 of the License, or (at your option) any later version.
016     *
017     * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
018     * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
019     * See the GNU Lesser General Public License for more details.
020     *
021     * You should have received a copy of the GNU Lesser General Public License along with this
022     * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
023     * Boston, MA 02111-1307, USA.
024     */
025    
026    package org.jrobin.core;
027    
028    import org.jrobin.data.Aggregates;
029    import org.jrobin.data.DataProcessor;
030    
031    import java.io.ByteArrayOutputStream;
032    import java.io.FileOutputStream;
033    import java.io.IOException;
034    import java.io.OutputStream;
035    
036    /**
037     * Class used to represent data fetched from the RRD.
038     * Object of this class is created when the method
039     * {@link FetchRequest#fetchData() fetchData()} is
040     * called on a {@link FetchRequest FetchRequest} object.<p>
041     * <p/>
042     * Data returned from the RRD is, simply, just one big table filled with
043     * timestamps and corresponding datasource values.
044     * Use {@link #getRowCount() getRowCount()} method to count the number
045     * of returned timestamps (table rows).<p>
046     * <p/>
047     * The first table column is filled with timestamps. Time intervals
048     * between consecutive timestamps are guaranteed to be equal. Use
049     * {@link #getTimestamps() getTimestamps()} method to get an array of
050     * timestamps returned.<p>
051     * <p/>
052     * Remaining columns are filled with datasource values for the whole timestamp range,
053     * on a column-per-datasource basis. Use {@link #getColumnCount() getColumnCount()} to find
054     * the number of datasources and {@link #getValues(int) getValues(i)} method to obtain
055     * all values for the i-th datasource. Returned datasource values correspond to
056     * the values returned with {@link #getTimestamps() getTimestamps()} method.<p>
057     */
058    public class FetchData implements ConsolFuns {
059            // anything fuuny will do
060            private static final String RPN_SOURCE_NAME = "WHERE THE SPEECHLES UNITE IN A SILENT ACCORD";
061    
062            private FetchRequest request;
063            private String[] dsNames;
064            private long[] timestamps;
065            private double[][] values;
066    
067            private Archive matchingArchive;
068            private long arcStep;
069            private long arcEndTime;
070    
071            FetchData(Archive matchingArchive, FetchRequest request) throws IOException {
072                    this.matchingArchive = matchingArchive;
073                    this.arcStep = matchingArchive.getArcStep();
074                    this.arcEndTime = matchingArchive.getEndTime();
075                    this.dsNames = request.getFilter();
076                    if (this.dsNames == null) {
077                            this.dsNames = matchingArchive.getParentDb().getDsNames();
078                    }
079                    this.request = request;
080            }
081    
082            void setTimestamps(long[] timestamps) {
083                    this.timestamps = timestamps;
084            }
085    
086            void setValues(double[][] values) {
087                    this.values = values;
088            }
089    
090            /**
091             * Returns the number of rows fetched from the corresponding RRD.
092             * Each row represents datasource values for the specific timestamp.
093             *
094             * @return Number of rows.
095             */
096            public int getRowCount() {
097                    return timestamps.length;
098            }
099    
100            /**
101             * Returns the number of columns fetched from the corresponding RRD.
102             * This number is always equal to the number of datasources defined
103             * in the RRD. Each column represents values of a single datasource.
104             *
105             * @return Number of columns (datasources).
106             */
107            public int getColumnCount() {
108                    return dsNames.length;
109            }
110    
111            /**
112             * Returns an array of timestamps covering the whole range specified in the
113             * {@link FetchRequest FetchReguest} object.
114             *
115             * @return Array of equidistant timestamps.
116             */
117            public long[] getTimestamps() {
118                    return timestamps;
119            }
120    
121            /**
122             * Returns the step with which this data was fetched.
123             *
124             * @return Step as long.
125             */
126            public long getStep() {
127                    return timestamps[1] - timestamps[0];
128            }
129    
130            /**
131             * Returns all archived values for a single datasource.
132             * Returned values correspond to timestamps
133             * returned with {@link #getTimestamps() getTimestamps()} method.
134             *
135             * @param dsIndex Datasource index.
136             * @return Array of single datasource values.
137             */
138            public double[] getValues(int dsIndex) {
139                    return values[dsIndex];
140            }
141    
142            /**
143             * Returns all archived values for all datasources.
144             * Returned values correspond to timestamps
145             * returned with {@link #getTimestamps() getTimestamps()} method.
146             *
147             * @return Two-dimensional aray of all datasource values.
148             */
149            public double[][] getValues() {
150                    return values;
151            }
152    
153            /**
154             * Returns all archived values for a single datasource.
155             * Returned values correspond to timestamps
156             * returned with {@link #getTimestamps() getTimestamps()} method.
157             *
158             * @param dsName Datasource name.
159             * @return Array of single datasource values.
160             * @throws RrdException Thrown if no matching datasource name is found.
161             */
162            public double[] getValues(String dsName) throws RrdException {
163                    for (int dsIndex = 0; dsIndex < getColumnCount(); dsIndex++) {
164                            if (dsName.equals(dsNames[dsIndex])) {
165                                    return getValues(dsIndex);
166                            }
167                    }
168                    throw new RrdException("Datasource [" + dsName + "] not found");
169            }
170    
171            /**
172             * Returns a set of values created by applying RPN expression to the fetched data.
173             * For example, if you have two datasources named <code>x</code> and <code>y</code>
174             * in this FetchData and you want to calculate values for <code>(x+y)/2<code> use something like: <p>
175             * <code>getRpnValues("x,y,+,2,/");</code><p>
176             *
177             * @param rpnExpression RRDTool-like RPN expression
178             * @return Calculated values
179             * @throws RrdException Thrown if invalid RPN expression is supplied
180             */
181            public double[] getRpnValues(String rpnExpression) throws RrdException {
182                    DataProcessor dataProcessor = createDataProcessor(rpnExpression);
183                    return dataProcessor.getValues(RPN_SOURCE_NAME);
184            }
185    
186            /**
187             * Returns {@link FetchRequest FetchRequest} object used to create this FetchData object.
188             *
189             * @return Fetch request object.
190             */
191            public FetchRequest getRequest() {
192                    return request;
193            }
194    
195            /**
196             * Returns the first timestamp in this FetchData object.
197             *
198             * @return The smallest timestamp.
199             */
200            public long getFirstTimestamp() {
201                    return timestamps[0];
202            }
203    
204            /**
205             * Returns the last timestamp in this FecthData object.
206             *
207             * @return The biggest timestamp.
208             */
209            public long getLastTimestamp() {
210                    return timestamps[timestamps.length - 1];
211            }
212    
213            /**
214             * Returns Archive object which is determined to be the best match for the
215             * timestamps specified in the fetch request. All datasource values are obtained
216             * from round robin archives belonging to this archive.
217             *
218             * @return Matching archive.
219             */
220            public Archive getMatchingArchive() {
221                    return matchingArchive;
222            }
223    
224            /**
225             * Returns array of datasource names found in the corresponding RRD. If the request
226             * was filtered (data was fetched only for selected datasources), only datasources selected
227             * for fetching are returned.
228             *
229             * @return Array of datasource names.
230             */
231            public String[] getDsNames() {
232                    return dsNames;
233            }
234    
235            /**
236             * Retrieve the table index number of a datasource by name.  Names are case sensitive.
237             *
238             * @param dsName Name of the datasource for which to find the index.
239             * @return Index number of the datasources in the value table.
240             */
241            public int getDsIndex(String dsName) {
242                    // Let's assume the table of dsNames is always small, so it is not necessary to use a hashmap for lookups
243                    for (int i = 0; i < dsNames.length; i++) {
244                            if (dsNames[i].equals(dsName)) {
245                                    return i;
246                            }
247                    }
248                    return -1;              // Datasource not found !
249            }
250    
251            /**
252             * Dumps the content of the whole FetchData object. Useful for debugging.
253             */
254            public String dump() {
255                    StringBuffer buffer = new StringBuffer("");
256                    for (int row = 0; row < getRowCount(); row++) {
257                            buffer.append(timestamps[row]);
258                            buffer.append(":  ");
259                            for (int dsIndex = 0; dsIndex < getColumnCount(); dsIndex++) {
260                                    buffer.append(Util.formatDouble(values[dsIndex][row], true));
261                                    buffer.append("  ");
262                            }
263                            buffer.append("\n");
264                    }
265                    return buffer.toString();
266            }
267    
268            /**
269             * Returns string representing fetched data in a RRDTool-like form.
270             *
271             * @return Fetched data as a string in a rrdfetch-like output form.
272             */
273            public String toString() {
274                    // print header row
275                    StringBuffer buff = new StringBuffer();
276                    buff.append(padWithBlanks("", 10));
277                    buff.append(" ");
278                    for (String dsName : dsNames) {
279                            buff.append(padWithBlanks(dsName, 18));
280                    }
281                    buff.append("\n \n");
282                    for (int i = 0; i < timestamps.length; i++) {
283                            buff.append(padWithBlanks("" + timestamps[i], 10));
284                            buff.append(":");
285                            for (int j = 0; j < dsNames.length; j++) {
286                                    double value = values[j][i];
287                                    String valueStr = Double.isNaN(value) ? "nan" : Util.formatDouble(value);
288                                    buff.append(padWithBlanks(valueStr, 18));
289                            }
290                            buff.append("\n");
291                    }
292                    return buff.toString();
293            }
294    
295            private static String padWithBlanks(String input, int width) {
296                    StringBuffer buff = new StringBuffer("");
297                    int diff = width - input.length();
298                    while (diff-- > 0) {
299                            buff.append(' ');
300                    }
301                    buff.append(input);
302                    return buff.toString();
303            }
304    
305            /**
306             * Returns single aggregated value from the fetched data for a single datasource.
307             *
308             * @param dsName        Datasource name
309             * @param consolFun Consolidation function to be applied to fetched datasource values.
310             *                  Valid consolidation functions are "MIN", "MAX", "LAST", "FIRST", "AVERAGE" and "TOTAL"
311             *                  (these string constants are conveniently defined in the {@link ConsolFuns} class)
312             * @return MIN, MAX, LAST, FIRST, AVERAGE or TOTAL value calculated from the fetched data
313             *         for the given datasource name
314             * @throws RrdException Thrown if the given datasource name cannot be found in fetched data.
315             */
316            public double getAggregate(String dsName, String consolFun) throws RrdException {
317                    DataProcessor dp = createDataProcessor(null);
318                    return dp.getAggregate(dsName, consolFun);
319            }
320    
321            /**
322             * Returns aggregated value from the fetched data for a single datasource.
323             * Before applying aggregation functions, specified RPN expression is applied to fetched data.
324             * For example, if you have a gauge datasource named 'foots' but you want to find the maximum
325             * fetched value in meters use something like: <p>
326             * <code>getAggregate("foots", "MAX", "foots,0.3048,*");</code><p>
327             *
328             * @param dsName                Datasource name
329             * @param consolFun      Consolidation function (MIN, MAX, LAST, FIRST, AVERAGE or TOTAL)
330             * @param rpnExpression RRDTool-like RPN expression
331             * @return Aggregated value
332             * @throws RrdException Thrown if the given datasource name cannot be found in fetched data, or if
333             *                      invalid RPN expression is supplied
334             * @throws IOException  Thrown in case of I/O error (unlikely to happen)
335             * @deprecated This method is preserved just for backward compatibility.
336             */
337            public double getAggregate(String dsName, String consolFun, String rpnExpression)
338                            throws RrdException, IOException {
339                    // for backward compatibility
340                    rpnExpression = rpnExpression.replaceAll("value", dsName);
341                    return getRpnAggregate(rpnExpression, consolFun);
342            }
343    
344            /**
345             * Returns aggregated value for a set of values calculated by applying an RPN expression to the
346             * fetched data. For example, if you have two datasources named <code>x</code> and <code>y</code>
347             * in this FetchData and you want to calculate MAX value of <code>(x+y)/2<code> use something like: <p>
348             * <code>getRpnAggregate("x,y,+,2,/", "MAX");</code><p>
349             *
350             * @param rpnExpression RRDTool-like RPN expression
351             * @param consolFun      Consolidation function (MIN, MAX, LAST, FIRST, AVERAGE or TOTAL)
352             * @return Aggregated value
353             * @throws RrdException Thrown if invalid RPN expression is supplied
354             */
355            public double getRpnAggregate(String rpnExpression, String consolFun) throws RrdException {
356                    DataProcessor dataProcessor = createDataProcessor(rpnExpression);
357                    return dataProcessor.getAggregate(RPN_SOURCE_NAME, consolFun);
358            }
359    
360            /**
361             * Returns all aggregated values (MIN, MAX, LAST, FIRST, AVERAGE or TOTAL) calculated from the fetched data
362             * for a single datasource.
363             *
364             * @param dsName Datasource name.
365             * @return Simple object containing all aggregated values.
366             * @throws RrdException Thrown if the given datasource name cannot be found in the fetched data.
367             */
368            public Aggregates getAggregates(String dsName) throws RrdException {
369                    DataProcessor dataProcessor = createDataProcessor(null);
370                    return dataProcessor.getAggregates(dsName);
371            }
372    
373            /**
374             * Returns all aggregated values for a set of values calculated by applying an RPN expression to the
375             * fetched data. For example, if you have two datasources named <code>x</code> and <code>y</code>
376             * in this FetchData and you want to calculate MIN, MAX, LAST, FIRST, AVERAGE and TOTAL value
377             * of <code>(x+y)/2<code> use something like: <p>
378             * <code>getRpnAggregates("x,y,+,2,/");</code><p>
379             *
380             * @param rpnExpression RRDTool-like RPN expression
381             * @return Object containing all aggregated values
382             * @throws RrdException Thrown if invalid RPN expression is supplied
383             */
384            public Aggregates getRpnAggregates(String rpnExpression) throws RrdException, IOException {
385                    DataProcessor dataProcessor = createDataProcessor(rpnExpression);
386                    return dataProcessor.getAggregates(RPN_SOURCE_NAME);
387            }
388    
389            /**
390             * Used by ISPs which charge for bandwidth utilization on a "95th percentile" basis.<p>
391             * <p/>
392             * The 95th percentile is the highest source value left when the top 5% of a numerically sorted set
393             * of source data is discarded. It is used as a measure of the peak value used when one discounts
394             * a fair amount for transitory spikes. This makes it markedly different from the average.<p>
395             * <p/>
396             * Read more about this topic at:<p>
397             * <a href="http://www.red.net/support/resourcecentre/leasedline/percentile.php">Rednet</a> or<br>
398             * <a href="http://www.bytemark.co.uk/support/tech/95thpercentile.html">Bytemark</a>.
399             *
400             * @param dsName Datasource name
401             * @return 95th percentile of fetched source values
402             * @throws RrdException Thrown if invalid source name is supplied
403             */
404            public double get95Percentile(String dsName) throws RrdException {
405                    DataProcessor dataProcessor = createDataProcessor(null);
406                    return dataProcessor.get95Percentile(dsName);
407            }
408    
409            /**
410             * Same as {@link #get95Percentile(String)}, but for a set of values calculated with the given
411             * RPN expression.
412             *
413             * @param rpnExpression RRDTool-like RPN expression
414             * @return 95-percentile
415             * @throws RrdException Thrown if invalid RPN expression is supplied
416             */
417            public double getRpn95Percentile(String rpnExpression) throws RrdException {
418                    DataProcessor dataProcessor = createDataProcessor(rpnExpression);
419                    return dataProcessor.get95Percentile(RPN_SOURCE_NAME);
420            }
421    
422            /**
423             * Dumps fetch data to output stream in XML format.
424             *
425             * @param outputStream Output stream to dump fetch data to
426             * @throws IOException Thrown in case of I/O error
427             */
428            public void exportXml(OutputStream outputStream) throws IOException {
429                    XmlWriter writer = new XmlWriter(outputStream);
430                    writer.startTag("fetch_data");
431                    writer.startTag("request");
432                    writer.writeTag("file", request.getParentDb().getPath());
433                    writer.writeComment(Util.getDate(request.getFetchStart()));
434                    writer.writeTag("start", request.getFetchStart());
435                    writer.writeComment(Util.getDate(request.getFetchEnd()));
436                    writer.writeTag("end", request.getFetchEnd());
437                    writer.writeTag("resolution", request.getResolution());
438                    writer.writeTag("cf", request.getConsolFun());
439                    writer.closeTag(); // request
440                    writer.startTag("datasources");
441                    for (String dsName : dsNames) {
442                            writer.writeTag("name", dsName);
443                    }
444                    writer.closeTag(); // datasources
445                    writer.startTag("data");
446                    for (int i = 0; i < timestamps.length; i++) {
447                            writer.startTag("row");
448                            writer.writeComment(Util.getDate(timestamps[i]));
449                            writer.writeTag("timestamp", timestamps[i]);
450                            writer.startTag("values");
451                            for (int j = 0; j < dsNames.length; j++) {
452                                    writer.writeTag("v", values[j][i]);
453                            }
454                            writer.closeTag(); // values
455                            writer.closeTag(); // row
456                    }
457                    writer.closeTag(); // data
458                    writer.closeTag(); // fetch_data
459                    writer.flush();
460            }
461    
462            /**
463             * Dumps fetch data to file in XML format.
464             *
465             * @param filepath Path to destination file
466             * @throws IOException Thrown in case of I/O error
467             */
468            public void exportXml(String filepath) throws IOException {
469                    OutputStream outputStream = null;
470                    try {
471                            outputStream = new FileOutputStream(filepath);
472                            exportXml(outputStream);
473                    }
474                    finally {
475                            if (outputStream != null) {
476                                    outputStream.close();
477                            }
478                    }
479            }
480    
481            /**
482             * Dumps fetch data in XML format.
483             *
484             * @return String containing XML formatted fetch data
485             * @throws IOException Thrown in case of I/O error
486             */
487            public String exportXml() throws IOException {
488                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
489                    exportXml(outputStream);
490                    return outputStream.toString();
491            }
492    
493            /**
494             * Returns the step of the corresponding RRA archive
495             *
496             * @return Archive step in seconds
497             */
498            public long getArcStep() {
499                    return arcStep;
500            }
501    
502            /**
503             * Returns the timestamp of the last populated slot in the corresponding RRA archive
504             *
505             * @return Timestamp in seconds
506             */
507            public long getArcEndTime() {
508                    return arcEndTime;
509            }
510    
511            private DataProcessor createDataProcessor(String rpnExpression) throws RrdException {
512                    DataProcessor dataProcessor = new DataProcessor(request.getFetchStart(), request.getFetchEnd());
513                    for (String dsName : dsNames) {
514                            dataProcessor.addDatasource(dsName, this);
515                    }
516                    if (rpnExpression != null) {
517                            dataProcessor.addDatasource(RPN_SOURCE_NAME, rpnExpression);
518                            try {
519                                    dataProcessor.processData();
520                            }
521                            catch (IOException ioe) {
522                                    // highly unlikely, since all datasources have already calculated values
523                                    throw new RuntimeException("Impossible error: " + ioe);
524                            }
525                    }
526                    return dataProcessor;
527            }
528    }