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

Add support for converting a String to an Instant #30312

Closed
JanecekPetr opened this issue Apr 4, 2023 · 14 comments
Closed

Add support for converting a String to an Instant #30312

JanecekPetr opened this issue Apr 4, 2023 · 14 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: superseded An issue that has been superseded by another type: enhancement A general enhancement
Milestone

Comments

@JanecekPetr
Copy link
Contributor

JanecekPetr commented Apr 4, 2023

What I'm trying to achieve:

@ConfigurationProperties
record ApplicationProps(Instant instant) {}

with the following application.properties:

instant = 1680612880417

where the value is millis since the epoch, see Instant.ofEpochMilli().

Edit:
Currently this fails with a

...
Caused by: java.lang.IllegalArgumentException: Parse attempt failed for value [123456789]
	at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:224) ~[spring-context-6.0.6.jar:6.0.6]
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41) ~[spring-core-6.0.6.jar:6.0.6]
	... 117 common frames omitted
Caused by: java.time.format.DateTimeParseException: Text '123456789' could not be parsed at index 0

Perhaps this is a good candidate for a default binding.

Considerations:

  • Why are we choosing Instant.ofEpochMilli() and not Instant.ofEpochSecond()? It just felt more natural. Is this uncertainty reason enough to refuse this request? That said, there already is a registered-by-default Converter<Long, Instant> which uses ofEpochMilli. Therefore, an additional Formatter would be consistent with the already-existing Converter.
  • Do we also need to add more similar formatters for types like java.util.Date or java.sql.Timestamp? I do no think think so as neither of them has a simple ofEpochMilli()-like creator method or clear semantics. Other java.time also are not good targets for this unless OffsetDateTime/ZonedDateTime is considered okay given a default timezone.
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Apr 4, 2023
@snicoll
Copy link
Member

snicoll commented Apr 4, 2023

Why are we choosing Instant.ofEpochMilli() and not Instant.ofEpochSecond()?

Because the usual way of representing a "time" is by using the number of ms, not seconds. I understand that you'd prefer seconds but that's not what the default representation is at the moment: the seconds variant is merely a shortcut for those who don't need ms precision.

Both inputs are numbers so it's impossible for us to know which format you want to use automatically. I can't even suggest to configure a custom converter, unless you want to trump the default one for every fields.

Having said all of that, I am not sure I understand what you'd expect from us. Can you clarify?

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Apr 4, 2023
@JanecekPetr
Copy link
Contributor Author

JanecekPetr commented Apr 4, 2023

I understand that you'd prefer seconds

Sorry, maybe I need to rephrase my question as that was not what I meant. I absolutely prefer millis and I agree. I was just adding considerations and alternatives that someone might ask for instead.

Can you clarify?

Sure! (Should I also edit the original question?)

Currently it is not possible to use a timestamp as a property of type Instant. I'd like Spring to parse that, ideally by default. My suggestion is for you to add this capability to the existing InstantFormatter.

If you try the code I suggested above, it fails with:

...
Caused by: org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.Instant'; Failed to convert from type [java.lang.String] to type [@org.springframework.beans.factory.annotation.Value java.time.Instant] for value '123456789'
	at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:79) ~[spring-beans-6.0.6.jar:6.0.6]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.0.6.jar:6.0.6]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1325) ~[spring-beans-6.0.6.jar:6.0.6]
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:885) ~[spring-beans-6.0.6.jar:6.0.6]
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:789) ~[spring-beans-6.0.6.jar:6.0.6]
	... 110 common frames omitted
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.beans.factory.annotation.Value java.time.Instant] for value '123456789'
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47) ~[spring-core-6.0.6.jar:6.0.6]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192) ~[spring-core-6.0.6.jar:6.0.6]
	at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:129) ~[spring-beans-6.0.6.jar:6.0.6]
	at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73) ~[spring-beans-6.0.6.jar:6.0.6]
	... 114 common frames omitted
Caused by: java.lang.IllegalArgumentException: Parse attempt failed for value [123456789]
	at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:224) ~[spring-context-6.0.6.jar:6.0.6]
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41) ~[spring-core-6.0.6.jar:6.0.6]
	... 117 common frames omitted
Caused by: java.time.format.DateTimeParseException: Text '123456789' could not be parsed at index 0
	at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2052) ~[na:na]
	at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1954) ~[na:na]
	at java.base/java.time.Instant.parse(Instant.java:397) ~[na:na]
	at org.springframework.format.datetime.standard.InstantFormatter.parse(InstantFormatter.java:50) ~[spring-context-6.0.6.jar:6.0.6]
	at org.springframework.format.datetime.standard.InstantFormatter.parse(InstantFormatter.java:40) ~[spring-context-6.0.6.jar:6.0.6]
	at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:218) ~[spring-context-6.0.6.jar:6.0.6]
	... 118 common frames omitted

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 4, 2023
@snicoll snicoll changed the title Consider adding an Instant formatter from timestamp-like properties Add support for binding Instant-based property Apr 4, 2023
@snicoll snicoll added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Apr 4, 2023
@snicoll
Copy link
Member

snicoll commented Apr 4, 2023

Oh. I think the considerations have confused me. Yes, I agree supporting instant would be nice and I don't think we should bother about the seconds variant.

@wilkinsona
Copy link
Member

@snicoll Given the involvement of org.springframework.format.datetime.standard.InstantFormatter, should this be transferred to Framework?

@snicoll
Copy link
Member

snicoll commented Apr 11, 2023

Yes. We do have issues to move some of the converters that we have in Spring Boot already so that makes sense.

@snicoll snicoll transferred this issue from spring-projects/spring-boot Apr 11, 2023
@snicoll snicoll changed the title Add support for binding Instant-based property Add support for converting a String to an Instant Apr 11, 2023
@snicoll snicoll added this to the 6.0.x milestone Apr 11, 2023
@RemusRD
Copy link
Contributor

RemusRD commented May 13, 2023

Hey! Can I take this one? :)

@simonbasle
Copy link
Contributor

@RemusRD I think it is worth a shot yes. From my perspective it should be possible to retrofit this parsing into existing InstantFormatter. I have the following test to suggest in DateTimeFormattingTests.java, but please include other more direct unit tests as well:

	@Test
	void testBindInstantAsLongEpochMillis() {
		MutablePropertyValues propertyValues = new MutablePropertyValues();
		propertyValues.add("instant", 1234L);
		binder.bind(propertyValues);
		assertThat(binder.getBindingResult().getErrorCount()).isZero();
		assertThat(binder.getBindingResult().getRawFieldValue("instant"))
				.isInstanceOf(Instant.class)
				.isEqualTo(Instant.ofEpochMilli(1234L));
		assertThat(binder.getBindingResult().getFieldValue("instant"))
				.hasToString("1970-01-01T00:00:01.234Z");
	}

@simonbasle simonbasle modified the milestones: 6.0.x, 6.1.0-M1 May 16, 2023
@RemusRD
Copy link
Contributor

RemusRD commented May 16, 2023

@RemusRD I think it is worth a shot yes. From my perspective it should be possible to retrofit this parsing into existing InstantFormatter. I have the following test to suggest in DateTimeFormattingTests.java, but please include other more direct unit tests as well:

	@Test
	void testBindInstantAsLongEpochMillis() {
		MutablePropertyValues propertyValues = new MutablePropertyValues();
		propertyValues.add("instant", 1234L);
		binder.bind(propertyValues);
		assertThat(binder.getBindingResult().getErrorCount()).isZero();
		assertThat(binder.getBindingResult().getRawFieldValue("instant"))
				.isInstanceOf(Instant.class)
				.isEqualTo(Instant.ofEpochMilli(1234L));
		assertThat(binder.getBindingResult().getFieldValue("instant"))
				.hasToString("1970-01-01T00:00:01.234Z");
	}

I already started working on this! I added your test which seems to be already passing, should I keep it?

Also, would be great if you could share with me any other merge requests that are already implemented similar to this one so I can take inspiration :) from.

@RemusRD
Copy link
Contributor

RemusRD commented May 17, 2023

I have added more tests and the functionality to the InstantFormatter.java however, I have found if I use RandomInstantProvider.java in my tests, the values provided will overflow the Long.class making my tests fail, any suggestion on how to approach this? I could create a new provider, or limit the current one to be from Long.MIN to Long.Max

@ParameterizedTest
@ArgumentsSource(RandomInstantProvider.class)
void should_parse_into_an_Instant_from_epoch_mili(Instant input) throws ParseException {
    String inputS = String.valueOf(input.toEpochMilli()); // toEpochMilli() will overflow and make the test fail
    Instant expected = Instant.ofEpochMilli(Long.parseLong(inputS));

    Instant actual = instantFormatter.parse(inputS, null);

    assertThat(actual).isEqualTo(expected);
}

@simonbasle
Copy link
Contributor

yeah RandomInstantProvider is not a good fit because it will create Instants that fall off the limit of epochMillis (due to using seconds and not millis). You could take inspiration from it to create a similar provider which uses ofEpochMillis(Long.MIN_VALUE)/ofEpochMilli(Long.MAX_VALUE). I would also advise to truncateTo(ChronoUnit.MILLIS) each generated Instant.

@simonbasle
Copy link
Contributor

any progress @RemusRD ?

@RemusRD
Copy link
Contributor

RemusRD commented May 25, 2023

any progress @RemusRD ?

Submitting the PR shortly, I added tests into the InstantFormatter, wondering if it would make sense to do a more end to end test? Like checking it works from parsing a yaml or something like that.

RemusRD added a commit to RemusRD/spring-framework that referenced this issue May 25, 2023
@RemusRD
Copy link
Contributor

RemusRD commented May 25, 2023

@simonbasle I have opened the PR, but not sure how I can link it here? #30546

@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label May 25, 2023
@simonbasle simonbasle added the status: superseded An issue that has been superseded by another label May 25, 2023
@simonbasle simonbasle removed this from the 6.1.0-M1 milestone May 25, 2023
@simonbasle
Copy link
Contributor

simonbasle commented May 25, 2023

@simonbasle simonbasle self-assigned this May 25, 2023
@simonbasle simonbasle added this to the 6.1.0-M1 milestone May 26, 2023
simonbasle pushed a commit that referenced this issue May 26, 2023
This commit adds support of parsing a simple long from a String and
turning it to an `Instant` by considering it represents a timestamp in
milliseconds (see `Instant.ofEpochMilli`). Failing to parse a long from
the String, the previous algorithm is used: first check for an RFC-1123
representation then an ISO_INSTANT representation.

See gh-30312
Closes gh-30546
mdeinum pushed a commit to mdeinum/spring-framework that referenced this issue Jun 29, 2023
This commit adds support of parsing a simple long from a String and
turning it to an `Instant` by considering it represents a timestamp in
milliseconds (see `Instant.ofEpochMilli`). Failing to parse a long from
the String, the previous algorithm is used: first check for an RFC-1123
representation then an ISO_INSTANT representation.

See spring-projectsgh-30312
Closes spring-projectsgh-30546
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) status: superseded An issue that has been superseded by another type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

7 participants