Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create StartDate and EndDate types [DHIS2-16019] #17454

Merged
merged 16 commits into from
May 22, 2024
Merged
32 changes: 30 additions & 2 deletions dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
Expand Down Expand Up @@ -109,6 +111,9 @@ public class DateUtils {
ObjectArrays.concat(
SUPPORTED_DATE_ONLY_PARSERS, SUPPORTED_DATE_TIME_FORMAT_PARSERS, DateTimeParser.class);

private static final DateTimeFormatter ONLY_DATE_FORMATTER =
new DateTimeFormatterBuilder().append(null, SUPPORTED_DATE_ONLY_PARSERS).toFormatter();

private static final DateTimeFormatter DATE_FORMATTER =
new DateTimeFormatterBuilder().append(null, SUPPORTED_DATE_FORMAT_PARSERS).toFormatter();

Expand Down Expand Up @@ -662,8 +667,8 @@ public static String getPrettyInterval(Date start, Date end) {
}

/**
* Parses the given string into a Date using the supported date formats. Returns null if the
* string cannot be parsed.
* Parses the given string into a Date using the supported date formats. Add time at the beginning
* of the day if no time was provided. Returns null if the string cannot be parsed.
*
* @param dateString the date string.
* @return a date.
Expand All @@ -672,6 +677,29 @@ public static Date parseDate(String dateString) {
return safeParseDateTime(dateString, DATE_FORMATTER);
}

/**
* Parses the given string into a Date using the supported date formats. Add time at the end of
* the day if no time was provided. Returns null if the string cannot be parsed.
*
* @param dateString the date string.
* @return a date.
*/
public static Date parseDateEndOfTheDay(String dateString) {
if (StringUtils.isEmpty(dateString)) {
return null;
}

try {
Date date = safeParseDateTime(dateString, ONLY_DATE_FORMATTER);
LocalDateTime localDateTime =
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).with(LocalTime.MAX);
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
} catch (IllegalArgumentException e) {
// dateString has time defined
teleivo marked this conversation as resolved.
Show resolved Hide resolved
}
return safeParseDateTime(dateString, DATE_FORMATTER);
}

/**
* Parses the given string into a Date using the supported date formats. Returns null if the
* string cannot be parsed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import org.hisp.dhis.webapi.controller.tracker.view.Event;
import org.hisp.dhis.webapi.controller.tracker.view.TrackedEntity;
import org.hisp.dhis.webapi.controller.tracker.view.User;
import org.hisp.dhis.webapi.webdomain.EndDate;
import org.hisp.dhis.webapi.webdomain.StartDate;

/**
* Represents query parameters sent to {@link EventsExportController}.
Expand Down Expand Up @@ -139,9 +141,9 @@ public class EventRequestParams implements PageRequestParams {

private Date scheduledBefore;

private Date updatedAfter;
private StartDate updatedAfter;
teleivo marked this conversation as resolved.
Show resolved Hide resolved

private Date updatedBefore;
private EndDate updatedBefore;

private String updatedWithin;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,14 @@ public EventOperationParams map(EventRequestParams eventRequestParams)
.occurredBefore(eventRequestParams.getOccurredBefore())
.scheduledAfter(eventRequestParams.getScheduledAfter())
.scheduledBefore(eventRequestParams.getScheduledBefore())
.updatedAfter(eventRequestParams.getUpdatedAfter())
.updatedBefore(eventRequestParams.getUpdatedBefore())
.updatedAfter(
eventRequestParams.getUpdatedAfter() != null
? eventRequestParams.getUpdatedAfter().getDate()
: null)
jbee marked this conversation as resolved.
Show resolved Hide resolved
.updatedBefore(
eventRequestParams.getUpdatedBefore() != null
? eventRequestParams.getUpdatedBefore().getDate()
: null)
.updatedWithin(eventRequestParams.getUpdatedWithin())
.enrollmentEnrolledBefore(eventRequestParams.getEnrollmentEnrolledBefore())
.enrollmentEnrolledAfter(eventRequestParams.getEnrollmentEnrolledAfter())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2004-2024, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of the HISP project nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.webapi.webdomain;

import java.util.Date;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.hisp.dhis.util.DateUtils;

/**
* EndDate represents an upper limit date used to filter results in search APIs.
*
* <p>EndDate accepts date and time to be defined. If no time is defined, then the time at the end
* of the day is used by default.
*
* <p>This behavior, combined with {@link StartDate}, allows to correctly implement an interval
* search including start and end dates. startDate=2020-10-10&endDate=2020-10-12 will include
* anything between 2020-10-10T00:00:00.000 and 2020-10-12T23:59:59.999.
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
enricocolasante marked this conversation as resolved.
Show resolved Hide resolved
public class EndDate {
private final Date date;

public static EndDate valueOf(String date) {
return new EndDate(DateUtils.parseDateEndOfTheDay(date));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2004-2024, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of the HISP project nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.webapi.webdomain;

import java.util.Date;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.hisp.dhis.util.DateUtils;

/**
* StartDate represents a lower limit date used to filter results in search APIs.
*
* <p>StartDate accepts date and time to be defined. If no time is defined, then the time at the
* beginning of the day is used by default.
*
* <p>This behavior, combined with {@link EndDate}, allows to correctly implement an interval search
* including start and end dates. startDate=2020-10-10&endDate=2020-10-12 will include anything
* between 2020-10-10T00:00:00.000 and 2020-10-12T23:59:59.999.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we leave

startDate=2020-10-10&endDate=2020-10-12 will include anything between 2020-10-10T00:00:00.000 and 2020-10-12T23:59:59.999.

from these docs here as this is at the discretion of the endpoint itself?

Or do we "standardize" on this behavior, in that case it makes sense to keep it here. I am not sure its 100% clear right now with include anything between that the endpoints are included as well. So maybe we could change the wording or add (represent a closed interval [startDate, endDate].

*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class StartDate {
private final Date date;

public static StartDate valueOf(String date) {
return new StartDate(DateUtils.parseDate(date));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ class UIDBindingTest {

@BeforeEach
public void setUp() {
mockMvc =
MockMvcBuilders.standaloneSetup(new UIDController())
.setControllerAdvice(new CrudControllerAdvice())
.build();
mockMvc = MockMvcBuilders.standaloneSetup(new UIDController()).build();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2004-2022, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of the HISP project nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.webapi.controller;

import static org.hamcrest.core.StringContains.containsString;
enricocolasante marked this conversation as resolved.
Show resolved Hide resolved
import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.ok;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

import java.util.Date;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hisp.dhis.dxf2.webmessage.WebMessage;
import org.hisp.dhis.util.DateUtils;
import org.hisp.dhis.webapi.webdomain.EndDate;
import org.hisp.dhis.webapi.webdomain.StartDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.stereotype.Controller;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

class DatesBindingTest {
private static final String ENDPOINT = "/binding";

private MockMvc mockMvc;

@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new BindingController()).build();
enricocolasante marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
void shouldReturnADateAtTheEndOfTheDayWhenAnEndDateIsPassedWithoutTime() throws Exception {
mockMvc
.perform(get(ENDPOINT).param("endDate", "2001-06-17"))
.andExpect(content().string(containsString("OK")))
.andExpect(content().string(containsString("2001-06-17T23:59:59.999")));
}

@Test
void shouldReturnADateWithTimeWhenAnEndDateIsPassedWithTime() throws Exception {
mockMvc
.perform(get(ENDPOINT).param("endDate", "2001-06-17T16:45:34"))
.andExpect(content().string(containsString("OK")))
.andExpect(content().string(containsString("2001-06-17T16:45:34")));
}

@Test
void shouldReturnADateAtTheStartOfTheDayWhenAnStartDateIsPassedWithoutTime() throws Exception {
mockMvc
.perform(get(ENDPOINT).param("startDate", "2001-06-17"))
.andExpect(content().string(containsString("OK")))
.andExpect(content().string(containsString("2001-06-17T00:00:00.000")));
}

@Test
void shouldReturnADateWithTimeWhenAnStartDateIsPassedWithTime() throws Exception {
mockMvc
.perform(get(ENDPOINT).param("startDate", "2001-06-17T16:45:34"))
.andExpect(content().string(containsString("OK")))
.andExpect(content().string(containsString("2001-06-17T16:45:34")));
}

@Controller
private class BindingController {
Fixed Show fixed Hide fixed
@GetMapping(value = ENDPOINT)
public @ResponseBody WebMessage getDefault(Criteria criteria) {
Date startDate = criteria.getStartDate() == null ? null : criteria.getStartDate().getDate();
Date endDate = criteria.getEndDate() == null ? null : criteria.getEndDate().getDate();
return ok(DateUtils.toIso8601NoTz(startDate) + " - " + DateUtils.toIso8601NoTz(endDate));
}
}

@NoArgsConstructor
@Data
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

Check notice

Code scanning / CodeQL

Use of default toString() Note test

Default toString(): EndDateTime inherits toString() from Object, and so is not suitable for printing.
Default toString(): StartDateTime inherits toString() from Object, and so is not suitable for printing.
private class Criteria {
Fixed Show fixed Hide fixed
private StartDate startDate;

private EndDate endDate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@ class OrderBindingTest {

@BeforeEach
public void setUp() {
mockMvc =
MockMvcBuilders.standaloneSetup(new OrderingController())
.setControllerAdvice(new CrudControllerAdvice())
.build();
mockMvc = MockMvcBuilders.standaloneSetup(new OrderingController()).build();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ class SpringBindingTest {

@BeforeEach
public void setUp() {
mockMvc =
MockMvcBuilders.standaloneSetup(new BindingController())
.setControllerAdvice(new CrudControllerAdvice())
.build();
mockMvc = MockMvcBuilders.standaloneSetup(new BindingController()).build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is enum related code in the CrudControllerAdvice. Not sure what it does but I wonder if CrudControllerAdvice should stay in here or the code in CrudControllerAdvice relating enums should be removed if it has no influence. Either way maybe do this in a separate PR.

}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
import org.hisp.dhis.user.User;
import org.hisp.dhis.user.UserService;
import org.hisp.dhis.webapi.controller.event.webrequest.OrderCriteria;
import org.hisp.dhis.webapi.webdomain.EndDate;
import org.hisp.dhis.webapi.webdomain.StartDate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand Down Expand Up @@ -262,15 +264,15 @@ void testMappingScheduledAfterBefore() throws BadRequestException {
void shouldMapAfterAndBeforeDatesWhenSupplied() throws BadRequestException {
EventRequestParams eventRequestParams = new EventRequestParams();

Date updatedAfter = parseDate("2022-01-01");
StartDate updatedAfter = StartDate.valueOf("2022-01-01");
eventRequestParams.setUpdatedAfter(updatedAfter);
Date updatedBefore = parseDate("2022-09-12");
EndDate updatedBefore = EndDate.valueOf("2022-09-12");
eventRequestParams.setUpdatedBefore(updatedBefore);

EventOperationParams params = mapper.map(eventRequestParams);

assertEquals(updatedAfter, params.getUpdatedAfter());
assertEquals(updatedBefore, params.getUpdatedBefore());
assertEquals(updatedAfter.getDate(), params.getUpdatedAfter());
assertEquals(updatedBefore.getDate(), params.getUpdatedBefore());
}

@Test
Expand All @@ -288,9 +290,9 @@ void shouldMapUpdatedWithinDateWhenSupplied() throws BadRequestException {
void shouldFailWithBadRequestExceptionWhenTryingToMapAllUpdateDatesTogether() {
EventRequestParams eventRequestParams = new EventRequestParams();

Date updatedAfter = parseDate("2022-01-01");
StartDate updatedAfter = StartDate.valueOf("2022-01-01");
eventRequestParams.setUpdatedAfter(updatedAfter);
Date updatedBefore = parseDate("2022-09-12");
EndDate updatedBefore = EndDate.valueOf("2022-09-12");
eventRequestParams.setUpdatedBefore(updatedBefore);
String updatedWithin = "P6M";
eventRequestParams.setUpdatedWithin(updatedWithin);
Expand Down
Loading