001/* 002 * PermissionsEx 003 * Copyright (C) zml and PermissionsEx contributors 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package ca.stellardrift.permissionsex.impl.context; 018 019import org.checkerframework.checker.nullness.qual.Nullable; 020 021import java.time.Instant; 022import java.time.ZoneId; 023import java.time.ZonedDateTime; 024import java.time.format.DateTimeFormatter; 025import java.time.format.DateTimeParseException; 026import java.time.temporal.ChronoUnit; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030/** 031 * Parsers to resolve a time from user input. 032 */ 033public interface TimeContextParser { 034 035 /** 036 * Get a list of parse 037 * @param zone the time zone to parse in 038 * @return parser candidates 039 */ 040 static TimeContextParser[] parsersForZone(final ZoneId zone) { 041 return new TimeContextParser[] { 042 new ByDateTimeFormatter(DateTimeFormatter.ISO_DATE_TIME.withZone(zone)), 043 new ByDateTimeFormatter(DateTimeFormatter.ISO_TIME.withZone(zone)), 044 new ByDateTimeFormatter(DateTimeFormatter.ISO_DATE.withZone(zone)), 045 new ByDateTimeFormatter(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(zone)), 046 new Relative(zone), 047 new ByEpochTime(zone) 048 }; 049 } 050 051 @Nullable ZonedDateTime parse(final String input); 052 053 /** 054 * Attemp to parse using an existing {@link DateTimeFormatter}. 055 */ 056 final class ByDateTimeFormatter implements TimeContextParser { 057 private final DateTimeFormatter formatter; 058 059 public ByDateTimeFormatter(final DateTimeFormatter formatter) { 060 this.formatter = formatter; 061 } 062 063 @Override 064 public @Nullable ZonedDateTime parse(final String input) { 065 try { 066 return ZonedDateTime.parse(input, this.formatter); 067 } catch (final DateTimeParseException ex) { 068 return null; 069 } 070 } 071 } 072 073 /** 074 * Given a second since the epoch, create a time in the local time zone. 075 */ 076 final class ByEpochTime implements TimeContextParser { 077 private final ZoneId zone; 078 079 public ByEpochTime(final ZoneId zone) { 080 this.zone = zone; 081 } 082 083 @Override 084 public @Nullable ZonedDateTime parse(final String input) { 085 try { 086 return ZonedDateTime.ofInstant(Instant.ofEpochSecond(Long.parseLong(input)), this.zone); 087 } catch (final NumberFormatException ex) { 088 return null; 089 } 090 } 091 } 092 093 /** 094 * Parse a time using relative time syntax. 095 */ 096 final class Relative implements TimeContextParser { 097 private static final Pattern RELATIVE_TIME_PART = Pattern.compile("^(((?:\\+|-)?)[0-9\\.]*[0-9])(seconds?|minutes?|hours?|days?|weeks?|months?|years?|s|m|h|d|w|y)"); 098 private final ZoneId zone; 099 100 public Relative(final ZoneId zone) { 101 this.zone = zone; 102 } 103 104 private static ChronoUnit unit(final String spec) { 105 switch (spec) { 106 case "second": 107 case "seconds": 108 case "s": 109 return ChronoUnit.SECONDS; 110 case "minute": 111 case "minutes": 112 case "m": 113 return ChronoUnit.MINUTES; 114 case "hour": 115 case "hours": 116 case "h": 117 return ChronoUnit.HOURS; 118 case "day": 119 case "days": 120 case "d": 121 return ChronoUnit.DAYS; 122 case "week": 123 case "weeks": 124 case "w": 125 return ChronoUnit.WEEKS; 126 case "month": 127 case "months": 128 return ChronoUnit.MONTHS; 129 case "year": 130 case "years": 131 case "y": 132 return ChronoUnit.YEARS; 133 // This shouldn't be reached -- it can't be matched in the regex 134 default: throw new IllegalStateException("Hit datetime opcode which was unspecified"); 135 } 136 } 137 138 @Override 139 public @Nullable ZonedDateTime parse(final String input) { 140 if (input.length() < 3) { 141 // This will never result in a successful parse 142 return null; 143 } 144 145 // validate the expression begins with either + or - 146 final char initial = input.charAt(0); 147 if (initial != '+' && initial != '-') { 148 // not a relative time 149 return null; 150 } 151 152 ZonedDateTime working = ZonedDateTime.now(zone); 153 final Matcher match = RELATIVE_TIME_PART.matcher(input); 154 int index = 0; 155 boolean positive = true; 156 while (match.find(index)) { 157 positive = !"-".equals(match.group(1)); // two options are plus or minus, no value is interpreted as a positive 158 159 long quantity; 160 try { 161 quantity = Long.parseLong(match.group(2)); 162 } catch (final NumberFormatException ex) { 163 // its possible the user can enter an extremely large number not representable by a long - do _not_ 164 // change this into an exception! 165 return null; 166 } 167 168 if (!positive) { 169 quantity = -quantity; 170 } 171 172 final ChronoUnit unit = unit(match.group(3)); 173 working = working.plus(quantity, unit); 174 index += match.group().length(); 175 } 176 177 if (index == input.length()) { // if we matched the whole string 178 return working; 179 } 180 return null; 181 } 182 } 183}