001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util;
018
019import java.text.DecimalFormat;
020import java.text.DecimalFormatSymbols;
021import java.text.NumberFormat;
022import java.time.Duration;
023import java.util.Locale;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030/**
031 * Time utils.
032 */
033public final class TimeUtils {
034
035    private static final Logger LOG = LoggerFactory.getLogger(TimeUtils.class);
036    private static final Pattern NUMBERS_ONLY_STRING_PATTERN = Pattern.compile("^[-]?(\\d)+$", Pattern.CASE_INSENSITIVE);
037    private static final Pattern HOUR_REGEX_PATTERN
038            = Pattern.compile("((\\d)*(\\d))\\s*h(our(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE);
039    private static final Pattern MINUTES_REGEX_PATTERN
040            = Pattern.compile("((\\d)*(\\d))\\s*m(in(ute(s)?)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE);
041    private static final Pattern SECONDS_REGEX_PATTERN
042            = Pattern.compile("((\\d)(\\d)*)(\\.(\\d+))?\\s*s(ec(ond)?(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE);
043    private static final Pattern MILLIS_REGEX_PATTERN
044            = Pattern.compile("((\\d)(\\d)*)(\\.(\\d+))?\\s*m(illi)?s(ec(ond)?(s)?)?(?=\\b|\\d|$)", Pattern.CASE_INSENSITIVE);
045
046    private TimeUtils() {
047    }
048
049    public static boolean isPositive(Duration dur) {
050        return dur.getSeconds() > 0 || dur.getNano() != 0;
051    }
052
053    public static String printDuration(Duration uptime) {
054        return printDuration(uptime.toMillis());
055    }
056
057    /**
058     * Prints the duration in a human readable format as X days Y hours Z minutes etc.
059     *
060     * @param  uptime the uptime in millis
061     * @return        the time used for displaying on screen or in logs
062     */
063    public static String printDuration(double uptime) {
064        // Code taken from Karaf
065        // https://svn.apache.org/repos/asf/karaf/trunk/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/InfoAction.java
066
067        NumberFormat fmtI = new DecimalFormat("###,###", new DecimalFormatSymbols(Locale.ENGLISH));
068        NumberFormat fmtD = new DecimalFormat("###,##0.000", new DecimalFormatSymbols(Locale.ENGLISH));
069
070        uptime /= 1000;
071        if (uptime < 60) {
072            return fmtD.format(uptime) + " seconds";
073        }
074        uptime /= 60;
075        if (uptime < 60) {
076            long minutes = (long) uptime;
077            String s = fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute");
078            return s;
079        }
080        uptime /= 60;
081        if (uptime < 24) {
082            long hours = (long) uptime;
083            long minutes = (long) ((uptime - hours) * 60);
084            String s = fmtI.format(hours) + (hours > 1 ? " hours" : " hour");
085            if (minutes != 0) {
086                s += " " + fmtI.format(minutes) + (minutes > 1 ? " minutes" : " minute");
087            }
088            return s;
089        }
090        uptime /= 24;
091        long days = (long) uptime;
092        long hours = (long) ((uptime - days) * 24);
093        String s = fmtI.format(days) + (days > 1 ? " days" : " day");
094        if (hours != 0) {
095            s += " " + fmtI.format(hours) + (hours > 1 ? " hours" : " hour");
096        }
097        return s;
098    }
099
100    public static Duration toDuration(String source) throws IllegalArgumentException {
101        return Duration.ofMillis(toMilliSeconds(source));
102    }
103
104    public static long toMilliSeconds(String source) throws IllegalArgumentException {
105        // quick conversion if its only digits
106        boolean digit = true;
107        for (int i = 0; i < source.length(); i++) {
108            char ch = source.charAt(i);
109            // special for fist as it can be negative number
110            if (i == 0 && ch == '-') {
111                continue;
112            }
113            // quick check if its 0..9
114            if (ch < '0' || ch > '9') {
115                digit = false;
116                break;
117            }
118        }
119        if (digit) {
120            return Long.parseLong(source);
121        }
122
123        long milliseconds = 0;
124        boolean foundFlag = false;
125
126        checkCorrectnessOfPattern(source);
127        Matcher matcher;
128
129        matcher = createMatcher(NUMBERS_ONLY_STRING_PATTERN, source);
130        if (matcher.find()) {
131            // Note: This will also be used for regular numeric strings.
132            //       This String -> long converter will be used for all strings.
133            milliseconds = Long.parseLong(source);
134        } else {
135            matcher = createMatcher(HOUR_REGEX_PATTERN, source);
136            if (matcher.find()) {
137                milliseconds = milliseconds + (3600000 * Long.parseLong(matcher.group(1)));
138                foundFlag = true;
139            }
140
141            matcher = createMatcher(MINUTES_REGEX_PATTERN, source);
142            if (matcher.find()) {
143                long minutes = Long.parseLong(matcher.group(1));
144                foundFlag = true;
145                milliseconds = milliseconds + (60000 * minutes);
146            }
147
148            matcher = createMatcher(SECONDS_REGEX_PATTERN, source);
149            if (matcher.find()) {
150                long seconds = Long.parseLong(matcher.group(1));
151                milliseconds += 1000 * seconds;
152                if (matcher.group(5) != null && !matcher.group(5).isEmpty()) {
153                    long ms = Long.parseLong(matcher.group(5));
154                    milliseconds += ms;
155                }
156                foundFlag = true;
157            }
158
159            matcher = createMatcher(MILLIS_REGEX_PATTERN, source);
160            if (matcher.find()) {
161                long millis = Long.parseLong(matcher.group(1));
162                foundFlag = true;
163                milliseconds += millis;
164            }
165
166            // No pattern matched... initiating fallback check and conversion (if required).
167            // The source at this point may contain illegal values or special characters
168            if (!foundFlag) {
169                milliseconds = Long.parseLong(source);
170            }
171        }
172
173        LOG.trace("source: [{}], milliseconds: {}", source, milliseconds);
174
175        return milliseconds;
176    }
177
178    private static void checkCorrectnessOfPattern(String source) {
179        //replace only numbers once
180        Matcher matcher = createMatcher(NUMBERS_ONLY_STRING_PATTERN, source);
181        String replaceSource = matcher.replaceFirst("");
182
183        //replace hour string once
184        matcher = createMatcher(HOUR_REGEX_PATTERN, replaceSource);
185        if (matcher.find() && matcher.find()) {
186            throw new IllegalArgumentException("Hours should not be specified more then once: " + source);
187        }
188        replaceSource = matcher.replaceFirst("");
189
190        //replace minutes once
191        matcher = createMatcher(MINUTES_REGEX_PATTERN, replaceSource);
192        if (matcher.find() && matcher.find()) {
193            throw new IllegalArgumentException("Minutes should not be specified more then once: " + source);
194        }
195        replaceSource = matcher.replaceFirst("");
196
197        //replace seconds once
198        matcher = createMatcher(SECONDS_REGEX_PATTERN, replaceSource);
199        if (matcher.find() && matcher.find()) {
200            throw new IllegalArgumentException("Seconds should not be specified more then once: " + source);
201        }
202        replaceSource = matcher.replaceFirst("");
203
204        //replace millis once
205        matcher = createMatcher(MILLIS_REGEX_PATTERN, replaceSource);
206        if (matcher.find() && matcher.find()) {
207            throw new IllegalArgumentException("Milliseconds should not be specified more then once: " + source);
208        }
209        replaceSource = matcher.replaceFirst("");
210
211        if (replaceSource.length() > 0) {
212            throw new IllegalArgumentException("Illegal characters: " + source);
213        }
214    }
215
216    private static Matcher createMatcher(Pattern pattern, String source) {
217        return pattern.matcher(source);
218    }
219
220}