55 */
66package io .jooby .internal .converter ;
77
8+ import static java .util .concurrent .TimeUnit .MILLISECONDS ;
9+ import static java .util .concurrent .TimeUnit .NANOSECONDS ;
10+
811import java .math .BigDecimal ;
912import java .math .BigInteger ;
1013import java .net .MalformedURLException ;
1518import java .time .*;
1619import java .time .format .DateTimeFormatter ;
1720import java .time .format .DateTimeParseException ;
21+ import java .time .temporal .ChronoUnit ;
1822import java .util .Date ;
1923import java .util .TimeZone ;
2024import java .util .UUID ;
2125import java .util .concurrent .TimeUnit ;
2226
23- import com .typesafe .config .ConfigException ;
24- import com .typesafe .config .ConfigFactory ;
25- import com .typesafe .config .ConfigValueFactory ;
2627import edu .umd .cs .findbugs .annotations .NonNull ;
2728import io .jooby .SneakyThrows ;
2829import io .jooby .StatusCode ;
@@ -101,15 +102,58 @@ public boolean supports(@NonNull Class<?> type) {
101102 try {
102103 return java .time .Duration .parse (value .value ());
103104 } catch (DateTimeParseException x ) {
104- try {
105- long duration =
106- ConfigFactory .empty ()
107- .withValue ("d" , ConfigValueFactory .fromAnyRef (value .value ()))
108- .getDuration ("d" , TimeUnit .MILLISECONDS );
109- return java .time .Duration .ofMillis (duration );
110- } catch (ConfigException ignored ) {
111- throw x ;
105+ var nanos = MILLISECONDS .convert (parseDuration (value .value ()), NANOSECONDS );
106+ return java .time .Duration .ofMillis (nanos );
107+ }
108+ }
109+
110+ /**
111+ * Parses a duration string. If no units are specified in the string, it is assumed to be in
112+ * milliseconds. The returned duration is in nanoseconds. The purpose of this function is to
113+ * implement the duration-related methods in the ConfigObject interface.
114+ *
115+ * @param value the string to parse
116+ * @return duration in nanoseconds
117+ */
118+ private static long parseDuration (String value ) {
119+ var unitString = getUnits (value );
120+ var numberString = value .substring (0 , value .length () - unitString .length ());
121+
122+ // this would be caught later anyway, but the error message
123+ // is more helpful if we check it here.
124+ if (numberString .isEmpty ()) {
125+ throw new DateTimeParseException (
126+ "No number in duration value: '" + numberString + "'" , value , 0 );
127+ }
128+
129+ // note that this is deliberately case-sensitive
130+ var units =
131+ switch (unitString ) {
132+ case "ms" , "milli" , "millis" , "millisecond" , "milliseconds" , "" -> MILLISECONDS ;
133+ case "us" , "micro" , "micros" , "microsecond" , "microseconds" -> TimeUnit .MICROSECONDS ;
134+ case "ns" , "nano" , "nanos" , "nanosecond" , "nanoseconds" -> NANOSECONDS ;
135+ case "s" , "second" , "seconds" -> TimeUnit .SECONDS ;
136+ case "m" , "minute" , "minutes" -> TimeUnit .MINUTES ;
137+ case "h" , "hour" , "hours" -> TimeUnit .HOURS ;
138+ case "d" , "day" , "days" -> TimeUnit .DAYS ;
139+ default ->
140+ throw new DateTimeParseException (
141+ "Could not parse time unit '" + unitString + "'" ,
142+ value ,
143+ value .length () - unitString .length ());
144+ };
145+ try {
146+ // if the string is purely digits, parse as an integer to avoid possible precision loss;
147+ // otherwise as a double.
148+ if (numberString .matches ("[+-]?[0-9]+" )) {
149+ return units .toNanos (Long .parseLong (numberString ));
150+ } else {
151+ long nanosInUnit = units .toNanos (1 );
152+ return (long ) (Double .parseDouble (numberString ) * nanosInUnit );
112153 }
154+ } catch (NumberFormatException e ) {
155+ throw new DateTimeParseException (
156+ "Could not parse duration number '" + numberString + "'" , numberString , 0 );
113157 }
114158 }
115159 },
@@ -121,7 +165,63 @@ public boolean supports(@NonNull Class<?> type) {
121165
122166 @ Override
123167 public @ NonNull Object convert (@ NonNull Value value , @ NonNull Class <?> type ) {
124- return java .time .Period .from ((Duration ) Duration .convert (value , type ));
168+ try {
169+ return java .time .Period .from ((Duration ) Duration .convert (value , type ));
170+ } catch (DateTimeException x ) {
171+ return parsePeriod (value .value ());
172+ }
173+ }
174+
175+ /**
176+ * Parses a period string. If no units are specified in the string, it is assumed to be in days.
177+ * The returned period is in days. The purpose of this function is to implement the
178+ * period-related methods in the ConfigObject interface.
179+ *
180+ * @param value the string to parse path to include in exceptions
181+ * @return duration in days
182+ */
183+ public static Period parsePeriod (String value ) {
184+ var unitString = getUnits (value );
185+ var numberString = value .substring (0 , value .length () - unitString .length ());
186+
187+ // this would be caught later anyway, but the error message
188+ // is more helpful if we check it here.
189+ if (numberString .isEmpty ())
190+ throw new DateTimeParseException (
191+ "No number in period value '" + numberString + "'" , numberString , 0 );
192+
193+ var units =
194+ switch (unitString ) {
195+ case "d" , "day" , "days" , "" -> ChronoUnit .DAYS ;
196+ case "w" , "week" , "weeks" -> ChronoUnit .WEEKS ;
197+ case "m" , "mo" , "month" , "months" -> ChronoUnit .MONTHS ;
198+ case "y" , "year" , "years" -> ChronoUnit .YEARS ;
199+ default ->
200+ throw new DateTimeParseException (
201+ "Could not parse time unit '" + unitString + "' (try d, w, mo, y)" ,
202+ value ,
203+ value .length () - unitString .length ());
204+ };
205+ try {
206+ return periodOf (Integer .parseInt (numberString ), units );
207+ } catch (NumberFormatException e ) {
208+ throw new DateTimeParseException (
209+ "Could not parse duration number '" + numberString + "'" , numberString , 0 );
210+ }
211+ }
212+
213+ private static Period periodOf (int n , ChronoUnit unit ) {
214+ if (unit .isTimeBased ()) {
215+ throw new DateTimeException (unit + " cannot be converted to a java.time.Period" );
216+ }
217+
218+ return switch (unit ) {
219+ case DAYS -> java .time .Period .ofDays (n );
220+ case WEEKS -> java .time .Period .ofWeeks (n );
221+ case MONTHS -> java .time .Period .ofMonths (n );
222+ case YEARS -> java .time .Period .ofYears (n );
223+ default -> throw new DateTimeException (unit + " cannot be converted to a java.time.Period" );
224+ };
125225 }
126226 },
127227 Instant {
@@ -239,4 +339,14 @@ public boolean supports(@NonNull Class<?> type) {
239339 return java .time .ZoneId .of (java .time .ZoneId .SHORT_IDS .getOrDefault (zoneId , zoneId ));
240340 }
241341 };
342+
343+ private static String getUnits (String s ) {
344+ int i = s .length () - 1 ;
345+ while (i >= 0 ) {
346+ char c = s .charAt (i );
347+ if (!Character .isLetter (c )) break ;
348+ i -= 1 ;
349+ }
350+ return s .substring (i + 1 );
351+ }
242352}
0 commit comments