/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.util.java;
import com.novell.ldapchai.util.StringHelper;
import password.pwm.PwmConstants;
import password.pwm.i18n.Display;
import password.pwm.util.LocaleHelper;
import java.io.Serializable;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* An immutable class representing a time period. The internal value of the time period is
* stored as milliseconds.
* <p/>
* Negative time durations are not permitted. Operations that would result in a negative value
* are negated and will instead result in positive values.
*
* @author Jason D. Rivard
*/
public class TimeDuration implements Comparable, Serializable {
// ------------------------------ FIELDS ------------------------------
public static final TimeDuration ZERO = new TimeDuration(0);
public static final TimeDuration MILLISECOND = new TimeDuration(1, TimeUnit.MILLISECONDS);
public static final TimeDuration SECOND = new TimeDuration(1, TimeUnit.SECONDS);
public static final TimeDuration SECONDS_10 = new TimeDuration(30, TimeUnit.SECONDS);
public static final TimeDuration SECONDS_30 = new TimeDuration(30, TimeUnit.SECONDS);
public static final TimeDuration MINUTE = new TimeDuration(1, TimeUnit.MINUTES);
public static final TimeDuration HOUR = new TimeDuration(1, TimeUnit.HOURS);
public static final TimeDuration DAY = new TimeDuration(1, TimeUnit.DAYS);
private final long ms;
private transient TimeDetail cachedTimeDetail;
// --------------------------- CONSTRUCTORS ---------------------------
/**
* Create a new TimeDuration using the specified duration, in milliseconds
*
* @param durationMilliseconds a time period in milliseconds
*/
public TimeDuration(final long durationMilliseconds) {
if (durationMilliseconds < 0) {
this.ms = 0;
} else {
this.ms = durationMilliseconds;
}
}
public TimeDuration(final long duration, final TimeUnit timeUnit) {
this(timeUnit.toMillis(duration));
}
public static TimeDuration fromCurrent(final long ms) {
return new TimeDuration(System.currentTimeMillis(), ms);
}
public static TimeDuration fromCurrent(final Date date) {
return new TimeDuration(System.currentTimeMillis(), date.getTime());
}
public static TimeDuration fromCurrent(final Instant instant) {
return new TimeDuration(System.currentTimeMillis(), instant.toEpochMilli());
}
public static String compactFromCurrent(final long ms) {
return new TimeDuration(System.currentTimeMillis(), ms).asCompactString();
}
public static String asCompactString(final long ms) {
return new TimeDuration(ms).asCompactString();
}
/**
* Create a new TimeDuration using the absolute difference as the time
* period between the two supplied timestamps
*
* @param date timestamp in Date format
* @param milliseconds timestamp in ms
*/
public TimeDuration(final Date date, final long milliseconds) {
this(date.getTime(), milliseconds);
}
/**
* Create a new TimeDuration using the absolute difference as the time
* period between the two supplied timestamps
*
* @param date timestamp in Date format
* @param milliseconds timestamp in ms
*/
public TimeDuration(final long milliseconds, final Date date) {
this(milliseconds, date.getTime());
}
/**
* Create a new TimeDuration using the absolute difference as the time
* period between the two supplied timestamps
*
* @param instant1 timestamp in Instant format
* @param instant2 timestamp in Instant format
*/
public TimeDuration(final Instant instant1, final Instant instant2) {
this(instant1.toEpochMilli(), instant2.toEpochMilli());
}
/**
* Create a new TimeDuration using the absolute difference as the time
* period between the two supplied timestamps
*
* @param date1 timestamp in Date format
* @param date2 timestamp in Date format
*/
public TimeDuration(final Date date1, final Date date2) {
this(date1.getTime(), date2.getTime());
}
/**
* Create a new TimeDuration using the absolute difference as the time
* period between the two supplied timestamps
*
* @param milliseconds1 timestamp in ms
* @param milliseconds2 timestamp in ms
*/
public TimeDuration(final long milliseconds1, final long milliseconds2) {
this(Math.abs(milliseconds1 - milliseconds2));
}
// ------------------------ CANONICAL METHODS ------------------------
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TimeDuration)) {
return false;
}
final TimeDuration timeDuration = (TimeDuration) o;
return ms == timeDuration.ms;
}
public int hashCode() {
return (int) (ms ^ (ms >>> 32));
}
// -------------------------- OTHER METHODS --------------------------
public TimeDuration add(final TimeDuration duration) {
return new TimeDuration(this.getTotalMilliseconds() + duration.getTotalMilliseconds());
}
public long getTotalMilliseconds() {
return ms;
}
public long getTotalSeconds() {
return ms / 1000;
}
public long getTotalMinutes() {
return ms / (60 * 1000);
}
public long getTotalDays() {
return ms / (60 * 1000 * 60 * 24);
}
public String asCompactString() {
final StringBuilder sb = new StringBuilder();
if (this.equals(ZERO)) {
return "0ms";
}
if (this.equals(DAY) || this.isLongerThan(DAY)) {
sb.append(this.getDays());
sb.append("d");
}
if (this.equals(HOUR) || this.isLongerThan(HOUR)) {
if (getHours() != 0 && getHours() != 24) {
if (sb.length() > 0) {
sb.append(":");
}
sb.append(getHours());
sb.append("h");
}
}
if (this.equals(MINUTE) || this.isLongerThan(MINUTE)) {
if (getMinutes() != 0) {
if (sb.length() > 0) {
sb.append(":");
}
sb.append(getMinutes());
sb.append("m");
}
}
if (this.isShorterThan(1000 * 60 * 10) && this.isLongerThan(1000 * 3)) { // 10 minutes to 3 seconds
if (getSeconds() != 0) {
if (sb.length() > 0) {
sb.append(":");
}
sb.append(getSeconds());
sb.append("s");
}
}
if (this.isShorterThan(1000 * 3)) { // 3 seconds
if (getMilliseconds() != 0) {
if (sb.length() > 0) {
sb.append(":");
}
sb.append(getMilliseconds());
sb.append("ms");
}
}
if (sb.length() == 0) {
sb.append(0);
sb.append("s");
}
return sb.toString();
}
public long getDays() {
return getTimeDetail().days;
}
private TimeDetail getTimeDetail() {
// lazy init, but not sync'd, no big deal if dupes created
if (cachedTimeDetail == null) {
cachedTimeDetail = new TimeDetail(ms);
}
return cachedTimeDetail;
}
public long getHours() {
return getTimeDetail().hours;
}
public boolean isLongerThan(final TimeDuration duration) {
return this.compareTo(duration) == 1;
}
public int compareTo(final Object o) {
final TimeDuration td = (TimeDuration) o;
final long otherMS = td.getTotalMilliseconds();
return (ms == otherMS ? 0 : (ms < otherMS ? -1 : 1));
}
public long getMinutes() {
return getTimeDetail().minutes;
}
public boolean isLongerThan(final long durationMS) {
return this.isLongerThan(new TimeDuration(durationMS));
}
public boolean isLongerThan(final long duration, final TimeUnit timeUnit) {
return this.isLongerThan(timeUnit.toMillis(duration));
}
public boolean isShorterThan(final long duration, final TimeUnit timeUnit) {
return this.isShorterThan(timeUnit.toMillis(duration));
}
public long getSeconds() {
return getTimeDetail().seconds;
}
public boolean isShorterThan(final long durationMS) {
return this.isShorterThan(new TimeDuration(durationMS));
}
public long getMilliseconds() {
return ms;
}
public String asLongString() {
return asLongString(PwmConstants.DEFAULT_LOCALE);
}
public String asLongString(final Locale locale) {
final TimeDetail timeDetail = getTimeDetail();
final List<String> segments = new ArrayList<>();
//output number of days
if (timeDetail.days > 0) {
final StringBuilder sb = new StringBuilder();
sb.append(timeDetail.days);
sb.append(" ");
sb.append(timeDetail.days == 1 ? LocaleHelper.getLocalizedMessage(locale, Display.Display_Day, null) : LocaleHelper.getLocalizedMessage(locale,Display.Display_Days,null));
segments.add(sb.toString());
}
//output number of hours
if (timeDetail.hours > 0) {
final StringBuilder sb = new StringBuilder();
sb.append(timeDetail.hours);
sb.append(" ");
sb.append(timeDetail.hours == 1 ? LocaleHelper.getLocalizedMessage(locale,Display.Display_Hour,null) : LocaleHelper.getLocalizedMessage(locale,Display.Display_Hours,null));
segments.add(sb.toString());
}
//output number of minutes
if (timeDetail.minutes > 0) {
final StringBuilder sb = new StringBuilder();
sb.append(timeDetail.minutes);
sb.append(" ");
sb.append(timeDetail.minutes == 1 ? LocaleHelper.getLocalizedMessage(locale,Display.Display_Minute,null) : LocaleHelper.getLocalizedMessage(locale,Display.Display_Minutes,null));
segments.add(sb.toString());
}
//seconds & ms
if (timeDetail.seconds > 0 || segments.isEmpty()) {
final StringBuilder sb = new StringBuilder();
if (sb.length() == 0) {
if (ms < 5000) {
final BigDecimal msDecimal = new BigDecimal(ms).movePointLeft(3);
final DecimalFormat formatter;
if (ms > 2000) {
formatter = new DecimalFormat("#.#");
} else if (ms > 1000) {
formatter = new DecimalFormat("#.##");
} else {
formatter = new DecimalFormat("#.###");
}
sb.append(formatter.format(msDecimal));
} else {
sb.append(timeDetail.seconds);
}
} else {
sb.append(timeDetail.seconds);
}
sb.append(" ");
sb.append(ms == 1000
? LocaleHelper.getLocalizedMessage(locale,Display.Display_Second,null)
: LocaleHelper.getLocalizedMessage(locale,Display.Display_Seconds,null)
);
segments.add(sb.toString());
}
return StringHelper.stringCollectionToString(segments, ", ");
}
public Date getDateAfterNow() {
return this.getDateAfter(new Date(System.currentTimeMillis()));
}
public Date getDateAfter(final Date specifiedDate) {
return new Date(specifiedDate.getTime() + ms);
}
public Instant getInstantAfter(final Instant specifiedDate) {
return specifiedDate.minusMillis(ms);
}
public Instant getInstantAfterNow() {
return Instant.now().minusMillis(ms);
}
public Date getDateBeforeNow() {
return this.getDateBefore(new Date(System.currentTimeMillis()));
}
public Date getDateBefore(final Date specifiedDate) {
return new Date(specifiedDate.getTime() - ms);
}
public boolean isShorterThan(final TimeDuration duration) {
return this.compareTo(duration) == -1;
}
public TimeDuration subtract(final TimeDuration duration) {
return new TimeDuration(Math.abs(this.getTotalMilliseconds() - duration.getTotalMilliseconds()));
}
@Override
public String toString() {
return "TimeDuration[" + this.asCompactString() + "]";
}
/**
* Pause the calling thread the specified amount of time.
*
* @param sleepTimeMS - a time duration in milliseconds
* @return time actually spent sleeping
*/
public static TimeDuration pause(final long sleepTimeMS) {
final long startTime = System.currentTimeMillis();
do {
try {
final long sleepTime = sleepTimeMS - (System.currentTimeMillis() - startTime);
Thread.sleep(sleepTime > 0 ? sleepTime : 5);
} catch (InterruptedException e) {
//who cares
}
} while ((System.currentTimeMillis() - startTime) < sleepTimeMS);
return TimeDuration.fromCurrent(startTime);
}
/**
* Pause the calling thread the specified amount of time.
*
* @return time actually spent sleeping
*/
public TimeDuration pause() {
return pause(this.getTotalMilliseconds());
}
// -------------------------- INNER CLASSES --------------------------
private static class TimeDetail implements Serializable {
private final long milliseconds;
private final long seconds;
private final long minutes;
private final long hours;
private final long days;
TimeDetail(final long duration) {
final long totalSeconds = new BigDecimal(duration).movePointLeft(3).longValue();
milliseconds = duration % 1000;
seconds = (totalSeconds) % 60;
minutes = (totalSeconds / 60) % 60;
hours = ((totalSeconds / 60) / 60) % 24;
days = (((totalSeconds / 60) / 60) / 24);
}
}
}