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.core.timespec.TimeParser;
029    import org.jrobin.core.timespec.TimeSpec;
030    import org.w3c.dom.Document;
031    import org.w3c.dom.Element;
032    import org.w3c.dom.Node;
033    import org.w3c.dom.NodeList;
034    import org.xml.sax.InputSource;
035    import org.xml.sax.SAXException;
036    
037    import javax.xml.parsers.DocumentBuilder;
038    import javax.xml.parsers.DocumentBuilderFactory;
039    import javax.xml.parsers.ParserConfigurationException;
040    import java.awt.*;
041    import java.io.*;
042    import java.text.DecimalFormat;
043    import java.text.NumberFormat;
044    import java.text.ParseException;
045    import java.text.SimpleDateFormat;
046    import java.util.ArrayList;
047    import java.util.Calendar;
048    import java.util.Date;
049    import java.util.Locale;
050    
051    /**
052     * Class defines various utility functions used in JRobin.
053     *
054     * @author <a href="mailto:saxon@jrobin.org">Sasa Markovic</a>
055     */
056    public class Util {
057    
058            public static final long MAX_LONG = Long.MAX_VALUE;
059            public static final long MIN_LONG = -Long.MAX_VALUE;
060    
061            public static final double MAX_DOUBLE = Double.MAX_VALUE;
062            public static final double MIN_DOUBLE = -Double.MAX_VALUE;
063    
064            // pattern RRDTool uses to format doubles in XML files
065            static final String PATTERN = "0.0000000000E00";
066            // directory under $USER_HOME used for demo graphs storing
067            static final String JROBIN_DIR = "jrobin-demo";
068    
069            static final DecimalFormat df;
070    
071            static {
072                    df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.ENGLISH);
073                    df.applyPattern(PATTERN);
074                    df.setPositivePrefix("+");
075            }
076    
077            /**
078             * Converts an array of long primitives to an array of doubles.
079             *
080             * @return Same array but with all values as double.
081             */
082            public static double[] toDoubleArray(final long[] array) {
083                    double[] values = new double[ array.length ];
084                    for (int i = 0; i < array.length; i++) {
085                            values[i] = array[i];
086                    }
087                    return values;
088            }
089    
090            /**
091             * Returns current timestamp in seconds (without milliseconds). Returned timestamp
092             * is obtained with the following expression: <p>
093             * <p/>
094             * <code>(System.currentTimeMillis() + 500L) / 1000L</code>
095             *
096             * @return Current timestamp
097             */
098            public static long getTime() {
099                    return (System.currentTimeMillis() + 500L) / 1000L;
100            }
101    
102            /**
103             * Just an alias for {@link #getTime()} method.
104             *
105             * @return Current timestamp (without milliseconds)
106             */
107            public static long getTimestamp() {
108                    return getTime();
109            }
110    
111            /**
112             * Rounds the given timestamp to the nearest whole &quote;step&quote;. Rounded value is obtained
113             * from the following expression:<p>
114             * <code>timestamp - timestamp % step;</code><p>
115             *
116             * @param timestamp Timestamp in seconds
117             * @param step    Step in seconds
118             * @return "Rounded" timestamp
119             */
120            public static long normalize(long timestamp, long step) {
121                    return timestamp - timestamp % step;
122            }
123    
124            /**
125             * Returns the greater of two double values, but treats NaN as the smallest possible
126             * value. Note that <code>Math.max()</code> behaves differently for NaN arguments.
127             *
128             * @param x an argument
129             * @param y another argument
130             * @return the lager of arguments
131             */
132            public static double max(double x, double y) {
133                    return Double.isNaN(x) ? y : Double.isNaN(y) ? x : Math.max(x, y);
134            }
135    
136            /**
137             * Returns the smaller of two double values, but treats NaN as the greatest possible
138             * value. Note that <code>Math.min()</code> behaves differently for NaN arguments.
139             *
140             * @param x an argument
141             * @param y another argument
142             * @return the smaller of arguments
143             */
144            public static double min(double x, double y) {
145                    return Double.isNaN(x) ? y : Double.isNaN(y) ? x : Math.min(x, y);
146            }
147    
148            /**
149             * Calculates sum of two doubles, but treats NaNs as zeros.
150             *
151             * @param x First double
152             * @param y Second double
153             * @return Sum(x,y) calculated as <code>Double.isNaN(x)? y: Double.isNaN(y)? x: x + y;</code>
154             */
155            public static double sum(double x, double y) {
156                    return Double.isNaN(x) ? y : Double.isNaN(y) ? x : x + y;
157            }
158    
159            static String formatDouble(double x, String nanString, boolean forceExponents) {
160                    if (Double.isNaN(x)) {
161                            return nanString;
162                    }
163                    if (forceExponents) {
164                            return df.format(x);
165                    }
166                    return "" + x;
167            }
168    
169            static String formatDouble(double x, boolean forceExponents) {
170                    return formatDouble(x, "" + Double.NaN, forceExponents);
171            }
172    
173            /**
174             * Formats double as a string using exponential notation (RRDTool like). Used for debugging
175             * throught the project.
176             *
177             * @param x value to be formatted
178             * @return string like "+1.234567E+02"
179             */
180            public static String formatDouble(double x) {
181                    return formatDouble(x, true);
182            }
183    
184            /**
185             * Returns <code>Date</code> object for the given timestamp (in seconds, without
186             * milliseconds)
187             *
188             * @param timestamp Timestamp in seconds.
189             * @return Corresponding Date object.
190             */
191            public static Date getDate(long timestamp) {
192                    return new Date(timestamp * 1000L);
193            }
194    
195            /**
196             * Returns <code>Calendar</code> object for the given timestamp
197             * (in seconds, without milliseconds)
198             *
199             * @param timestamp Timestamp in seconds.
200             * @return Corresponding Calendar object.
201             */
202            public static Calendar getCalendar(long timestamp) {
203                    Calendar calendar = Calendar.getInstance();
204                    calendar.setTimeInMillis(timestamp * 1000L);
205                    return calendar;
206            }
207    
208            /**
209             * Returns <code>Calendar</code> object for the given Date object
210             *
211             * @param date Date object
212             * @return Corresponding Calendar object.
213             */
214            public static Calendar getCalendar(Date date) {
215                    Calendar calendar = Calendar.getInstance();
216                    calendar.setTime(date);
217                    return calendar;
218            }
219    
220            /**
221             * Returns timestamp (unix epoch) for the given Date object
222             *
223             * @param date Date object
224             * @return Corresponding timestamp (without milliseconds)
225             */
226            public static long getTimestamp(Date date) {
227                    // round to whole seconds, ignore milliseconds
228                    return (date.getTime() + 499L) / 1000L;
229            }
230    
231            /**
232             * Returns timestamp (unix epoch) for the given Calendar object
233             *
234             * @param gc Calendar object
235             * @return Corresponding timestamp (without milliseconds)
236             */
237            public static long getTimestamp(Calendar gc) {
238                    return getTimestamp(gc.getTime());
239            }
240    
241            /**
242             * Returns timestamp (unix epoch) for the given year, month, day, hour and minute.
243             *
244             * @param year  Year
245             * @param month Month (zero-based)
246             * @param day   Day in month
247             * @param hour  Hour
248             * @param min   Minute
249             * @return Corresponding timestamp
250             */
251            public static long getTimestamp(int year, int month, int day, int hour, int min) {
252                    Calendar calendar = Calendar.getInstance();
253                    calendar.clear();
254                    calendar.set(year, month, day, hour, min);
255                    return Util.getTimestamp(calendar);
256            }
257    
258            /**
259             * Returns timestamp (unix epoch) for the given year, month and day.
260             *
261             * @param year  Year
262             * @param month Month (zero-based)
263             * @param day   Day in month
264             * @return Corresponding timestamp
265             */
266            public static long getTimestamp(int year, int month, int day) {
267                    return Util.getTimestamp(year, month, day, 0, 0);
268            }
269    
270            /**
271             * Parses at-style time specification and returns the corresponding timestamp. For example:<p>
272             * <pre>
273             * long t = Util.getTimestamp("now-1d");
274             * </pre>
275             *
276             * @param atStyleTimeSpec at-style time specification. For the complete explanation of the syntax
277             *                        allowed see RRDTool's <code>rrdfetch</code> man page.<p>
278             * @return timestamp in seconds since epoch.
279             * @throws RrdException Thrown if invalid time specification is supplied.
280             */
281            public static long getTimestamp(String atStyleTimeSpec) throws RrdException {
282                    TimeSpec timeSpec = new TimeParser(atStyleTimeSpec).parse();
283                    return timeSpec.getTimestamp();
284            }
285    
286            /**
287             * Parses two related at-style time specifications and returns corresponding timestamps. For example:<p>
288             * <pre>
289             * long[] t = Util.getTimestamps("end-1d","now");
290             * </pre>
291             *
292             * @param atStyleTimeSpec1 Starting at-style time specification. For the complete explanation of the syntax
293             *                         allowed see RRDTool's <code>rrdfetch</code> man page.<p>
294             * @param atStyleTimeSpec2 Ending at-style time specification. For the complete explanation of the syntax
295             *                         allowed see RRDTool's <code>rrdfetch</code> man page.<p>
296             * @return An array of two longs representing starting and ending timestamp in seconds since epoch.
297             * @throws RrdException Thrown if any input time specification is invalid.
298             */
299            public static long[] getTimestamps(String atStyleTimeSpec1, String atStyleTimeSpec2) throws RrdException {
300                    TimeSpec timeSpec1 = new TimeParser(atStyleTimeSpec1).parse();
301                    TimeSpec timeSpec2 = new TimeParser(atStyleTimeSpec2).parse();
302                    return TimeSpec.getTimestamps(timeSpec1, timeSpec2);
303            }
304    
305            /**
306             * Parses input string as a double value. If the value cannot be parsed, Double.NaN
307             * is returned (NumberFormatException is never thrown).
308             *
309             * @param valueStr String representing double value
310             * @return a double corresponding to the input string
311             */
312            public static double parseDouble(String valueStr) {
313                    double value;
314                    try {
315                            value = Double.parseDouble(valueStr);
316                    }
317                    catch (NumberFormatException nfe) {
318                            value = Double.NaN;
319                    }
320                    return value;
321            }
322    
323            /**
324             * Checks if a string can be parsed as double.
325             *
326             * @param s Input string
327             * @return <code>true</code> if the string can be parsed as double, <code>false</code> otherwise
328             */
329            public static boolean isDouble(String s) {
330                    try {
331                            Double.parseDouble(s);
332                            return true;
333                    }
334                    catch (NumberFormatException nfe) {
335                            return false;
336                    }
337            }
338    
339            /**
340             * Parses input string as a boolean value. The parser is case insensitive.
341             *
342             * @param valueStr String representing boolean value
343             * @return <code>true</code>, if valueStr equals to 'true', 'on', 'yes', 'y' or '1';
344             *         <code>false</code> in all other cases.
345             */
346            public static boolean parseBoolean(String valueStr) {
347                    return valueStr.equalsIgnoreCase("true") ||
348                                    valueStr.equalsIgnoreCase("on") ||
349                                    valueStr.equalsIgnoreCase("yes") ||
350                                    valueStr.equalsIgnoreCase("y") ||
351                                    valueStr.equalsIgnoreCase("1");
352            }
353    
354            /**
355             * Parses input string as color. The color string should be of the form #RRGGBB (no alpha specified,
356             * opaque color) or #RRGGBBAA (alpa specified, transparent colors). Leading character '#' is
357             * optional.
358             *
359             * @param valueStr Input string, for example #FFAA24, #AABBCC33, 010203 or ABC13E4F
360             * @return Paint object
361             * @throws RrdException If the input string is not 6 or 8 characters long (without optional '#')
362             */
363            public static Paint parseColor(String valueStr) throws RrdException {
364                    String c = valueStr.startsWith("#") ? valueStr.substring(1) : valueStr;
365                    if (c.length() != 6 && c.length() != 8) {
366                            throw new RrdException("Invalid color specification: " + valueStr);
367                    }
368                    String r = c.substring(0, 2), g = c.substring(2, 4), b = c.substring(4, 6);
369                    if (c.length() == 6) {
370                            return new Color(Integer.parseInt(r, 16), Integer.parseInt(g, 16), Integer.parseInt(b, 16));
371                    }
372                    else {
373                            String a = c.substring(6);
374                            return new Color(Integer.parseInt(r, 16), Integer.parseInt(g, 16),
375                                            Integer.parseInt(b, 16), Integer.parseInt(a, 16));
376                    }
377            }
378    
379            /**
380             * Returns file system separator string.
381             *
382             * @return File system separator ("/" on Unix, "\" on Windows)
383             */
384            public static String getFileSeparator() {
385                    return System.getProperty("file.separator");
386            }
387    
388            /**
389             * Returns path to user's home directory.
390             *
391             * @return Path to users home directory, with file separator appended.
392             */
393            public static String getUserHomeDirectory() {
394                    return System.getProperty("user.home") + getFileSeparator();
395            }
396    
397            /**
398             * Returns path to directory used for placement of JRobin demo graphs and creates it
399             * if necessary.
400             *
401             * @return Path to demo directory (defaults to $HOME/jrobin/) if directory exists or
402             *         was successfully created. Null if such directory could not be created.
403             */
404            public static String getJRobinDemoDirectory() {
405                    String homeDirPath = getUserHomeDirectory() + JROBIN_DIR + getFileSeparator();
406                    File homeDirFile = new File(homeDirPath);
407                    return (homeDirFile.exists() || homeDirFile.mkdirs()) ? homeDirPath : null;
408            }
409    
410            /**
411             * Returns full path to the file stored in the demo directory of JRobin
412             *
413             * @param filename Partial path to the file stored in the demo directory of JRobin
414             *                 (just name and extension, without parent directories)
415             * @return Full path to the file
416             */
417            public static String getJRobinDemoPath(String filename) {
418                    String demoDir = getJRobinDemoDirectory();
419                    if (demoDir != null) {
420                            return demoDir + filename;
421                    }
422                    else {
423                            return null;
424                    }
425            }
426    
427            static boolean sameFilePath(String path1, String path2) throws IOException {
428                    File file1 = new File(path1);
429                    File file2 = new File(path2);
430                    return file1.getCanonicalPath().equals(file2.getCanonicalPath());
431            }
432    
433            static int getMatchingDatasourceIndex(RrdDb rrd1, int dsIndex, RrdDb rrd2) throws IOException {
434                    String dsName = rrd1.getDatasource(dsIndex).getDsName();
435                    try {
436                            return rrd2.getDsIndex(dsName);
437                    }
438                    catch (RrdException e) {
439                            return -1;
440                    }
441            }
442    
443            static int getMatchingArchiveIndex(RrdDb rrd1, int arcIndex, RrdDb rrd2)
444                            throws IOException {
445                    Archive archive = rrd1.getArchive(arcIndex);
446                    String consolFun = archive.getConsolFun();
447                    int steps = archive.getSteps();
448                    try {
449                            return rrd2.getArcIndex(consolFun, steps);
450                    }
451                    catch (RrdException e) {
452                            return -1;
453                    }
454            }
455    
456            static String getTmpFilename() throws IOException {
457                    return File.createTempFile("JROBIN_", ".tmp").getCanonicalPath();
458            }
459    
460            static final String ISO_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";   // ISO
461    
462            /**
463             * Creates Calendar object from a string. The string should represent
464             * either a long integer (UNIX timestamp in seconds without milliseconds,
465             * like "1002354657") or a human readable date string in the format "yyyy-MM-dd HH:mm:ss"
466             * (like "2004-02-25 12:23:45").
467             *
468             * @param timeStr Input string
469             * @return Calendar object
470             */
471            public static Calendar getCalendar(String timeStr) {
472                    // try to parse it as long
473                    try {
474                            long timestamp = Long.parseLong(timeStr);
475                            return Util.getCalendar(timestamp);
476                    }
477                    catch (NumberFormatException nfe) {
478                            // not a long timestamp, try to parse it as data
479                            SimpleDateFormat df = new SimpleDateFormat(ISO_DATE_FORMAT);
480                            df.setLenient(false);
481                            try {
482                                    Date date = df.parse(timeStr);
483                                    return Util.getCalendar(date);
484                            }
485                            catch (ParseException pe) {
486                                    throw new IllegalArgumentException("Time/date not in " + ISO_DATE_FORMAT +
487                                                    " format: " + timeStr);
488                            }
489                    }
490            }
491    
492            /**
493             * Various DOM utility functions
494             */
495            public static class Xml {
496                    public static Node[] getChildNodes(Node parentNode) {
497                            return getChildNodes(parentNode, null);
498                    }
499    
500                    public static Node[] getChildNodes(Node parentNode, String childName) {
501                            ArrayList<Node> nodes = new ArrayList<Node>();
502                            NodeList nodeList = parentNode.getChildNodes();
503                            for (int i = 0; i < nodeList.getLength(); i++) {
504                                    Node node = nodeList.item(i);
505                                    if (childName == null || node.getNodeName().equals(childName)) {
506                                            nodes.add(node);
507                                    }
508                            }
509                            return nodes.toArray(new Node[0]);
510                    }
511    
512                    public static Node getFirstChildNode(Node parentNode, String childName) throws RrdException {
513                            Node[] childs = getChildNodes(parentNode, childName);
514                            if (childs.length > 0) {
515                                    return childs[0];
516                            }
517                            throw new RrdException("XML Error, no such child: " + childName);
518                    }
519    
520                    public static boolean hasChildNode(Node parentNode, String childName) {
521                            Node[] childs = getChildNodes(parentNode, childName);
522                            return childs.length > 0;
523                    }
524    
525                    // -- Wrapper around getChildValue with trim
526                    public static String getChildValue(Node parentNode, String childName) throws RrdException {
527                            return getChildValue(parentNode, childName, true);
528                    }
529    
530                    public static String getChildValue(Node parentNode, String childName, boolean trim) throws RrdException {
531                            NodeList children = parentNode.getChildNodes();
532                            for (int i = 0; i < children.getLength(); i++) {
533                                    Node child = children.item(i);
534                                    if (child.getNodeName().equals(childName)) {
535                                            return getValue(child, trim);
536                                    }
537                            }
538                            throw new RrdException("XML Error, no such child: " + childName);
539                    }
540    
541                    // -- Wrapper around getValue with trim
542                    public static String getValue(Node node) {
543                            return getValue(node, true);
544                    }
545    
546                    public static String getValue(Node node, boolean trimValue) {
547                            String value = null;
548                            Node child = node.getFirstChild();
549                            if (child != null) {
550                                    value = child.getNodeValue();
551                                    if (value != null && trimValue) {
552                                            value = value.trim();
553                                    }
554                            }
555                            return value;
556                    }
557    
558                    public static int getChildValueAsInt(Node parentNode, String childName) throws RrdException {
559                            String valueStr = getChildValue(parentNode, childName);
560                            return Integer.parseInt(valueStr);
561                    }
562    
563                    public static int getValueAsInt(Node node) {
564                            String valueStr = getValue(node);
565                            return Integer.parseInt(valueStr);
566                    }
567    
568                    public static long getChildValueAsLong(Node parentNode, String childName) throws RrdException {
569                            String valueStr = getChildValue(parentNode, childName);
570                            return Long.parseLong(valueStr);
571                    }
572    
573                    public static long getValueAsLong(Node node) {
574                            String valueStr = getValue(node);
575                            return Long.parseLong(valueStr);
576                    }
577    
578                    public static double getChildValueAsDouble(Node parentNode, String childName) throws RrdException {
579                            String valueStr = getChildValue(parentNode, childName);
580                            return Util.parseDouble(valueStr);
581                    }
582    
583                    public static double getValueAsDouble(Node node) {
584                            String valueStr = getValue(node);
585                            return Util.parseDouble(valueStr);
586                    }
587    
588                    public static boolean getChildValueAsBoolean(Node parentNode, String childName) throws RrdException {
589                            String valueStr = getChildValue(parentNode, childName);
590                            return Util.parseBoolean(valueStr);
591                    }
592    
593                    public static boolean getValueAsBoolean(Node node) {
594                            String valueStr = getValue(node);
595                            return Util.parseBoolean(valueStr);
596                    }
597    
598                    public static Element getRootElement(InputSource inputSource) throws RrdException, IOException {
599                            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
600                            factory.setValidating(false);
601                            factory.setNamespaceAware(false);
602                            try {
603                                    DocumentBuilder builder = factory.newDocumentBuilder();
604                                    Document doc = builder.parse(inputSource);
605                                    return doc.getDocumentElement();
606                            }
607                            catch (ParserConfigurationException e) {
608                                    throw new RrdException(e);
609                            }
610                            catch (SAXException e) {
611                                    throw new RrdException(e);
612                            }
613                    }
614    
615                    public static Element getRootElement(String xmlString) throws RrdException, IOException {
616                            return getRootElement(new InputSource(new StringReader(xmlString)));
617                    }
618    
619                    public static Element getRootElement(File xmlFile) throws RrdException, IOException {
620                            Reader reader = null;
621                            try {
622                                    reader = new FileReader(xmlFile);
623                                    return getRootElement(new InputSource(reader));
624                            }
625                            finally {
626                                    if (reader != null) {
627                                            reader.close();
628                                    }
629                            }
630                    }
631            }
632    
633            private static long lastLap = System.currentTimeMillis();
634    
635            /**
636             * Function used for debugging purposes and performance bottlenecks detection.
637             * Probably of no use for end users of JRobin.
638             *
639             * @return String representing time in seconds since last
640             *         <code>getLapTime()</code> method call.
641             */
642            public static String getLapTime() {
643                    long newLap = System.currentTimeMillis();
644                    double seconds = (newLap - lastLap) / 1000.0;
645                    lastLap = newLap;
646                    return "[" + seconds + " sec]";
647            }
648    
649            /**
650             * Returns the root directory of the JRobin distribution. Useful in some demo applications,
651             * probably of no use anywhere else.<p>
652             * <p/>
653             * The function assumes that all JRobin .class files are placed under
654             * the &lt;root&gt;/classes subdirectory and that all jars (libraries) are placed in the
655             * &lt;root&gt;/lib subdirectory (the original JRobin directory structure).<p>
656             *
657             * @return absolute path to JRobin's home directory
658             */
659            public static String getJRobinHomeDirectory() {
660                    String className = Util.class.getName().replace('.', '/');
661                    String uri = Util.class.getResource("/" + className + ".class").toString();
662                    //System.out.println(uri);
663                    if (uri.startsWith("file:/")) {
664                            uri = uri.substring(6);
665                            File file = new File(uri);
666                            // let's go 5 steps backwards
667                            for (int i = 0; i < 5; i++) {
668                                    file = file.getParentFile();
669                            }
670                            uri = file.getAbsolutePath();
671                    }
672                    else if (uri.startsWith("jar:file:/")) {
673                            uri = uri.substring(9, uri.lastIndexOf('!'));
674                            File file = new File(uri);
675                            // let's go 2 steps backwards
676                            for (int i = 0; i < 2; i++) {
677                                    file = file.getParentFile();
678                            }
679                            uri = file.getAbsolutePath();
680                    }
681                    else {
682                            uri = null;
683                    }
684                    return uri;
685            }
686    
687            /**
688             * Compares two doubles but treats all NaNs as equal.
689             * In Java (by default) Double.NaN == Double.NaN always returns <code>false</code>
690             *
691             * @param x the first value
692             * @param y the second value
693             * @return <code>true</code> if x and y are both equal to Double.NaN, or if x == y. <code>false</code> otherwise
694             */
695            public static boolean equal(double x, double y) {
696                    return (Double.isNaN(x) && Double.isNaN(y)) || (x == y);
697            }
698    
699            /**
700             * Returns canonical file path for the given file path
701             *
702             * @param path Absolute or relative file path
703             * @return Canonical file path
704             * @throws IOException Thrown if canonical file path could not be resolved
705             */
706            public static String getCanonicalPath(String path) throws IOException {
707                    return new File(path).getCanonicalPath();
708            }
709    
710            /**
711             * Returns last modification time for the given file.
712             *
713             * @param file File object representing file on the disk
714             * @return Last modification time in seconds (without milliseconds)
715             */
716            public static long getLastModified(String file) {
717                    return (new File(file).lastModified() + 500L) / 1000L;
718            }
719    
720            /**
721             * Checks if the file with the given file name exists
722             *
723             * @param filename File name
724             * @return <code>true</code> if file exists, <code>false</code> otherwise
725             */
726            public static boolean fileExists(String filename) {
727                    return new File(filename).exists();
728            }
729    
730            /**
731             * Finds max value for an array of doubles (NaNs are ignored). If all values in the array
732             * are NaNs, NaN is returned.
733             *
734             * @param values Array of double values
735             * @return max value in the array (NaNs are ignored)
736             */
737            public static double max(double[] values) {
738                    double max = Double.NaN;
739                    for (double value : values) {
740                            max = Util.max(max, value);
741                    }
742                    return max;
743            }
744    
745            /**
746             * Finds min value for an array of doubles (NaNs are ignored). If all values in the array
747             * are NaNs, NaN is returned.
748             *
749             * @param values Array of double values
750             * @return min value in the array (NaNs are ignored)
751             */
752            public static double min(double[] values) {
753                    double min = Double.NaN;
754                    for (double value : values) {
755                            min = Util.min(min, value);
756                    }
757                    return min;
758            }
759    
760            /**
761             * Equivalent of the C-style sprintf function. Sorry, it works only in Java5.
762             *
763             * @param format Format string
764             * @param args   Arbitrary list of arguments
765             * @return Formatted string
766             */
767            public static String sprintf(String format, Object ... args) {
768                    String fmt = format.replaceAll("([^%]|^)%([^a-zA-Z%]*)l(f|g|e)", "$1%$2$3");
769                    return String.format(fmt, args);
770            }
771    }