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