diff --git a/core/src/main/java/org/ocpsoft/prettytime/PrettyTime.java b/core/src/main/java/org/ocpsoft/prettytime/PrettyTime.java index 21bd3a61..b51b053b 100644 --- a/core/src/main/java/org/ocpsoft/prettytime/PrettyTime.java +++ b/core/src/main/java/org/ocpsoft/prettytime/PrettyTime.java @@ -74,7 +74,7 @@ public class PrettyTime private volatile Locale locale = Locale.getDefault(); private final Map units = new ConcurrentHashMap<>(); private volatile List cachedUnits; - private String overrideResourceBundle; + private final String overrideResourceBundle; /** * Create a new {@link PrettyTime} instance that will always use the current value of @@ -1333,127 +1333,160 @@ public Date getReferenceAsLegacyDate() } /** - * Converts the given {@link Date} to the reference {@link Instant}. If null, {@link PrettyTime} will - * always use the current value of {@link System#currentTimeMillis()} as the reference {@link Instant}. - *

- * If the {@link Date} formatted is before the reference {@link Instant}, the format command will produce a - * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, - * the format command will produce a {@link String} that is in the future tense. - * - * @see #setReference(Instant) + * Get the unmodifiable {@link List} of the current configured {@link TimeUnit} instances in calculations. */ - public PrettyTime setReference(final Date timestamp) + public List getUnits() { - return setReference(timestamp != null ? timestamp.toInstant() : null); + if (cachedUnits == null) { + List result = new ArrayList<>(units.keySet()); + result.sort(Comparator.comparing(TimeUnit::getMillisPerUnit)); + cachedUnits = Collections.unmodifiableList(result); + } + + return cachedUnits; } /** - * Set the reference {@link Instant}. If null, {@link PrettyTime} will always use the current value of - * {@link System#currentTimeMillis()} as the reference {@link Instant}. - *

- * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a - * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, - * the format command will produce a {@link String} that is in the future tense. + * Get the registered {@link TimeUnit} for the given {@link TimeUnit} type or null if none exists. */ - public PrettyTime setReference(final Instant timestamp) + @SuppressWarnings("unchecked") + public UNIT getUnit(final Class unitType) { - reference = timestamp; - return this; + if (unitType == null) + return null; + + for (TimeUnit unit : units.keySet()) { + if (unitType.isAssignableFrom(unit.getClass())) { + return (UNIT) unit; + } + } + return null; } /** - * Converts the given {@link LocalDateTime} to the reference {@link Instant} using the system default {@link ZoneId}. - * If null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} - * as the reference {@link Instant}. - *

- * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a - * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, - * the format command will produce a {@link String} that is in the future tense. - * - * @see #setReference(Instant) + * Get the currently configured {@link Locale} for this {@link PrettyTime} object. */ - public PrettyTime setReference(final LocalDateTime localDateTime) + public Locale getLocale() { - return setReference(localDateTime, ZoneId.systemDefault()); + return locale; } - /** - * Converts the given {@link LocalDateTime} to the reference {@link Instant} using the given {@link ZoneId}. If - * null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} as - * the reference {@link Instant}. - *

- * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a - * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, - * the format command will produce a {@link String} that is in the future tense. - * - * @see #setReference(Instant) - */ - public PrettyTime setReference(final LocalDateTime localDateTime, final ZoneId zoneId) + @Override + public String toString() { - return setReference(localDateTime != null ? localDateTime.atZone(zoneId).toInstant() : null); + return "PrettyTime [reference=" + reference + ", locale=" + locale + "]"; } - /** - * Converts the given {@link LocalDate} to the reference {@link Instant} using the given {@link ZoneId}. If - * null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} as - * the reference {@link Instant}. - *

- * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a - * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, - * the format command will produce a {@link String} that is in the future tense. - * - * @see #setReference(Instant) + /* + * Internal methods. */ - public PrettyTime setReference(final LocalDate localDate) + private Date now() { - return setReference(localDate != null ? localDate.atStartOfDay() : null); + return new Date(); } - /** - * Converts the given {@link LocalDate} to the reference {@link Instant} using the given {@link ZoneId}. If - * null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} as - * the reference {@link Instant}. - *

- * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a - * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, - * the format command will produce a {@link String} that is in the future tense. - * - * @see #setReference(Instant) - */ - public PrettyTime setReference(final LocalDate localDate, final ZoneId zoneId) + private void initTimeUnits() { - return setReference(localDate != null ? localDate.atStartOfDay(zoneId).toInstant() : null); + addUnit(new JustNow()); + addUnit(new Millisecond()); + addUnit(new Second()); + addUnit(new Minute()); + addUnit(new Hour()); + addUnit(new Day()); + addUnit(new Week()); + addUnit(new Month()); + addUnit(new Year()); + addUnit(new Decade()); + addUnit(new Century()); + addUnit(new Millennium()); } - /** - * Get the unmodifiable {@link List} of the current configured {@link TimeUnit} instances in calculations. - */ - public List getUnits() + private void addUnit(ResourcesTimeUnit unit) { - if (cachedUnits == null) { - List result = new ArrayList<>(units.keySet()); - Collections.sort(result, Comparator.comparing(TimeUnit::getMillisPerUnit)); - cachedUnits = Collections.unmodifiableList(result); + registerUnit(unit, new ResourcesTimeFormat(unit, overrideResourceBundle)); + } + + private Duration calculateDuration(final long difference) + { + long absoluteDifference = Math.abs(difference); + + /* + * Required for thread-safety + */ + List localUnits = getUnits(); + + DurationImpl result = new DurationImpl(); + + for (int i = 0; i < localUnits.size(); i++) { + TimeUnit unit = localUnits.get(i); + long millisPerUnit = Math.abs(unit.getMillisPerUnit()); + long quantity = Math.abs(unit.getMaxQuantity()); + + boolean isLastUnit = (i == localUnits.size() - 1); + + if ((0 == quantity) && !isLastUnit) { + quantity = localUnits.get(i + 1).getMillisPerUnit() / unit.getMillisPerUnit(); + } + + /* + * Does our unit encompass the time duration? + */ + if ((millisPerUnit * quantity > absoluteDifference) || isLastUnit) { + result.setUnit(unit); + if (millisPerUnit > absoluteDifference) { + result.setQuantity(getSign(difference)); + result.setDelta(0); + } + else { + result.setQuantity(difference / millisPerUnit); + result.setDelta(difference - result.getQuantity() * millisPerUnit); + } + break; + } + } + return result; + } - return cachedUnits; + private long getSign(final long difference) + { + return (0 > difference) ? -1 : 1; } + /* + * These methods are package-private because they change the internal object state. + */ /** - * Get the registered {@link TimeUnit} for the given {@link TimeUnit} type or null if none exists. + * Set the the {@link Locale} for this {@link PrettyTime} object. This may be an expensive operation, since this + * operation calls {@link LocaleAware#setLocale(Locale)} for each {@link TimeUnit} in {@link #getUnits()}. */ - @SuppressWarnings("unchecked") - public UNIT getUnit(final Class unitType) + protected PrettyTime setLocale(Locale locale) { - if (unitType == null) - return null; + if (locale == null) + locale = Locale.getDefault(); + this.locale = locale; for (TimeUnit unit : units.keySet()) { - if (unitType.isAssignableFrom(unit.getClass())) { - return (UNIT) unit; - } + if (unit instanceof LocaleAware) + ((LocaleAware) unit).setLocale(locale); } - return null; + for (TimeFormat format : units.values()) { + if (format instanceof LocaleAware) + ((LocaleAware) format).setLocale(locale); + } + cachedUnits = null; + return this; + } + + /** + * Remove all registered {@link TimeUnit} instances. Returns all {@link TimeUnit} instances that were removed. + */ + protected List clearUnits() + { + List result = getUnits(); + cachedUnits = null; + units.clear(); + return result; } /** @@ -1461,12 +1494,12 @@ public UNIT getUnit(final Class unitType) * an entry already exists for the given {@link TimeUnit}, its {@link TimeFormat} will be overwritten with the given * {@link TimeFormat}. ({@link TimeUnit} and {@link TimeFormat} must not be null.) */ - public PrettyTime registerUnit(final TimeUnit unit, TimeFormat format) + protected PrettyTime registerUnit(final TimeUnit unit, TimeFormat format) { cachedUnits = null; units.put(Objects.requireNonNull(unit, "TimeUnit to register must not be null."), - Objects.requireNonNull(format, "TimeFormat to register must not be null.")); + Objects.requireNonNull(format, "TimeFormat to register must not be null.")); if (unit instanceof LocaleAware) ((LocaleAware) unit).setLocale(locale); if (format instanceof LocaleAware) @@ -1474,7 +1507,7 @@ public PrettyTime registerUnit(final TimeUnit unit, TimeFormat format) return this; } - public PrettyTime setUnits(final ResourcesTimeUnit... units) + protected PrettyTime setUnits(final ResourcesTimeUnit... units) { if (units == null || units.length == 0) throw new IllegalArgumentException("TimeUnit instance(s) to register must be provided."); @@ -1488,7 +1521,7 @@ public PrettyTime setUnits(final ResourcesTimeUnit... units) return this; } - public PrettyTime setUnits(TimeFormat format, final TimeUnit... units) + protected PrettyTime setUnits(TimeFormat format, final TimeUnit... units) { if (units == null || units.length == 0) throw new IllegalArgumentException("TimeUnit instance(s) to register must be provided."); @@ -1507,7 +1540,7 @@ public PrettyTime setUnits(TimeFormat format, final TimeUnit... units) * will not be used in formatting. Returns the {@link TimeFormat} that was removed, or null if no unit * of the given type was registered. */ - public TimeFormat removeUnit(final Class unitType) + protected TimeFormat removeUnit(final Class unitType) { if (unitType == null) return null; @@ -1527,7 +1560,7 @@ public TimeFormat removeUnit(final Class unitType) * not be used in formatting. Returns the {@link TimeFormat} that was removed, or null if no such unit was * registered. */ - public TimeFormat removeUnit(final TimeUnit unit) + protected TimeFormat removeUnit(final TimeUnit unit) { if (unit == null) return null; @@ -1538,130 +1571,95 @@ public TimeFormat removeUnit(final TimeUnit unit) } /** - * Get the currently configured {@link Locale} for this {@link PrettyTime} object. + * Converts the given {@link Date} to the reference {@link Instant}. If null, {@link PrettyTime} will + * always use the current value of {@link System#currentTimeMillis()} as the reference {@link Instant}. + *

+ * If the {@link Date} formatted is before the reference {@link Instant}, the format command will produce a + * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, + * the format command will produce a {@link String} that is in the future tense. + * + * @see #setReference(Instant) */ - public Locale getLocale() + protected PrettyTime setReference(final Date timestamp) { - return locale; + return setReference(timestamp != null ? timestamp.toInstant() : null); } /** - * Set the the {@link Locale} for this {@link PrettyTime} object. This may be an expensive operation, since this - * operation calls {@link LocaleAware#setLocale(Locale)} for each {@link TimeUnit} in {@link #getUnits()}. + * Set the reference {@link Instant}. If null, {@link PrettyTime} will always use the current value of + * {@link System#currentTimeMillis()} as the reference {@link Instant}. + *

+ * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a + * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, + * the format command will produce a {@link String} that is in the future tense. */ - public PrettyTime setLocale(Locale locale) + protected PrettyTime setReference(final Instant timestamp) { - if (locale == null) - locale = Locale.getDefault(); - - this.locale = locale; - for (TimeUnit unit : units.keySet()) { - if (unit instanceof LocaleAware) - ((LocaleAware) unit).setLocale(locale); - } - for (TimeFormat format : units.values()) { - if (format instanceof LocaleAware) - ((LocaleAware) format).setLocale(locale); - } - cachedUnits = null; + reference = timestamp; return this; } - @Override - public String toString() - { - return "PrettyTime [reference=" + reference + ", locale=" + locale + "]"; - } - /** - * Remove all registered {@link TimeUnit} instances. Returns all {@link TimeUnit} instances that were removed. + * Converts the given {@link LocalDateTime} to the reference {@link Instant} using the system default {@link ZoneId}. + * If null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} + * as the reference {@link Instant}. + *

+ * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a + * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, + * the format command will produce a {@link String} that is in the future tense. + * + * @see #setReference(Instant) */ - public List clearUnits() + protected PrettyTime setReference(final LocalDateTime localDateTime) { - List result = getUnits(); - cachedUnits = null; - units.clear(); - return result; + return setReference(localDateTime, ZoneId.systemDefault()); } - /* - * Internal methods. + /** + * Converts the given {@link LocalDateTime} to the reference {@link Instant} using the given {@link ZoneId}. If + * null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} as + * the reference {@link Instant}. + *

+ * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a + * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, + * the format command will produce a {@link String} that is in the future tense. + * + * @see #setReference(Instant) */ - private Date now() + protected PrettyTime setReference(final LocalDateTime localDateTime, final ZoneId zoneId) { - return new Date(); - } - - private void initTimeUnits() - { - addUnit(new JustNow()); - addUnit(new Millisecond()); - addUnit(new Second()); - addUnit(new Minute()); - addUnit(new Hour()); - addUnit(new Day()); - addUnit(new Week()); - addUnit(new Month()); - addUnit(new Year()); - addUnit(new Decade()); - addUnit(new Century()); - addUnit(new Millennium()); - } - - private void addUnit(ResourcesTimeUnit unit) - { - registerUnit(unit, new ResourcesTimeFormat(unit, overrideResourceBundle)); + return setReference(localDateTime != null ? localDateTime.atZone(zoneId).toInstant() : null); } - private Duration calculateDuration(final long difference) + /** + * Converts the given {@link LocalDate} to the reference {@link Instant} using the given {@link ZoneId}. If + * null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} as + * the reference {@link Instant}. + *

+ * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a + * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, + * the format command will produce a {@link String} that is in the future tense. + * + * @see #setReference(Instant) + */ + protected PrettyTime setReference(final LocalDate localDate) { - long absoluteDifference = Math.abs(difference); - - /* - * Required for thread-safety - */ - List localUnits = getUnits(); - - DurationImpl result = new DurationImpl(); - - for (int i = 0; i < localUnits.size(); i++) { - TimeUnit unit = localUnits.get(i); - long millisPerUnit = Math.abs(unit.getMillisPerUnit()); - long quantity = Math.abs(unit.getMaxQuantity()); - - boolean isLastUnit = (i == localUnits.size() - 1); - - if ((0 == quantity) && !isLastUnit) { - quantity = localUnits.get(i + 1).getMillisPerUnit() / unit.getMillisPerUnit(); - } - - /* - * Does our unit encompass the time duration? - */ - if ((millisPerUnit * quantity > absoluteDifference) || isLastUnit) { - result.setUnit(unit); - if (millisPerUnit > absoluteDifference) { - result.setQuantity(getSign(difference)); - result.setDelta(0); - } - else { - result.setQuantity(difference / millisPerUnit); - result.setDelta(difference - result.getQuantity() * millisPerUnit); - } - break; - } - - } - return result; + return setReference(localDate != null ? localDate.atStartOfDay() : null); } - private long getSign(final long difference) + /** + * Converts the given {@link LocalDate} to the reference {@link Instant} using the given {@link ZoneId}. If + * null, {@link PrettyTime} will always use the current value of {@link System#currentTimeMillis()} as + * the reference {@link Instant}. + *

+ * If the {@link Instant} formatted is before the reference {@link Instant}, the format command will produce a + * {@link String} that is in the past tense. If the {@link Instant} formatted is after the reference {@link Instant}, + * the format command will produce a {@link String} that is in the future tense. + * + * @see #setReference(Instant) + */ + protected PrettyTime setReference(final LocalDate localDate, final ZoneId zoneId) { - if (0 > difference) { - return -1; - } - else { - return 1; - } + return setReference(localDate != null ? localDate.atStartOfDay(zoneId).toInstant() : null); } } diff --git a/jstl/src/main/java/org/ocpsoft/prettytime/jstl/PrettyTimeTag.java b/jstl/src/main/java/org/ocpsoft/prettytime/jstl/PrettyTimeTag.java index 2d6a62f9..9941b29a 100644 --- a/jstl/src/main/java/org/ocpsoft/prettytime/jstl/PrettyTimeTag.java +++ b/jstl/src/main/java/org/ocpsoft/prettytime/jstl/PrettyTimeTag.java @@ -1,24 +1,34 @@ package org.ocpsoft.prettytime.jstl; -import org.apache.taglibs.standard.tag.common.fmt.SetLocaleSupport; -import org.ocpsoft.prettytime.PrettyTime; +import java.io.IOException; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.SimpleTagSupport; -import java.io.IOException; -import java.util.Date; -import java.util.Locale; + +import org.apache.taglibs.standard.tag.common.fmt.SetLocaleSupport; +import org.ocpsoft.prettytime.PrettyTime; /** * Custom tag to pretty print {@link java.util.Date} objects using prettytime. */ public class PrettyTimeTag extends SimpleTagSupport { - /** - * The pretty time object. - */ - private PrettyTime prettyTime; + private static final int MAX_CACHE_SIZE = 20; + + // Cache PrettyTime per locale. LRU cache to prevent memory leak. + private static final Map PRETTY_TIME_LOCALE_MAP = new LinkedHashMap(MAX_CACHE_SIZE + 1, 1.1F, true) { + private static final long serialVersionUID = 5093634937930600141L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_CACHE_SIZE; + } + }; /** * The date to pretty print. @@ -31,23 +41,25 @@ public class PrettyTimeTag extends SimpleTagSupport { private String locale; public PrettyTimeTag() { - prettyTime = new PrettyTime(); } @Override public void doTag() throws JspException, IOException { + final Locale thisLocale; if (locale != null) { - Locale locale = SetLocaleSupport.parseLocale(this.locale); - prettyTime.setLocale(locale); + thisLocale = SetLocaleSupport.parseLocale(this.locale); + } else { + thisLocale = Locale.getDefault(); } + PrettyTime prettyTime = PRETTY_TIME_LOCALE_MAP.computeIfAbsent(thisLocale, PrettyTime::new); JspWriter out = getJspContext().getOut(); out.print(prettyTime.format(date)); } /* - * setters for tag attributes - */ + * setters for tag attributes + */ public void setDate(Date date) { this.date = date;