001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. 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 org.apache.juneau.marshall; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.math.*; 022import java.time.*; 023import java.util.Locale; 024 025/** 026 * Supported wire formats for {@link Duration} values. 027 */ 028public enum DurationFormat { 029 030 NOT_SET, 031 ISO_8601, 032 ISO_8601_WITH_DAYS, 033 NANOS, 034 MILLIS, 035 SECONDS, 036 HOCON; 037 038 private static final BigDecimal NANOS_PER_SECOND = BigDecimal.valueOf(1_000_000_000L); 039 040 /** 041 * Formats the specified duration using this format. 042 * 043 * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>). 044 * @return The formatted value, or <jk>null</jk> if {@code value} is <jk>null</jk>. 045 */ 046 public String format(Duration value) { 047 if (value == null) 048 return null; 049 return switch (this) { 050 case NOT_SET, ISO_8601 -> value.toString(); 051 case ISO_8601_WITH_DAYS -> formatIso8601WithDays(value); 052 case NANOS -> Long.toString(value.toNanos()); 053 case MILLIS -> Long.toString(value.toMillis()); 054 case SECONDS -> { 055 long seconds = value.getSeconds(); 056 int nanos = value.getNano(); 057 yield seconds + "." + f("%09d", nanos); 058 } 059 case HOCON -> formatHocon(value); 060 }; 061 } 062 063 /** 064 * Parses the specified wire value using this format. 065 * 066 * @param value The value to parse. Can be <jk>null</jk> or blank (returns <jk>null</jk>). 067 * @return The parsed duration, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank. 068 */ 069 public Duration parse(String value) { 070 if (value == null) 071 return null; 072 String s = value.trim(); 073 if (s.isEmpty()) 074 return null; 075 return switch (this) { 076 case NOT_SET, ISO_8601, ISO_8601_WITH_DAYS -> Duration.parse(normalizeIso(s)); 077 case NANOS -> Duration.ofNanos(Long.parseLong(s)); 078 case MILLIS -> Duration.ofMillis(Long.parseLong(s)); 079 case SECONDS -> parseSeconds(s); 080 case HOCON -> parseHocon(s); 081 }; 082 } 083 084 /** 085 * Returns <jk>true</jk> if this format is numeric on the wire. 086 * 087 * @return <jk>true</jk> if this format emits numeric wire values. 088 */ 089 public boolean isNumeric() { 090 return this == NANOS || this == MILLIS || this == SECONDS; 091 } 092 093 private static String normalizeIso(String s) { 094 String x = s; 095 if (x.startsWith("PT-")) 096 x = "-PT" + x.substring(3); 097 return x; 098 } 099 100 private static Duration parseSeconds(String s) { 101 var bd = new BigDecimal(s); 102 long seconds = bd.longValue(); 103 BigDecimal frac = bd.subtract(BigDecimal.valueOf(seconds)); 104 int nanos = frac.multiply(NANOS_PER_SECOND).intValue(); 105 return Duration.ofSeconds(seconds, nanos); 106 } 107 108 @SuppressWarnings({ 109 "java:S3776" // Cognitive complexity acceptable for ISO-8601 duration formatting logic 110 }) 111 private static String formatIso8601WithDays(Duration value) { 112 boolean neg = value.isNegative(); 113 Duration d = neg ? value.abs() : value; 114 long totalSeconds = d.getSeconds(); 115 int nanos = d.getNano(); 116 long days = totalSeconds / 86400; 117 long rem = totalSeconds % 86400; 118 long hours = rem / 3600; 119 rem %= 3600; 120 long minutes = rem / 60; 121 long seconds = rem % 60; 122 123 var sb = new StringBuilder(); 124 if (neg) 125 sb.append('-'); 126 sb.append('P'); 127 if (days > 0) 128 sb.append(days).append('D'); 129 if (hours > 0 || minutes > 0 || seconds > 0 || nanos > 0 || days == 0) { 130 sb.append('T'); 131 if (hours > 0) 132 sb.append(hours).append('H'); 133 if (minutes > 0) 134 sb.append(minutes).append('M'); 135 if (seconds > 0 || nanos > 0 || (hours == 0 && minutes == 0)) 136 sb.append(seconds).append(nanos == 0 ? "" : "." + trimTrailingZeros(f("%09d", nanos))).append('S'); 137 } 138 return sb.toString(); 139 } 140 141 private static String formatHocon(Duration value) { 142 long nanos = value.toNanos(); 143 long abs = Math.abs(nanos); 144 if (abs == 0) 145 return "0s"; 146 long sign = nanos < 0 ? -1 : 1; 147 long[] divisors = {86_400_000_000_000L, 3_600_000_000_000L, 60_000_000_000L, 1_000_000_000L, 1_000_000L, 1_000L, 1L}; 148 String[] suffixes = {"d", "h", "m", "s", "ms", "us", "ns"}; 149 for (int i = 0; i < divisors.length; i++) { 150 long divisor = divisors[i]; 151 if (abs % divisor == 0) 152 return (sign * (abs / divisor)) + suffixes[i]; 153 } 154 return nanos + "ns"; 155 } 156 157 private static Duration parseHocon(String s) { 158 // Programmatic equivalent of full match ^([+-]?\d++(?:\.\d++)?)(ns|us|ms|s|m|h|d)$ (CASE_INSENSITIVE) to avoid per-parse matching. 159 // Group 1 = the numeric part, group 2 = the (case-insensitive) unit suffix. \d = ASCII [0-9]. 160 var len = s.length(); 161 var i = 0; 162 if (i < len && (s.charAt(i) == '+' || s.charAt(i) == '-')) 163 i++; 164 var digitsStart = i; 165 while (i < len && isAsciiDigit(s.charAt(i))) 166 i++; 167 var valid = i > digitsStart; 168 if (valid && i < len && s.charAt(i) == '.') { 169 i++; 170 var fracStart = i; 171 while (i < len && isAsciiDigit(s.charAt(i))) 172 i++; 173 valid = i > fracStart; 174 } 175 BigDecimal nanos = null; 176 if (valid) { 177 var value = new BigDecimal(s.substring(0, i)); 178 nanos = switch (s.substring(i).toLowerCase(Locale.ROOT)) { 179 case "d" -> value.multiply(BigDecimal.valueOf(86_400_000_000_000L)); 180 case "h" -> value.multiply(BigDecimal.valueOf(3_600_000_000_000L)); 181 case "m" -> value.multiply(BigDecimal.valueOf(60_000_000_000L)); 182 case "s" -> value.multiply(BigDecimal.valueOf(1_000_000_000L)); 183 case "ms" -> value.multiply(BigDecimal.valueOf(1_000_000L)); 184 case "us" -> value.multiply(BigDecimal.valueOf(1_000L)); 185 case "ns" -> value; 186 default -> null; 187 }; 188 } 189 if (nanos == null) 190 throw iaex("Invalid HOCON duration: %s", s); 191 return Duration.ofNanos(nanos.longValue()); 192 } 193 194 private static boolean isAsciiDigit(char c) { 195 return c >= '0' && c <= '9'; 196 } 197 198 // Removes trailing '0' characters. Equivalent of replaceAll("0++$", ""); returns "" if the input is all zeros. 199 private static String trimTrailingZeros(String s) { 200 var end = s.length(); 201 while (end > 0 && s.charAt(end - 1) == '0') 202 end--; 203 return s.substring(0, end); 204 } 205}