Skip to content

Commit

Permalink
Enables support for java.time field types
Browse files Browse the repository at this point in the history
This commit facilitates the automatic conversion of `java.time.temporal.Temporal` instances into `java.util.Date` within the report generation process. This conversion is necessary to ensure proper recognition of date cells by Excel.
  • Loading branch information
hprange committed Oct 12, 2023
1 parent b55cf81 commit d13d315
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 33 deletions.
58 changes: 58 additions & 0 deletions src/main/java/com/woreports/custom/TemporalToDateConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.woreports.custom;

import java.sql.Date;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.Temporal;
import java.util.Map;
import java.util.NoSuchElementException;

import ar.com.fdvs.dj.domain.CustomExpression;

/**
* @author <a href="mailto:hprange@gmail.com">Henrique Prange</a>
*/
public class TemporalToDateConverter implements CustomExpression {
private final String fieldName;

public TemporalToDateConverter(String fieldName) {
this.fieldName = fieldName;
}

@Override
@SuppressWarnings("rawtypes")
public Object evaluate(Map fields, Map variables, Map parameters) {
if (!fields.containsKey(fieldName)) {
throw new NoSuchElementException(String.format("There's no field '%s' in this report.", fieldName));
}

Object object = fields.get(fieldName);

if (object == null) {
return null;
}

if (!(object instanceof Temporal)) {
throw new IllegalStateException(String.format("Expecting a %s, but got a %s.", Temporal.class.getName(), object.getClass().getName()));
}

Temporal temporal = null;

if (object instanceof LocalDate) {
temporal = ((LocalDate) object).atStartOfDay(ZoneId.systemDefault());
} else if (object instanceof LocalDateTime) {
temporal = ((LocalDateTime) object).atZone(ZoneId.systemDefault());
} else {
temporal = (Temporal) object;
}

return Date.from(Instant.from(temporal));
}

@Override
public String getClassName() {
return Date.class.getName();
}
}
94 changes: 61 additions & 33 deletions src/main/java/com/woreports/jasper/JasperModelFiller.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.Map;

Expand All @@ -20,6 +23,7 @@
import com.woreports.api.ReportModel;
import com.woreports.api.ReportProcessingException;
import com.woreports.custom.JasperReportColumnCustomizer;
import com.woreports.custom.TemporalToDateConverter;
import com.woreports.localization.LocalizerKeyGenerator;

import ar.com.fdvs.dj.core.DynamicJasperHelper;
Expand Down Expand Up @@ -48,6 +52,42 @@
* @author <a href="mailto:hprange@gmail.com">Henrique Prange</a>
*/
public class JasperModelFiller implements JasperFiller {
/**
* Checks if the given class name is a temporal class, meaning that it represents a date, time, or duration using
* java.time.
*
* @param classname
* The class name to check.
* @return True if the class name is a temporal class, false otherwise.
*/
private static boolean isTemporal(String classname) {
return classname.startsWith("java.time");
}

/**
* Creates a custom expression for the given report column.
*
* @param column
* The report column.
* @return A custom expression for the report column, or null if the column does not have a custom expression.
* @throws ReportProcessingException
* If an error occurs while creating the custom expression.
*/
private static CustomExpression customExpressionFor(ReportColumn column) throws ReportProcessingException {
Class<? extends CustomExpression> customExpressionClass = column.customExpressionClass();

if (customExpressionClass == null) {
return null;
}

try {
Constructor<? extends CustomExpression> constructor = customExpressionClass.getConstructor(String.class);
return constructor.newInstance(column.keypath());
} catch (Exception exception) {
throw new ReportProcessingException(exception);
}
}

private final DynamicReportBuilder builder;
private final Format format;
private final ERXLocalizer localizer;
Expand Down Expand Up @@ -75,14 +115,16 @@ public JasperModelFiller(Provider<Style> styleProvider, ERXLocalizer localizer,

if (NSTimestamp.class.getName().equals(classname)) {
classname = Date.class.getName();
}

if (ERXCryptoString.class.getSimpleName().equals(classname)) {
} else if (ERXCryptoString.class.getSimpleName().equals(classname)) {
classname = String.class.getName();
}

if (LocalDate.class.getSimpleName().equals(classname)) {
} else if (LocalDate.class.getSimpleName().equals(classname)) {
classname = LocalDate.class.getName();
} else if (LocalDateTime.class.getSimpleName().equals(classname)) {
classname = LocalDateTime.class.getName();
} else if (OffsetDateTime.class.getSimpleName().equals(classname)) {
classname = OffsetDateTime.class.getName();
} else if (ZonedDateTime.class.getSimpleName().equals(classname)) {
classname = ZonedDateTime.class.getName();
}

String columnTitle = titleForColumn(entity, column);
Expand All @@ -99,7 +141,19 @@ public JasperModelFiller(Provider<Style> styleProvider, ERXLocalizer localizer,
}

processColumnStyle(column, columnBuilder, classname);
processCustomExpression(model, column, columnBuilder, builder);

CustomExpression customExpression = customExpressionFor(column);

if (customExpression == null && isTemporal(classname)) {
customExpression = new TemporalToDateConverter(column.keypath());
}

if (customExpression != null) {
columnBuilder.setColumnProperty(null);
columnBuilder.setCustomExpression(customExpression);

builder.addField(column.keypath(), classname);
}

AbstractColumn djColumn = null;

Expand Down Expand Up @@ -207,32 +261,6 @@ private void processColumnStyle(ReportColumn column, ColumnBuilder columnBuilder
columnBuilder.setStyle(style);
}

private void processCustomExpression(ReportModel model, ReportColumn column, ColumnBuilder columnBuilder, DynamicReportBuilder reportBuilder) throws ReportProcessingException {
Class<? extends CustomExpression> customExpressionClass = column.customExpressionClass();

if (customExpressionClass == null) {
return;
}

CustomExpression customExpression = null;

try {
Constructor<? extends CustomExpression> constructor = customExpressionClass.getConstructor(String.class);
customExpression = constructor.newInstance(column.keypath());
} catch (Exception exception) {
throw new ReportProcessingException(exception);
}

columnBuilder.setColumnProperty(null);
columnBuilder.setCustomExpression(customExpression);

EOEntity entity = model.baseEntity();

String classname = entity._attributeForPath(column.keypath()).className();

reportBuilder.addField(column.keypath(), classname);
}

private String titleForColumn(EOEntity entity, ReportColumn column) {
String key = column.title();

Expand Down
177 changes: 177 additions & 0 deletions src/test/java/com/woreports/custom/TestTemporalToDateConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.woreports.custom;

import static java.util.Collections.emptyMap;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

/**
* @author <a href="mailto:hprange@gmail.com">Henrique Prange</a>
*/
public class TestTemporalToDateConverter {
private static int minute(Calendar calendar) {
return calendar.get(Calendar.MINUTE);
}

private static int hour(Calendar calendar) {
return calendar.get(Calendar.HOUR_OF_DAY);
}

private static int day(Calendar calendar) {
return calendar.get(Calendar.DAY_OF_MONTH);
}

private static int month(Calendar calendar) {
return calendar.get(Calendar.MONTH) + 1;
}

private static int year(Calendar calendar) {
return calendar.get(Calendar.YEAR);
}

private static int second(Calendar calendar) {
return calendar.get(Calendar.SECOND);
}

private TemporalToDateConverter converter;

@Rule
public ExpectedException thrown = ExpectedException.none();

@Before
public void setup() {
converter = new TemporalToDateConverter("fieldName");
}

@Test
public void convertLocalDateToDateWhenEvaluatingExpression() {
Map<String, Object> fields = new HashMap<>();
fields.put("fieldName", LocalDate.of(2021, 11, 4));

Object result = converter.evaluate(fields, emptyMap(), emptyMap());

assertThat(result, instanceOf(Date.class));

Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) result);

assertThat(year(calendar), is(2021));
assertThat(month(calendar), is(11));
assertThat(day(calendar), is(4));
assertThat(hour(calendar), is(0));
assertThat(minute(calendar), is(0));
assertThat(second(calendar), is(0));
}

@Test
public void convertLocalDateTimeToDateWhenEvaluatingExpression() {
Map<String, Object> fields = new HashMap<>();
fields.put("fieldName", LocalDateTime.of(2021, 11, 4, 10, 56, 24));

Object result = converter.evaluate(fields, emptyMap(), emptyMap());

assertThat(result, instanceOf(Date.class));

Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) result);

assertThat(year(calendar), is(2021));
assertThat(month(calendar), is(11));
assertThat(day(calendar), is(4));
assertThat(hour(calendar), is(10));
assertThat(minute(calendar), is(56));
assertThat(second(calendar), is(24));
}

@Test
public void convertOffsetDateTimeToDateWhenEvaluatingExpression() {
Map<String, Object> fields = new HashMap<>();
ZoneOffset offset = ZoneId.systemDefault().getRules().getOffset(Instant.now());
fields.put("fieldName", OffsetDateTime.of(2021, 11, 4, 10, 56, 24, 0, offset));

Object result = converter.evaluate(fields, emptyMap(), emptyMap());

assertThat(result, instanceOf(Date.class));

Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) result);

assertThat(year(calendar), is(2021));
assertThat(month(calendar), is(11));
assertThat(day(calendar), is(4));
assertThat(hour(calendar), is(10));
assertThat(minute(calendar), is(56));
assertThat(second(calendar), is(24));
}

@Test
public void convertZonedDateTimeToDateWhenEvaluatingExpression() {
Map<String, Object> fields = new HashMap<>();
fields.put("fieldName", ZonedDateTime.of(2021, 11, 4, 10, 56, 24, 0, ZoneId.systemDefault()));

Object result = converter.evaluate(fields, emptyMap(), emptyMap());

assertThat(result, instanceOf(Date.class));

Calendar calendar = Calendar.getInstance();
calendar.setTime((Date) result);

assertThat(year(calendar), is(2021));
assertThat(month(calendar), is(11));
assertThat(day(calendar), is(4));
assertThat(hour(calendar), is(10));
assertThat(minute(calendar), is(56));
assertThat(second(calendar), is(24));
}

@Test
public void returnNullIfValueNullWhenEvalutatingExpression() throws Exception {
Map<String, Object> fields = new HashMap<String, Object>();
fields.put("fieldName", null);

Object result = converter.evaluate(fields, emptyMap(), emptyMap());

assertThat(result, nullValue());
}

@Test
public void throwExceptionIfFieldNotFoundWhenEvaluatingExpression() throws Exception {
Map<String, Object> fields = new HashMap<>();
fields.put("unknown", LocalDate.of(2021, 11, 4));

thrown.expect(NoSuchElementException.class);
thrown.expectMessage(is("There's no field 'fieldName' in this report."));

converter.evaluate(fields, emptyMap(), emptyMap());
}

@Test
public void throwExceptionIfValueIsNotLocalDateWhenEvaluatingExpression() throws Exception {
Map<String, Object> fields = new HashMap<>();
fields.put("fieldName", "I'm a String");

thrown.expect(IllegalStateException.class);
thrown.expectMessage(is("Expecting a java.time.temporal.Temporal, but got a java.lang.String."));

converter.evaluate(fields, emptyMap(), emptyMap());
}
}

0 comments on commit d13d315

Please sign in to comment.