Skip to content

Commit

Permalink
Merge pull request #12128 from mkouba/issue-11181
Browse files Browse the repository at this point in the history
Qute: add basic date-time formatting support
  • Loading branch information
geoand authored Sep 16, 2020
2 parents f32b078 + f6c5655 commit 05a4d9f
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 3 deletions.
24 changes: 22 additions & 2 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -877,11 +877,11 @@ NOTE: A value resolver is also generated for all types used in parameter declara
[[template_extension_methods]]
=== Template Extension Methods

Extension methods can be used to extend the data classes with new functionality or resolve expressions for a specific <<namespace_extension_methods,namespace>>.
Extension methods can be used to extend the data classes with new functionality (to extend the set of accessible properties and methods) or to resolve expressions for a specific <<namespace_extension_methods,namespace>>.
For example, it is possible to add _computed properties_ and _virtual methods_.

A value resolver is automatically generated for a method annotated with `@TemplateExtension`.
If a class is annotated with `@TemplateExtension` then a value resolver is generated for every non-private static method declared on the class.
If a class is annotated with `@TemplateExtension` then a value resolver is generated for every _non-private static method_ declared on the class.
Method-level annotations override the behavior defined on the class.
Methods that do not meet the following requirements are ignored.

Expand Down Expand Up @@ -1038,6 +1038,26 @@ TIP: A list element can be accessed directly: `{list.10}` or `{list[10]}`.
* `config:property(name)`: Returns the config value for the given property name; the name can be obtained dynamically by an expression
** `{config:property('quarkus.foo')}` or `{config:property(foo.getPropertyName())}`

===== Time

* `format(pattern)`: Formats temporal objects from the `java.time` package
** `{dateTime.format('d MMM uuuu')}`

* `format(pattern,locale)`: Formats temporal objects from the `java.time` package
** `{dateTime.format('d MMM uuuu',myLocale)}`

* `format(pattern,locale,timeZone)`: Formats temporal objects from the `java.time` package
** `{dateTime.format('d MMM uuuu',myLocale,myTimeZoneId)}`

* `time:format(dateTime,pattern)`: Formats temporal objects from the `java.time` package, `java.util.Date`, `java.util.Calendar` and `java.lang.Number`
** `{time:format(myDate,'d MMM uuuu')}`

* `time:format(dateTime,pattern,locale)`: Formats temporal objects from the `java.time` package, `java.util.Date`, `java.util.Calendar` and `java.lang.Number`
** `{time:format(myDate,'d MMM uuuu', myLocale)}`

* `time:format(dateTime,pattern,locale,timeZone)`: Formats temporal objects from the `java.time` package, `java.util.Date`, `java.util.Calendar` and `java.lang.Number`
** `{time:format(myDate,'d MMM uuuu',myTimeZoneId)}`

[[template_data]]
=== @TemplateData

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import io.quarkus.qute.runtime.extensions.ConfigTemplateExtensions;
import io.quarkus.qute.runtime.extensions.MapTemplateExtensions;
import io.quarkus.qute.runtime.extensions.NumberTemplateExtensions;
import io.quarkus.qute.runtime.extensions.TimeTemplateExtensions;

public class QuteProcessor {

Expand Down Expand Up @@ -162,7 +163,8 @@ AdditionalBeanBuildItem additionalBeans() {
.setUnremovable()
.addBeanClasses(EngineProducer.class, TemplateProducer.class, ContentTypes.class, ResourcePath.class,
Template.class, TemplateInstance.class, CollectionTemplateExtensions.class,
MapTemplateExtensions.class, NumberTemplateExtensions.class, ConfigTemplateExtensions.class)
MapTemplateExtensions.class, NumberTemplateExtensions.class, ConfigTemplateExtensions.class,
TimeTemplateExtensions.class)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.quarkus.qute.deployment.extensions;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

import javax.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.Template;
import io.quarkus.test.QuarkusUnitTest;

public class TimeTemplateExtensionsTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset(
"{now.format('d uuuu')}:{nowLocalDate.format('d MMM uuuu',myLocale)}:{time:format(nowDate,'d MMM uuuu',myLocale)}:{time:format(nowCalendar,'d uuuu')}"),
"templates/foo.html"));

@Inject
Template foo;

@Test
public void testFormat() {
Calendar nowCal = Calendar.getInstance();
nowCal.set(Calendar.YEAR, 2020);
nowCal.set(Calendar.MONTH, Calendar.SEPTEMBER);
nowCal.set(Calendar.DAY_OF_MONTH, 10);
Date nowDate = nowCal.getTime();
assertEquals("10 2020:10 Sep 2020:10 Sep 2020:10 2020", foo
.data("now", LocalDateTime.of(2020, 9, 10, 11, 12))
.data("nowLocalDate", LocalDate.of(2020, 9, 10))
.data("nowDate", nowDate)
.data("nowCalendar", nowCal)
.data("myLocale", Locale.ENGLISH).render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import java.util.List;

import javax.enterprise.inject.Vetoed;

import io.quarkus.qute.Results.Result;
import io.quarkus.qute.TemplateExtension;

@Vetoed // Make sure no bean is created from this class
@TemplateExtension
public class CollectionTemplateExtensions {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

import java.util.Optional;

import javax.enterprise.inject.Vetoed;

import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkus.qute.Results.Result;
import io.quarkus.qute.TemplateExtension;

@Vetoed // Make sure no bean is created from this class
public class ConfigTemplateExtensions {

static final String CONFIG = "config";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

import java.util.Map;

import javax.enterprise.inject.Vetoed;

import io.quarkus.qute.Results.Result;
import io.quarkus.qute.TemplateExtension;

@Vetoed // Make sure no bean is created from this class
@TemplateExtension
public class MapTemplateExtensions {
@SuppressWarnings({ "rawtypes", "unchecked" })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package io.quarkus.qute.runtime.extensions;

import javax.enterprise.inject.Vetoed;

import io.quarkus.qute.TemplateExtension;

@Vetoed // Make sure no bean is created from this class
@TemplateExtension
public class NumberTemplateExtensions {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package io.quarkus.qute.runtime.extensions;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import javax.enterprise.inject.Vetoed;

import io.quarkus.qute.TemplateExtension;

@Vetoed // Make sure no bean is created from this class
@TemplateExtension
public class TimeTemplateExtensions {

private static final Map<Key, DateTimeFormatter> FORMATTER_CACHE = new ConcurrentHashMap<>();

static String format(TemporalAccessor temporal, String pattern) {
return FORMATTER_CACHE.computeIfAbsent(new Key(pattern, null, null), TimeTemplateExtensions::formatterForKey)
.format(temporal);
}

static String format(TemporalAccessor temporal, String pattern, Locale locale) {
return FORMATTER_CACHE.computeIfAbsent(new Key(pattern, locale, null), TimeTemplateExtensions::formatterForKey)
.format(temporal);
}

static String format(TemporalAccessor temporal, String pattern, Locale locale, ZoneId timeZone) {
return FORMATTER_CACHE.computeIfAbsent(new Key(pattern, locale, timeZone), TimeTemplateExtensions::formatterForKey)
.format(temporal);
}

@TemplateExtension(namespace = "time")
static String format(Object dateTimeObject, String pattern) {
return format(getFormattableObject(dateTimeObject, ZoneId.systemDefault()), pattern);
}

@TemplateExtension(namespace = "time")
static String format(Object dateTimeObject, String pattern, Locale locale) {
return format(getFormattableObject(dateTimeObject, ZoneId.systemDefault()), pattern, locale);
}

@TemplateExtension(namespace = "time")
static String format(Object dateTimeObject, String pattern, Locale locale, ZoneId timeZone) {
return format(getFormattableObject(dateTimeObject, timeZone), pattern, locale, timeZone);
}

private static TemporalAccessor getFormattableObject(Object value,
ZoneId timeZone) {
if (value instanceof TemporalAccessor) {
return (TemporalAccessor) value;
} else if (value instanceof Date) {
return LocalDateTime.ofInstant(((Date) value).toInstant(),
timeZone);
} else if (value instanceof Calendar) {
return LocalDateTime.ofInstant(((Calendar) value).toInstant(),
timeZone);
} else if (value instanceof Number) {
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(((Number) value).longValue()),
timeZone);
} else {
throw new IllegalArgumentException("Not a formattable date/time object: " + value);
}
}

private static DateTimeFormatter formatterForKey(Key key) {
DateTimeFormatter formatter;
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.appendPattern(key.pattern);
if (key.locale != null) {
formatter = builder.toFormatter(key.locale);
} else {
formatter = builder.toFormatter();
}
return key.timeZone != null ? formatter.withZone(key.timeZone) : formatter;
}

static final class Key {

private final String pattern;
private final Locale locale;
private final ZoneId timeZone;

public Key(String pattern, Locale locale, ZoneId timeZone) {
this.pattern = pattern;
this.locale = locale;
this.timeZone = timeZone;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((locale == null) ? 0 : locale.hashCode());
result = prime * result + ((pattern == null) ? 0 : pattern.hashCode());
result = prime * result + ((timeZone == null) ? 0 : timeZone.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Key other = (Key) obj;
return Objects.equals(locale, other.locale) && Objects.equals(pattern, other.pattern)
&& Objects.equals(timeZone, other.timeZone);
}

}

}

0 comments on commit 05a4d9f

Please sign in to comment.