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 java.io.ByteArrayOutputStream;
029    import java.io.FileOutputStream;
030    import java.io.IOException;
031    import java.io.OutputStream;
032    import java.util.*;
033    
034    /**
035     * <p>Class to represent definition of new Round Robin Database (RRD).
036     * Object of this class is used to create
037     * new RRD from scratch - pass its reference as a <code>RrdDb</code> constructor
038     * argument (see documentation for {@link RrdDb RrdDb} class). <code>RrdDef</code>
039     * object <b>does not</b> actually create new RRD. It just holds all necessary
040     * information which will be used during the actual creation process</p>
041     * <p/>
042     * <p>RRD definition (RrdDef object) consists of the following elements:</p>
043     * <p/>
044     * <ul>
045     * <li> path to RRD that will be created
046     * <li> starting timestamp
047     * <li> step
048     * <li> one or more datasource definitions
049     * <li> one or more archive definitions
050     * </ul>
051     * <p>RrdDef provides API to set all these elements. For the complete explanation of all
052     * RRD definition parameters, see RRDTool's
053     * <a href="../../../../man/rrdcreate.html" target="man">rrdcreate man page</a>.</p>
054     *
055     * @author <a href="mailto:saxon@jrobin.org">Sasa Markovic</a>
056     */
057    public class RrdDef {
058            /**
059             * default RRD step to be used if not specified in constructor (300 seconds)
060             */
061            public static final long DEFAULT_STEP = 300L;
062            /**
063             * if not specified in constructor, starting timestamp will be set to the
064             * current timestamp plus DEFAULT_INITIAL_SHIFT seconds (-10)
065             */
066            public static final long DEFAULT_INITIAL_SHIFT = -10L;
067    
068            private String path;
069            private long startTime = Util.getTime() + DEFAULT_INITIAL_SHIFT;
070            private long step = DEFAULT_STEP;
071            private ArrayList<DsDef> dsDefs = new ArrayList<DsDef>();
072            private ArrayList<ArcDef> arcDefs = new ArrayList<ArcDef>();
073    
074            /**
075             * <p>Creates new RRD definition object with the given path.
076             * When this object is passed to
077             * <code>RrdDb</code> constructor, new RRD will be created using the
078             * specified path. </p>
079             *
080             * @param path Path to new RRD.
081             * @throws RrdException Thrown if name is invalid (null or empty).
082             */
083            public RrdDef(String path) throws RrdException {
084                    if (path == null || path.length() == 0) {
085                            throw new RrdException("No path specified");
086                    }
087                    this.path = path;
088            }
089    
090            /**
091             * <p>Creates new RRD definition object with the given path and step.</p>
092             *
093             * @param path Path to new RRD.
094             * @param step RRD step.
095             * @throws RrdException Thrown if supplied parameters are invalid.
096             */
097            public RrdDef(String path, long step) throws RrdException {
098                    this(path);
099                    if (step <= 0) {
100                            throw new RrdException("Invalid RRD step specified: " + step);
101                    }
102                    this.step = step;
103            }
104    
105            /**
106             * <p>Creates new RRD definition object with the given path, starting timestamp
107             * and step.</p>
108             *
109             * @param path    Path to new RRD.
110             * @param startTime RRD starting timestamp.
111             * @param step    RRD step.
112             * @throws RrdException Thrown if supplied parameters are invalid.
113             */
114            public RrdDef(String path, long startTime, long step) throws RrdException {
115                    this(path, step);
116                    if (startTime < 0) {
117                            throw new RrdException("Invalid RRD start time specified: " + startTime);
118                    }
119                    this.startTime = startTime;
120            }
121    
122            /**
123             * Returns path for the new RRD
124             *
125             * @return path to the new RRD which should be created
126             */
127            public String getPath() {
128                    return path;
129            }
130    
131            /**
132             * Returns starting timestamp for the RRD that should be created.
133             *
134             * @return RRD starting timestamp
135             */
136            public long getStartTime() {
137                    return startTime;
138            }
139    
140            /**
141             * Returns time step for the RRD that will be created.
142             *
143             * @return RRD step
144             */
145            public long getStep() {
146                    return step;
147            }
148    
149            /**
150             * Sets path to RRD.
151             *
152             * @param path to new RRD.
153             */
154            public void setPath(String path) {
155                    this.path = path;
156            }
157    
158            /**
159             * Sets RRD's starting timestamp.
160             *
161             * @param startTime starting timestamp.
162             */
163            public void setStartTime(long startTime) {
164                    this.startTime = startTime;
165            }
166    
167            /**
168             * Sets RRD's starting timestamp.
169             *
170             * @param date starting date
171             */
172            public void setStartTime(Date date) {
173                    this.startTime = Util.getTimestamp(date);
174            }
175    
176            /**
177             * Sets RRD's starting timestamp.
178             *
179             * @param gc starting date
180             */
181            public void setStartTime(Calendar gc) {
182                    this.startTime = Util.getTimestamp(gc);
183            }
184    
185            /**
186             * Sets RRD's time step.
187             *
188             * @param step RRD time step.
189             */
190            public void setStep(long step) {
191                    this.step = step;
192            }
193    
194            /**
195             * Adds single datasource definition represented with object of class <code>DsDef</code>.
196             *
197             * @param dsDef Datasource definition.
198             * @throws RrdException Thrown if new datasource definition uses already used data
199             *                      source name.
200             */
201            public void addDatasource(DsDef dsDef) throws RrdException {
202                    if (dsDefs.contains(dsDef)) {
203                            throw new RrdException("Datasource already defined: " + dsDef.dump());
204                    }
205                    dsDefs.add(dsDef);
206            }
207    
208            /**
209             * <p>Adds single datasource to RRD definition by specifying its data source name, source type,
210             * heartbeat, minimal and maximal value. For the complete explanation of all data
211             * source definition parameters see RRDTool's
212             * <a href="../../../../man/rrdcreate.html" target="man">rrdcreate man page</a>.</p>
213             * <p/>
214             * <p><b>IMPORTANT NOTE:</b> If datasource name ends with '!', corresponding archives will never
215             * store NaNs as datasource values. In that case, NaN datasource values will be silently
216             * replaced with zeros by the framework.</p>
217             *
218             * @param dsName        Data source name.
219             * @param dsType        Data source type. Valid types are "COUNTER",
220             *                  "GAUGE", "DERIVE" and "ABSOLUTE" (these string constants are conveniently defined in
221             *                  the {@link DsTypes} class).
222             * @param heartbeat Data source heartbeat.
223             * @param minValue  Minimal acceptable value. Use <code>Double.NaN</code> if unknown.
224             * @param maxValue  Maximal acceptable value. Use <code>Double.NaN</code> if unknown.
225             * @throws RrdException Thrown if new datasource definition uses already used data
226             *                      source name.
227             */
228            public void addDatasource(String dsName, String dsType, long heartbeat,
229                                                              double minValue, double maxValue) throws RrdException {
230                    addDatasource(new DsDef(dsName, dsType, heartbeat, minValue, maxValue));
231            }
232    
233            /**
234             * Adds single datasource to RRD definition from a RRDTool-like
235             * datasource definition string. The string must have six elements separated with colons
236             * (:) in the following order:<p>
237             * <pre>
238             * DS:name:type:heartbeat:minValue:maxValue
239             * </pre>
240             * For example:</p>
241             * <pre>
242             * DS:input:COUNTER:600:0:U
243             * </pre>
244             * For more information on datasource definition parameters see <code>rrdcreate</code>
245             * man page.<p>
246             *
247             * @param rrdToolDsDef Datasource definition string with the syntax borrowed from RRDTool.
248             * @throws RrdException Thrown if invalid string is supplied.
249             */
250            public void addDatasource(String rrdToolDsDef) throws RrdException {
251                    RrdException rrdException = new RrdException(
252                                    "Wrong rrdtool-like datasource definition: " + rrdToolDsDef);
253                    StringTokenizer tokenizer = new StringTokenizer(rrdToolDsDef, ":");
254                    if (tokenizer.countTokens() != 6) {
255                            throw rrdException;
256                    }
257                    String[] tokens = new String[6];
258                    for (int curTok = 0; tokenizer.hasMoreTokens(); curTok++) {
259                            tokens[curTok] = tokenizer.nextToken();
260                    }
261                    if (!tokens[0].equalsIgnoreCase("DS")) {
262                            throw rrdException;
263                    }
264                    String dsName = tokens[1];
265                    String dsType = tokens[2];
266                    long dsHeartbeat;
267                    try {
268                            dsHeartbeat = Long.parseLong(tokens[3]);
269                    }
270                    catch (NumberFormatException nfe) {
271                            throw rrdException;
272                    }
273                    double minValue = Double.NaN;
274                    if (!tokens[4].equalsIgnoreCase("U")) {
275                            try {
276                                    minValue = Double.parseDouble(tokens[4]);
277                            }
278                            catch (NumberFormatException nfe) {
279                                    throw rrdException;
280                            }
281                    }
282                    double maxValue = Double.NaN;
283                    if (!tokens[5].equalsIgnoreCase("U")) {
284                            try {
285                                    maxValue = Double.parseDouble(tokens[5]);
286                            }
287                            catch (NumberFormatException nfe) {
288                                    throw rrdException;
289                            }
290                    }
291                    addDatasource(new DsDef(dsName, dsType, dsHeartbeat, minValue, maxValue));
292            }
293    
294            /**
295             * Adds data source definitions to RRD definition in bulk.
296             *
297             * @param dsDefs Array of data source definition objects.
298             * @throws RrdException Thrown if duplicate data source name is used.
299             */
300            public void addDatasource(DsDef[] dsDefs) throws RrdException {
301                    for (DsDef dsDef : dsDefs) {
302                            addDatasource(dsDef);
303                    }
304            }
305    
306            /**
307             * Adds single archive definition represented with object of class <code>ArcDef</code>.
308             *
309             * @param arcDef Archive definition.
310             * @throws RrdException Thrown if archive with the same consolidation function
311             *                      and the same number of steps is already added.
312             */
313            public void addArchive(ArcDef arcDef) throws RrdException {
314                    if (arcDefs.contains(arcDef)) {
315                            throw new RrdException("Archive already defined: " + arcDef.dump());
316                    }
317                    arcDefs.add(arcDef);
318            }
319    
320            /**
321             * Adds archive definitions to RRD definition in bulk.
322             *
323             * @param arcDefs Array of archive definition objects
324             * @throws RrdException Thrown if RRD definition already contains archive with
325             *                      the same consolidation function and the same number of steps.
326             */
327            public void addArchive(ArcDef[] arcDefs) throws RrdException {
328                    for (ArcDef arcDef : arcDefs) {
329                            addArchive(arcDef);
330                    }
331            }
332    
333            /**
334             * Adds single archive definition by specifying its consolidation function, X-files factor,
335             * number of steps and rows. For the complete explanation of all archive
336             * definition parameters see RRDTool's
337             * <a href="../../../../man/rrdcreate.html" target="man">rrdcreate man page</a>.</p>
338             *
339             * @param consolFun Consolidation function. Valid values are "AVERAGE",
340             *                  "MIN", "MAX" and "LAST" (these constants are conveniently defined in the
341             *                  {@link ConsolFuns} class)
342             * @param xff      X-files factor. Valid values are between 0 and 1.
343             * @param steps  Number of archive steps
344             * @param rows    Number of archive rows
345             * @throws RrdException Thrown if archive with the same consolidation function
346             *                      and the same number of steps is already added.
347             */
348            public void addArchive(String consolFun, double xff, int steps, int rows)
349                            throws RrdException {
350                    addArchive(new ArcDef(consolFun, xff, steps, rows));
351            }
352    
353            /**
354             * Adds single archive to RRD definition from a RRDTool-like
355             * archive definition string. The string must have five elements separated with colons
356             * (:) in the following order:<p>
357             * <pre>
358             * RRA:consolidationFunction:XFilesFactor:steps:rows
359             * </pre>
360             * For example:</p>
361             * <pre>
362             * RRA:AVERAGE:0.5:10:1000
363             * </pre>
364             * For more information on archive definition parameters see <code>rrdcreate</code>
365             * man page.<p>
366             *
367             * @param rrdToolArcDef Archive definition string with the syntax borrowed from RRDTool.
368             * @throws RrdException Thrown if invalid string is supplied.
369             */
370            public void addArchive(String rrdToolArcDef) throws RrdException {
371                    RrdException rrdException = new RrdException(
372                                    "Wrong rrdtool-like archive definition: " + rrdToolArcDef);
373                    StringTokenizer tokenizer = new StringTokenizer(rrdToolArcDef, ":");
374                    if (tokenizer.countTokens() != 5) {
375                            throw rrdException;
376                    }
377                    String[] tokens = new String[5];
378                    for (int curTok = 0; tokenizer.hasMoreTokens(); curTok++) {
379                            tokens[curTok] = tokenizer.nextToken();
380                    }
381                    if (!tokens[0].equalsIgnoreCase("RRA")) {
382                            throw rrdException;
383                    }
384                    String consolFun = tokens[1];
385                    double xff;
386                    try {
387                            xff = Double.parseDouble(tokens[2]);
388                    }
389                    catch (NumberFormatException nfe) {
390                            throw rrdException;
391                    }
392                    int steps;
393                    try {
394                            steps = Integer.parseInt(tokens[3]);
395                    }
396                    catch (NumberFormatException nfe) {
397                            throw rrdException;
398                    }
399                    int rows;
400                    try {
401                            rows = Integer.parseInt(tokens[4]);
402                    }
403                    catch (NumberFormatException nfe) {
404                            throw rrdException;
405                    }
406                    addArchive(new ArcDef(consolFun, xff, steps, rows));
407            }
408    
409            void validate() throws RrdException {
410                    if (dsDefs.size() == 0) {
411                            throw new RrdException("No RRD datasource specified. At least one is needed.");
412                    }
413                    if (arcDefs.size() == 0) {
414                            throw new RrdException("No RRD archive specified. At least one is needed.");
415                    }
416            }
417    
418            /**
419             * Returns all data source definition objects specified so far.
420             *
421             * @return Array of data source definition objects
422             */
423            public DsDef[] getDsDefs() {
424                    return dsDefs.toArray(new DsDef[0]);
425            }
426    
427            /**
428             * Returns all archive definition objects specified so far.
429             *
430             * @return Array of archive definition objects.
431             */
432            public ArcDef[] getArcDefs() {
433                    return arcDefs.toArray(new ArcDef[0]);
434            }
435    
436            /**
437             * Returns number of defined datasources.
438             *
439             * @return Number of defined datasources.
440             */
441            public int getDsCount() {
442                    return dsDefs.size();
443            }
444    
445            /**
446             * Returns number of defined archives.
447             *
448             * @return Number of defined archives.
449             */
450            public int getArcCount() {
451                    return arcDefs.size();
452            }
453    
454            /**
455             * Returns string that represents all specified RRD creation parameters. Returned string
456             * has the syntax of RRDTool's <code>create</code> command.
457             *
458             * @return Dumped content of <code>RrdDb</code> object.
459             */
460            public String dump() {
461                    StringBuffer buffer = new StringBuffer("create \"");
462                    buffer.append(path).append("\"");
463                    buffer.append(" --start ").append(getStartTime());
464                    buffer.append(" --step ").append(getStep()).append(" ");
465                    for (DsDef dsDef : dsDefs) {
466                            buffer.append(dsDef.dump()).append(" ");
467                    }
468                    for (ArcDef arcDef : arcDefs) {
469                            buffer.append(arcDef.dump()).append(" ");
470                    }
471                    return buffer.toString().trim();
472            }
473    
474            String getRrdToolCommand() {
475                    return dump();
476            }
477    
478            void removeDatasource(String dsName) throws RrdException {
479                    for (int i = 0; i < dsDefs.size(); i++) {
480                            DsDef dsDef = dsDefs.get(i);
481                            if (dsDef.getDsName().equals(dsName)) {
482                                    dsDefs.remove(i);
483                                    return;
484                            }
485                    }
486                    throw new RrdException("Could not find datasource named '" + dsName + "'");
487            }
488    
489            void saveSingleDatasource(String dsName) {
490                    Iterator<DsDef> it = dsDefs.iterator();
491                    while (it.hasNext()) {
492                            DsDef dsDef = it.next();
493                            if (!dsDef.getDsName().equals(dsName)) {
494                                    it.remove();
495                            }
496                    }
497            }
498    
499            void removeArchive(String consolFun, int steps) throws RrdException {
500                    ArcDef arcDef = findArchive(consolFun, steps);
501                    if (!arcDefs.remove(arcDef)) {
502                            throw new RrdException("Could not remove archive " + consolFun + "/" + steps);
503                    }
504            }
505    
506            ArcDef findArchive(String consolFun, int steps) throws RrdException {
507                    for (ArcDef arcDef : arcDefs) {
508                            if (arcDef.getConsolFun().equals(consolFun) && arcDef.getSteps() == steps) {
509                                    return arcDef;
510                            }
511                    }
512                    throw new RrdException("Could not find archive " + consolFun + "/" + steps);
513            }
514    
515            /**
516             * Exports RrdDef object to output stream in XML format. Generated XML code can be parsed
517             * with {@link RrdDefTemplate} class.
518             *
519             * @param out Output stream
520             */
521            public void exportXmlTemplate(OutputStream out) {
522                    XmlWriter xml = new XmlWriter(out);
523                    xml.startTag("rrd_def");
524                    xml.writeTag("path", getPath());
525                    xml.writeTag("step", getStep());
526                    xml.writeTag("start", getStartTime());
527                    // datasources
528                    DsDef[] dsDefs = getDsDefs();
529                    for (DsDef dsDef : dsDefs) {
530                            xml.startTag("datasource");
531                            xml.writeTag("name", dsDef.getDsName());
532                            xml.writeTag("type", dsDef.getDsType());
533                            xml.writeTag("heartbeat", dsDef.getHeartbeat());
534                            xml.writeTag("min", dsDef.getMinValue(), "U");
535                            xml.writeTag("max", dsDef.getMaxValue(), "U");
536                            xml.closeTag(); // datasource
537                    }
538                    ArcDef[] arcDefs = getArcDefs();
539                    for (ArcDef arcDef : arcDefs) {
540                            xml.startTag("archive");
541                            xml.writeTag("cf", arcDef.getConsolFun());
542                            xml.writeTag("xff", arcDef.getXff());
543                            xml.writeTag("steps", arcDef.getSteps());
544                            xml.writeTag("rows", arcDef.getRows());
545                            xml.closeTag(); // archive
546                    }
547                    xml.closeTag(); // rrd_def
548                    xml.flush();
549            }
550    
551            /**
552             * Exports RrdDef object to string in XML format. Generated XML string can be parsed
553             * with {@link RrdDefTemplate} class.
554             *
555             * @return XML formatted string representing this RrdDef object
556             */
557            public String exportXmlTemplate() {
558                    ByteArrayOutputStream out = new ByteArrayOutputStream();
559                    exportXmlTemplate(out);
560                    return out.toString();
561            }
562    
563            /**
564             * Exports RrdDef object to a file in XML format. Generated XML code can be parsed
565             * with {@link RrdDefTemplate} class.
566             *
567             * @param filePath Path to the file
568             */
569            public void exportXmlTemplate(String filePath) throws IOException {
570                    FileOutputStream out = new FileOutputStream(filePath, false);
571                    exportXmlTemplate(out);
572                    out.close();
573            }
574    
575            /**
576             * Returns the number of storage bytes required to create RRD from this
577             * RrdDef object.
578             *
579             * @return Estimated byte count of the underlying RRD storage.
580             */
581            public long getEstimatedSize() {
582                    int dsCount = dsDefs.size();
583                    int arcCount = arcDefs.size();
584                    int rowsCount = 0;
585                    for (ArcDef arcDef : arcDefs) {
586                            rowsCount += arcDef.getRows();
587                    }
588                    return calculateSize(dsCount, arcCount, rowsCount);
589            }
590    
591            static long calculateSize(int dsCount, int arcCount, int rowsCount) {
592                    // return 64L + 128L * dsCount + 56L * arcCount +
593                    //      20L * dsCount * arcCount + 8L * dsCount * rowsCount;
594                    return (24L + 48L * dsCount + 16L * arcCount +
595                                    20L * dsCount * arcCount + 8L * dsCount * rowsCount) +
596                                    (1L + 2L * dsCount + arcCount) * 2L * RrdPrimitive.STRING_LENGTH;
597            }
598    
599            /**
600             * Compares the current RrdDef with another. RrdDefs are considered equal if:<p>
601             * <ul>
602             * <li>RRD steps match
603             * <li>all datasources have exactly the same definition in both RrdDef objects (datasource names,
604             * types, heartbeat, min and max values must match)
605             * <li>all archives have exactly the same definition in both RrdDef objects (archive consolidation
606             * functions, X-file factors, step and row counts must match)
607             * </ul>
608             *
609             * @param obj The second RrdDef object
610             * @return true if RrdDefs match exactly, false otherwise
611             */
612            public boolean equals(Object obj) {
613                    if (obj == null || !(obj instanceof RrdDef)) {
614                            return false;
615                    }
616                    RrdDef rrdDef2 = (RrdDef) obj;
617                    // check primary RRD step
618                    if (step != rrdDef2.step) {
619                            return false;
620                    }
621                    // check datasources
622                    DsDef[] dsDefs = getDsDefs(), dsDefs2 = rrdDef2.getDsDefs();
623                    if (dsDefs.length != dsDefs2.length) {
624                            return false;
625                    }
626                    for (DsDef dsDef : dsDefs) {
627                            boolean matched = false;
628                            for (DsDef dsDef2 : dsDefs2) {
629                                    if (dsDef.exactlyEqual(dsDef2)) {
630                                            matched = true;
631                                            break;
632                                    }
633                            }
634                            // this datasource could not be matched
635                            if (!matched) {
636                                    return false;
637                            }
638                    }
639                    // check archives
640                    ArcDef[] arcDefs = getArcDefs(), arcDefs2 = rrdDef2.getArcDefs();
641                    if (arcDefs.length != arcDefs2.length) {
642                            return false;
643                    }
644                    for (ArcDef arcDef : arcDefs) {
645                            boolean matched = false;
646                            for (ArcDef arcDef2 : arcDefs2) {
647                                    if (arcDef.exactlyEqual(arcDef2)) {
648                                            matched = true;
649                                            break;
650                                    }
651                            }
652                            // this archive could not be matched
653                            if (!matched) {
654                                    return false;
655                            }
656                    }
657                    // everything matches
658                    return true;
659            }
660    
661            /**
662             * Removes all datasource definitions.
663             */
664            public void removeDatasources() {
665                    dsDefs.clear();
666            }
667    
668            /**
669             * Removes all RRA archive definitions.
670             */
671            public void removeArchives() {
672                    arcDefs.clear();
673            }
674    }