• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  *      https://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  * See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 package com.networknt.schema.format;
15 
16 import java.text.ParsePosition;
17 import java.time.DateTimeException;
18 import java.time.format.DateTimeFormatter;
19 import java.time.format.DateTimeFormatterBuilder;
20 import java.time.temporal.TemporalAccessor;
21 
22 import com.networknt.schema.ExecutionContext;
23 import com.networknt.schema.Format;
24 
25 import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
26 import static java.time.temporal.ChronoField.*;
27 
28 /**
29  * Format for time.
30  * <p>
31  * Validates that a value conforms to the time specification in RFC 3339.
32  */
33 public class TimeFormat implements Format {
34     // In 2023, time-zone offsets around the world extend from -12:00 to +14:00.
35     // However, RFC 3339 accepts -23:59 to +23:59.
36     private static final long MAX_OFFSET_MIN = 24 * 60 - 1;
37     private static final long MIN_OFFSET_MIN = -MAX_OFFSET_MIN;
38 
39     private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
40             .parseCaseInsensitive()
41             .append(ISO_LOCAL_TIME)
42             .appendOffset("+HH:MM", "Z")
43             .parseLenient()
44             .toFormatter();
45 
46     @Override
matches(ExecutionContext executionContext, String value)47     public boolean matches(ExecutionContext executionContext, String value) {
48         try {
49             if (null == value) return true;
50 
51             int pos = value.indexOf('Z');
52             if (-1 != pos && pos != value.length() - 1) return false;
53 
54             TemporalAccessor accessor = formatter.parseUnresolved(value, new ParsePosition(0));
55             if (null == accessor) return false;
56 
57             long offset = accessor.getLong(OFFSET_SECONDS) / 60;
58             if (MAX_OFFSET_MIN < offset || MIN_OFFSET_MIN > offset) return false;
59 
60             long hr = accessor.getLong(HOUR_OF_DAY) - offset / 60;
61             long min = accessor.getLong(MINUTE_OF_HOUR) - offset % 60;
62             long sec = accessor.getLong(SECOND_OF_MINUTE);
63 
64             if (min < 0) {
65                 --hr;
66                 min += 60;
67             }
68             if (hr < 0) {
69                 hr += 24;
70             }
71 
72             boolean isStandardTimeRange = (sec <= 59 && min <= 59 && hr <= 23);
73             boolean isSpecialCaseEndOfDay = (sec == 60 && min == 59 && hr == 23);
74 
75             return isStandardTimeRange
76                     || isSpecialCaseEndOfDay;
77 
78         } catch (DateTimeException e) {
79             return false;
80         }
81     }
82 
83     @Override
getName()84     public String getName() {
85         return "time";
86     }
87 
88     @Override
getMessageKey()89     public String getMessageKey() {
90         return "format.time";
91     }
92 }
93