001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.convert;
018
019import java.time.Duration;
020import java.time.temporal.ChronoUnit;
021import java.util.function.Function;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.springframework.util.Assert;
026import org.springframework.util.StringUtils;
027
028/**
029 * Duration format styles.
030 *
031 * @author Phillip Webb
032 * @since 2.0.0
033 */
034public enum DurationStyle {
035
036        /**
037         * Simple formatting, for example '1s'.
038         */
039        SIMPLE("^([\\+\\-]?\\d+)([a-zA-Z]{0,2})$") {
040
041                @Override
042                public Duration parse(String value, ChronoUnit unit) {
043                        try {
044                                Matcher matcher = matcher(value);
045                                Assert.state(matcher.matches(), "Does not match simple duration pattern");
046                                String suffix = matcher.group(2);
047                                return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix)
048                                                : Unit.fromChronoUnit(unit)).parse(matcher.group(1));
049                        }
050                        catch (Exception ex) {
051                                throw new IllegalArgumentException(
052                                                "'" + value + "' is not a valid simple duration", ex);
053                        }
054                }
055
056                @Override
057                public String print(Duration value, ChronoUnit unit) {
058                        return Unit.fromChronoUnit(unit).print(value);
059                }
060
061        },
062
063        /**
064         * ISO-8601 formatting.
065         */
066        ISO8601("^[\\+\\-]?P.*$") {
067
068                @Override
069                public Duration parse(String value, ChronoUnit unit) {
070                        try {
071                                return Duration.parse(value);
072                        }
073                        catch (Exception ex) {
074                                throw new IllegalArgumentException(
075                                                "'" + value + "' is not a valid ISO-8601 duration", ex);
076                        }
077                }
078
079                @Override
080                public String print(Duration value, ChronoUnit unit) {
081                        return value.toString();
082                }
083
084        };
085
086        private final Pattern pattern;
087
088        DurationStyle(String pattern) {
089                this.pattern = Pattern.compile(pattern);
090        }
091
092        protected final boolean matches(String value) {
093                return this.pattern.matcher(value).matches();
094        }
095
096        protected final Matcher matcher(String value) {
097                return this.pattern.matcher(value);
098        }
099
100        /**
101         * Parse the given value to a duration.
102         * @param value the value to parse
103         * @return a duration
104         */
105        public Duration parse(String value) {
106                return parse(value, null);
107        }
108
109        /**
110         * Parse the given value to a duration.
111         * @param value the value to parse
112         * @param unit the duration unit to use if the value doesn't specify one ({@code null}
113         * will default to ms)
114         * @return a duration
115         */
116        public abstract Duration parse(String value, ChronoUnit unit);
117
118        /**
119         * Print the specified duration.
120         * @param value the value to print
121         * @return the printed result
122         */
123        public String print(Duration value) {
124                return print(value, null);
125        }
126
127        /**
128         * Print the specified duration using the given unit.
129         * @param value the value to print
130         * @param unit the value to use for printing
131         * @return the printed result
132         */
133        public abstract String print(Duration value, ChronoUnit unit);
134
135        /**
136         * Detect the style then parse the value to return a duration.
137         * @param value the value to parse
138         * @return the parsed duration
139         * @throws IllegalStateException if the value is not a known style or cannot be parsed
140         */
141        public static Duration detectAndParse(String value) {
142                return detectAndParse(value, null);
143        }
144
145        /**
146         * Detect the style then parse the value to return a duration.
147         * @param value the value to parse
148         * @param unit the duration unit to use if the value doesn't specify one ({@code null}
149         * will default to ms)
150         * @return the parsed duration
151         * @throws IllegalStateException if the value is not a known style or cannot be parsed
152         */
153        public static Duration detectAndParse(String value, ChronoUnit unit) {
154                return detect(value).parse(value, unit);
155        }
156
157        /**
158         * Detect the style from the given source value.
159         * @param value the source value
160         * @return the duration style
161         * @throws IllegalStateException if the value is not a known style
162         */
163        public static DurationStyle detect(String value) {
164                Assert.notNull(value, "Value must not be null");
165                for (DurationStyle candidate : values()) {
166                        if (candidate.matches(value)) {
167                                return candidate;
168                        }
169                }
170                throw new IllegalArgumentException("'" + value + "' is not a valid duration");
171        }
172
173        /**
174         * Units that we support.
175         */
176        enum Unit {
177
178                /**
179                 * Nanoseconds.
180                 */
181                NANOS(ChronoUnit.NANOS, "ns", Duration::toNanos),
182
183                /**
184                 * Microseconds.
185                 */
186                MICROS(ChronoUnit.MICROS, "us", (duration) -> duration.toMillis() * 1000L),
187
188                /**
189                 * Milliseconds.
190                 */
191                MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis),
192
193                /**
194                 * Seconds.
195                 */
196                SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds),
197
198                /**
199                 * Minutes.
200                 */
201                MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes),
202
203                /**
204                 * Hours.
205                 */
206                HOURS(ChronoUnit.HOURS, "h", Duration::toHours),
207
208                /**
209                 * Days.
210                 */
211                DAYS(ChronoUnit.DAYS, "d", Duration::toDays);
212
213                private final ChronoUnit chronoUnit;
214
215                private final String suffix;
216
217                private Function<Duration, Long> longValue;
218
219                Unit(ChronoUnit chronoUnit, String suffix, Function<Duration, Long> toUnit) {
220                        this.chronoUnit = chronoUnit;
221                        this.suffix = suffix;
222                        this.longValue = toUnit;
223                }
224
225                public Duration parse(String value) {
226                        return Duration.of(Long.valueOf(value), this.chronoUnit);
227                }
228
229                public String print(Duration value) {
230                        return longValue(value) + this.suffix;
231                }
232
233                public long longValue(Duration value) {
234                        return this.longValue.apply(value);
235                }
236
237                public static Unit fromChronoUnit(ChronoUnit chronoUnit) {
238                        if (chronoUnit == null) {
239                                return Unit.MILLIS;
240                        }
241                        for (Unit candidate : values()) {
242                                if (candidate.chronoUnit == chronoUnit) {
243                                        return candidate;
244                                }
245                        }
246                        throw new IllegalArgumentException("Unknown unit " + chronoUnit);
247                }
248
249                public static Unit fromSuffix(String suffix) {
250                        for (Unit candidate : values()) {
251                                if (candidate.suffix.equalsIgnoreCase(suffix)) {
252                                        return candidate;
253                                }
254                        }
255                        throw new IllegalArgumentException("Unknown unit '" + suffix + "'");
256                }
257
258        }
259
260}