http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java b/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java new file mode 100644 index 0000000..b11c0b0 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java @@ -0,0 +1,317 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.time; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; + +/** simple class determines a length of time */ +public class Duration implements Comparable<Duration>, Serializable { + + private static final long serialVersionUID = -2303909964519279617L; + + public static final Duration ZERO = of(0, null); + public static final Duration ONE_MILLISECOND = of(1, TimeUnit.MILLISECONDS); + public static final Duration ONE_SECOND = of(1, TimeUnit.SECONDS); + public static final Duration FIVE_SECONDS = of(5, TimeUnit.SECONDS); + public static final Duration TEN_SECONDS = of(10, TimeUnit.SECONDS); + public static final Duration THIRTY_SECONDS = of(30, TimeUnit.SECONDS); + public static final Duration ONE_MINUTE = of(1, TimeUnit.MINUTES); + public static final Duration TWO_MINUTES = of(2, TimeUnit.MINUTES); + public static final Duration FIVE_MINUTES = of(5, TimeUnit.MINUTES); + public static final Duration ONE_HOUR = of(1, TimeUnit.HOURS); + public static final Duration ONE_DAY = of(1, TimeUnit.DAYS); + + /** longest supported duration, 2^{63}-1 nanoseconds, approx ten billion seconds, or 300 years */ + public static final Duration PRACTICALLY_FOREVER = of(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + + private final long nanos; + + public Duration(long value, TimeUnit unit) { + if (value != 0) { + Preconditions.checkNotNull(unit, "Cannot accept null timeunit (unless value is 0)"); + } else { + unit = TimeUnit.MILLISECONDS; + } + nanos = TimeUnit.NANOSECONDS.convert(value, unit); + } + + @Override + public int compareTo(Duration o) { + return ((Long)toNanoseconds()).compareTo(o.toNanoseconds()); + } + + @Override + public String toString() { + return Time.makeTimeStringExact(this); + } + + public String toStringRounded() { + return Time.makeTimeStringRounded(this); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Duration)) return false; + return toMilliseconds() == ((Duration)o).toMilliseconds(); + } + + @Override + public int hashCode() { + return Long.valueOf(toMilliseconds()).hashCode(); + } + + /** converts to the given {@link TimeUnit}, using {@link TimeUnit#convert(long, TimeUnit)} which rounds _down_ + * (so 1 nanosecond converted to milliseconds gives 0 milliseconds, and -1 ns gives -1 ms) */ + public long toUnit(TimeUnit unit) { + return unit.convert(nanos, TimeUnit.NANOSECONDS); + } + + /** as {@link #toUnit(TimeUnit)} but rounding as indicated + * (rather than always taking the floor which is TimeUnit's default behaviour) */ + public long toUnit(TimeUnit unit, RoundingMode rounding) { + long result = unit.convert(nanos, TimeUnit.NANOSECONDS); + long check = TimeUnit.NANOSECONDS.convert(result, unit); + if (check==nanos || rounding==null || rounding==RoundingMode.UNNECESSARY) return result; + return new BigDecimal(nanos).divide(new BigDecimal(unit.toNanos(1)), rounding).longValue(); + } + + /** as {@link #toUnit(TimeUnit)} but rounding away from zero, + * so 1 ns converted to ms gives 1 ms, and -1 ns gives 1ms */ + public long toUnitRoundingUp(TimeUnit unit) { + return toUnit(unit, RoundingMode.UP); + } + + public long toMilliseconds() { + return toUnit(TimeUnit.MILLISECONDS); + } + + /** as {@link #toMilliseconds()} but rounding away from zero (so 1 nanosecond gets rounded to 1 millisecond); + * see {@link #toUnitRoundingUp(TimeUnit)}; provided as a convenience on top of {@link #toUnit(TimeUnit, RoundingMode)} + * as this is a common case (when you want to make sure you wait at least a certain amount of time) */ + public long toMillisecondsRoundingUp() { + return toUnitRoundingUp(TimeUnit.MILLISECONDS); + } + + public long toNanoseconds() { + return nanos; + } + + public long toSeconds() { + return toUnit(TimeUnit.SECONDS); + } + + /** number of nanoseconds of this duration */ + public long nanos() { + return nanos; + } + + /** + * See {@link Time#parseElapsedTime(String)}; + * also accepts "forever" (and for those who prefer things exceedingly accurate, "practically_forever"). + * Also see {@link #of(Object)}. */ + public static Duration parse(String textualDescription) { + if (textualDescription==null) return null; + if ("null".equalsIgnoreCase(textualDescription)) return null; + + if ("forever".equalsIgnoreCase(textualDescription)) return Duration.PRACTICALLY_FOREVER; + if ("practicallyforever".equalsIgnoreCase(textualDescription)) return Duration.PRACTICALLY_FOREVER; + if ("practically_forever".equalsIgnoreCase(textualDescription)) return Duration.PRACTICALLY_FOREVER; + + return new Duration((long) Time.parseElapsedTimeAsDouble(textualDescription), TimeUnit.MILLISECONDS); + } + + /** creates new {@link Duration} instance of the given length of time */ + public static Duration days(Number n) { + return new Duration((long) (n.doubleValue() * TimeUnit.DAYS.toNanos(1)), TimeUnit.NANOSECONDS); + } + + /** creates new {@link Duration} instance of the given length of time */ + public static Duration hours(Number n) { + return new Duration((long) (n.doubleValue() * TimeUnit.HOURS.toNanos(1)), TimeUnit.NANOSECONDS); + } + + /** creates new {@link Duration} instance of the given length of time */ + public static Duration minutes(Number n) { + return new Duration((long) (n.doubleValue() * TimeUnit.MINUTES.toNanos(1)), TimeUnit.NANOSECONDS); + } + + /** creates new {@link Duration} instance of the given length of time */ + public static Duration seconds(Number n) { + return new Duration((long) (n.doubleValue() * TimeUnit.SECONDS.toNanos(1)), TimeUnit.NANOSECONDS); + } + + /** creates new {@link Duration} instance of the given length of time */ + public static Duration millis(Number n) { + return new Duration((long) (n.doubleValue() * TimeUnit.MILLISECONDS.toNanos(1)), TimeUnit.NANOSECONDS); + } + + /** creates new {@link Duration} instance of the given length of time */ + public static Duration nanos(Number n) { + return new Duration(n.longValue(), TimeUnit.NANOSECONDS); + } + + public static Function<Number, String> millisToStringRounded() { return millisToStringRounded; } + private static Function<Number, String> millisToStringRounded = new Function<Number, String>() { + @Override + @Nullable + public String apply(@Nullable Number input) { + if (input == null) return null; + return Duration.millis(input).toStringRounded(); + } + }; + + public static Function<Number, String> secondsToStringRounded() { return secondsToStringRounded; } + private static Function<Number, String> secondsToStringRounded = new Function<Number, String>() { + @Override + @Nullable + public String apply(@Nullable Number input) { + if (input == null) return null; + return Duration.seconds(input).toStringRounded(); + } + }; + + /** tries to convert given object to a Duration, parsing strings, treating numbers as millis, etc; + * throws IAE if not convertible */ + public static Duration of(Object o) { + if (o == null) return null; + if (o instanceof Duration) return (Duration)o; + if (o instanceof String) return parse((String)o); + if (o instanceof Number) return millis((Number)o); + if (o instanceof Stopwatch) return millis(((Stopwatch)o).elapsed(TimeUnit.MILLISECONDS)); + + try { + // this allows it to work with groovy TimeDuration + Method millisMethod = o.getClass().getMethod("toMilliseconds"); + return millis((Long)millisMethod.invoke(o)); + } catch (Exception e) { + // probably no such method + } + + throw new IllegalArgumentException("Cannot convert "+o+" (type "+o.getClass()+") to a duration"); + } + + public static Duration of(long value, TimeUnit unit) { + return new Duration(value, unit); + } + + public static Duration max(Duration first, Duration second) { + return checkNotNull(first, "first").nanos >= checkNotNull(second, "second").nanos ? first : second; + } + + public static Duration min(Duration first, Duration second) { + return checkNotNull(first, "first").nanos <= checkNotNull(second, "second").nanos ? first : second; + } + + public static Duration untilUtc(long millisSinceEpoch) { + return millis(millisSinceEpoch - System.currentTimeMillis()); + } + + public static Duration sinceUtc(long millisSinceEpoch) { + return millis(System.currentTimeMillis() - millisSinceEpoch); + } + + public Duration add(Duration other) { + return nanos(nanos() + other.nanos()); + } + + public Duration subtract(Duration other) { + return nanos(nanos() - other.nanos()); + } + + public Duration multiply(long x) { + return nanos(nanos() * x); + } + public Duration times(long x) { + return multiply(x); + } + + /** as #multiply(long), but approximate due to the division (nano precision) */ + public Duration multiply(double d) { + return nanos(nanos() * d); + } + + public Duration half() { + return multiply(0.5); + } + + /** see {@link Time#sleep(long)} */ + public static void sleep(Duration duration) { + Time.sleep(duration); + } + + /** returns a new started {@link CountdownTimer} with this duration */ + public CountdownTimer countdownTimer() { + return CountdownTimer.newInstanceStarted(this); + } + + public boolean isPositive() { + return nanos()>0; + } + + public boolean isLongerThan(Duration x) { + return compareTo(x) > 0; + } + + public boolean isLongerThan(Stopwatch stopwatch) { + return isLongerThan(Duration.millis(stopwatch.elapsed(TimeUnit.MILLISECONDS))); + } + + public boolean isShorterThan(Duration x) { + return compareTo(x) < 0; + } + + public boolean isShorterThan(Stopwatch stopwatch) { + return isShorterThan(Duration.millis(stopwatch.elapsed(TimeUnit.MILLISECONDS))); + } + + /** returns the larger of this value or the argument */ + public Duration lowerBound(Duration alternateMinimumValue) { + if (isShorterThan(alternateMinimumValue)) return alternateMinimumValue; + return this; + } + + /** returns the smaller of this value or the argument */ + public Duration upperBound(Duration alternateMaximumValue) { + if (isLongerThan(alternateMaximumValue)) return alternateMaximumValue; + return this; + } + + /** @deprecated since 0.7.0 use {@link #lowerBound(Duration)} */ @Deprecated + public Duration minimum(Duration alternateMinimumValue) { + return lowerBound(alternateMinimumValue); + } + + /** @deprecated since 0.7.0 use {@link #upperBound(Duration)} */ @Deprecated + /** returns the smaller of this value or the argument */ + public Duration maximum(Duration alternateMaximumValue) { + return upperBound(alternateMaximumValue); + } +}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/time/Durations.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/time/Durations.java b/utils/common/src/main/java/org/apache/brooklyn/util/time/Durations.java new file mode 100644 index 0000000..7680184 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/time/Durations.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.time; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; + +public class Durations { + + public static boolean await(CountDownLatch latch, Duration time) throws InterruptedException { + return latch.await(time.toNanoseconds(), TimeUnit.NANOSECONDS); + } + + public static void join(Thread thread, Duration time) throws InterruptedException { + thread.join(time.toMillisecondsRoundingUp()); + } + + public static <T> Maybe<T> get(Future<T> t, Duration timeout) { + try { + if (timeout==null || timeout.toMilliseconds()<0 || Duration.PRACTICALLY_FOREVER.equals(timeout)) + return Maybe.of(t.get()); + if (timeout.toMilliseconds()==0 && !t.isDone()) { + for (int i=0; i<10; i++) { + // give it 10 nanoseconds to complete - heuristically this is often enough + // (Thread.yield should do it, but often seems to have no effect, e.g. on Mac) + Thread.yield(); + Thread.sleep(0, 1); + } + return Maybe.absent("Task "+t+" not completed when immediate completion requested"); + } + return Maybe.of(t.get(timeout.toMilliseconds(), TimeUnit.MILLISECONDS)); + } catch (TimeoutException e) { + return Maybe.absent("Task "+t+" did not complete within "+timeout); + } catch (CancellationException e) { + return Maybe.absent("Task "+t+" was cancelled"); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + + public static <T> Maybe<T> get(Future<T> t, CountdownTimer timer) { + if (timer==null) return get(t, (Duration)null); + Duration remaining = timer.getDurationRemaining(); + if (remaining.isPositive()) return get(t, remaining); + return get(t, Duration.ZERO); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/time/Time.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/time/Time.java b/utils/common/src/main/java/org/apache/brooklyn/util/time/Time.java new file mode 100644 index 0000000..b1ab8f3 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/time/Time.java @@ -0,0 +1,961 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.time; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.SimpleTimeZone; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; + +public class Time { + + private static final Logger log = LoggerFactory.getLogger(Time.class); + + public static final String DATE_FORMAT_PREFERRED_W_TZ = "yyyy-MM-dd HH:mm:ss.SSS Z"; + public static final String DATE_FORMAT_PREFERRED = "yyyy-MM-dd HH:mm:ss.SSS"; + public static final String DATE_FORMAT_STAMP = "yyyyMMdd-HHmmssSSS"; + public static final String DATE_FORMAT_SIMPLE_STAMP = "yyyy-MM-dd-HHmm"; + public static final String DATE_FORMAT_OF_DATE_TOSTRING = "EEE MMM dd HH:mm:ss zzz yyyy"; + public static final String DATE_FORMAT_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + public static final String DATE_FORMAT_ISO8601_NO_MILLIS = "yyyy-MM-dd'T'HH:mm:ssZ"; + + public static final long MILLIS_IN_SECOND = 1000; + public static final long MILLIS_IN_MINUTE = 60*MILLIS_IN_SECOND; + public static final long MILLIS_IN_HOUR = 60*MILLIS_IN_MINUTE; + public static final long MILLIS_IN_DAY = 24*MILLIS_IN_HOUR; + public static final long MILLIS_IN_YEAR = 365*MILLIS_IN_DAY; + + /** GMT/UTC/Z time zone constant */ + public static final TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone(""); + + /** as {@link #makeDateString(Date)} for current date/time */ + public static String makeDateString() { + return makeDateString(System.currentTimeMillis()); + } + + /** as {@link #makeDateString(Date)} for long millis since UTC epock */ + public static String makeDateString(long date) { + return makeDateString(new Date(date), DATE_FORMAT_PREFERRED); + } + /** returns the time in {@value #DATE_FORMAT_PREFERRED} format for the given date; + * this format is numeric big-endian but otherwise optimized for people to read, with spaces and colons and dots; + * time is local to the server and time zone is <i>not</i> included */ + public static String makeDateString(Date date) { + return makeDateString(date, DATE_FORMAT_PREFERRED); + } + /** as {@link #makeDateString(Date, String, TimeZone)} for the local time zone */ + public static String makeDateString(Date date, String format) { + return makeDateString(date, format, null); + } + /** as {@link #makeDateString(Date, String, TimeZone)} for the given time zone; consider {@link TimeZone#GMT} */ + public static String makeDateString(Date date, String format, TimeZone tz) { + SimpleDateFormat fmt = new SimpleDateFormat(format); + if (tz!=null) fmt.setTimeZone(tz); + return fmt.format(date); + } + /** as {@link #makeDateString(Date, String)} using {@link #DATE_FORMAT_PREFERRED_W_TZ} */ + public static String makeDateString(Calendar date) { + return makeDateString(date.getTime(), DATE_FORMAT_PREFERRED_W_TZ); + } + /** as {@link #makeDateString(Date, String, TimeZone)} for the time zone of the given calendar object */ + public static String makeDateString(Calendar date, String format) { + return makeDateString(date.getTime(), format, date.getTimeZone()); + } + + public static Function<Long, String> toDateString() { return dateString; } + private static Function<Long, String> dateString = new Function<Long, String>() { + @Override + @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return Time.makeDateString(input); + } + }; + + /** returns the current time in {@value #DATE_FORMAT_STAMP} format, + * suitable for machines to read with only numbers and dashes and quite precise (ms) */ + public static String makeDateStampString() { + return makeDateStampString(System.currentTimeMillis()); + } + + /** returns the time in {@value #DATE_FORMAT_STAMP} format, given a long (e.g. returned by System.currentTimeMillis); + * cf {@link #makeDateStampString()} */ + public static String makeDateStampString(long date) { + return new SimpleDateFormat(DATE_FORMAT_STAMP).format(new Date(date)); + } + + /** returns the current time in {@value #DATE_FORMAT_SIMPLE_STAMP} format, + * suitable for machines to read but easier for humans too, + * like {@link #makeDateStampString()} but not as precise */ + public static String makeDateSimpleStampString() { + return makeDateSimpleStampString(System.currentTimeMillis()); + } + + /** returns the time in {@value #DATE_FORMAT_SIMPLE_STAMP} format, given a long (e.g. returned by System.currentTimeMillis); + * cf {@link #makeDateSimpleStampString()} */ + public static String makeDateSimpleStampString(long date) { + return new SimpleDateFormat(DATE_FORMAT_SIMPLE_STAMP).format(new Date(date)); + } + + public static Function<Long, String> toDateStampString() { return dateStampString; } + private static Function<Long, String> dateStampString = new Function<Long, String>() { + @Override + @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return Time.makeDateStampString(input); + } + }; + + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringExact(long t, TimeUnit unit) { + long nanos = unit.toNanos(t); + return makeTimeStringNanoExact(nanos); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringRounded(long t, TimeUnit unit) { + long nanos = unit.toNanos(t); + return makeTimeStringNanoRounded(nanos); + } + public static String makeTimeStringRounded(Stopwatch timer) { + return makeTimeStringRounded(timer.elapsed(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringExact(long t) { + return makeTimeString(t, false); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringRounded(long t) { + return makeTimeString(t, true); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringRoundedSince(long utc) { + return makeTimeString(System.currentTimeMillis() - utc, true); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringExact(Duration d) { + return makeTimeStringNanoExact(d.toNanoseconds()); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringRounded(Duration d) { + return makeTimeStringNanoRounded(d.toNanoseconds()); + } + /** given an elapsed time, makes it readable, eg 44d 6h, or 8s 923ms, optionally rounding */ + public static String makeTimeString(long t, boolean round) { + return makeTimeStringNano(t*1000000L, round); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringNanoExact(long tn) { + return makeTimeStringNano(tn, false); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringNanoRounded(long tn) { + return makeTimeStringNano(tn, true); + } + /** @see #makeTimeString(long, boolean) */ + public static String makeTimeStringNano(long tn, boolean round) { + if (tn<0) return "-"+makeTimeStringNano(-tn, round); + // units don't matter, but since ms is the usual finest granularity let's use it + // (previously was just "0" but that was too ambiguous in contexts like "took 0") + if (tn==0) return "0ms"; + + long tnm = tn % 1000000; + long t = tn/1000000; + String result = ""; + + long d = t/MILLIS_IN_DAY; t %= MILLIS_IN_DAY; + long h = t/MILLIS_IN_HOUR; t %= MILLIS_IN_HOUR; + long m = t/MILLIS_IN_MINUTE; t %= MILLIS_IN_MINUTE; + long s = t/MILLIS_IN_SECOND; t %= MILLIS_IN_SECOND; + long ms = t; + + int segments = 0; + if (d>0) { result += d+"d "; segments++; } + if (h>0) { result += h+"h "; segments++; } + if (round && segments>=2) return Strings.removeAllFromEnd(result, " "); + if (m>0) { result += m+"m "; segments++; } + if (round && (segments>=2 || d>0)) return Strings.removeAllFromEnd(result, " "); + if (s>0) { + if (ms==0 && tnm==0) { + result += s+"s"; segments++; + return result; + } + if (round && segments>0) { + result += s+"s"; segments++; + return result; + } + if (round && s>10) { + result += toDecimal(s, ms/1000.0, 1)+"s"; segments++; + return result; + } + if (round) { + result += toDecimal(s, ms/1000.0, 2)+"s"; segments++; + return result; + } + result += s+"s "; + } + if (round && segments>0) + return Strings.removeAllFromEnd(result, " "); + if (ms>0) { + if (tnm==0) { + result += ms+"ms"; segments++; + return result; + } + if (round && ms>=100) { + result += toDecimal(ms, tnm/1000000.0, 1)+"ms"; segments++; + return result; + } + if (round && ms>=10) { + result += toDecimal(ms, tnm/1000000.0, 2)+"ms"; segments++; + return result; + } + if (round) { + result += toDecimal(ms, tnm/1000000.0, 3)+"ms"; segments++; + return result; + } + result += ms+"ms "; + } + + long us = tnm/1000; + long ns = tnm % 1000; + + if (us>0) { + if (ns==0) { + result += us+"us"; segments++; + return result; + } + if (round && us>=100) { + result += toDecimal(us, ns/1000.0, 1)+"us"; segments++; + return result; + } + if (round && us>=10) { + result += toDecimal(us, ns/1000.0, 2)+"us"; segments++; + return result; + } + if (round) { + result += toDecimal(us, ns/1000.0, 3)+"us"; segments++; + return result; + } + result += us+"us "; + } + + if (ns>0) result += ns+"ns"; + return Strings.removeAllFromEnd(result, " "); + } + + public static Function<Long, String> fromLongToTimeStringExact() { return LONG_TO_TIME_STRING_EXACT; } + private static final Function<Long, String> LONG_TO_TIME_STRING_EXACT = new FunctionLongToTimeStringExact(); + private static final class FunctionLongToTimeStringExact implements Function<Long, String> { + @Override @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return Time.makeTimeStringExact(input); + } + } + + /** @deprecated since 0.7.0 use {@link #fromLongToTimeStringExact()} */ @Deprecated + public static Function<Long, String> toTimeString() { return timeString; } + @Deprecated + private static Function<Long, String> timeString = new Function<Long, String>() { + @Override + @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return Time.makeTimeStringExact(input); + } + }; + + public static Function<Long, String> fromLongToTimeStringRounded() { return LONG_TO_TIME_STRING_ROUNDED; } + private static final Function<Long, String> LONG_TO_TIME_STRING_ROUNDED = new FunctionLongToTimeStringRounded(); + private static final class FunctionLongToTimeStringRounded implements Function<Long, String> { + @Override @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return Time.makeTimeStringRounded(input); + } + } + + /** @deprecated since 0.7.0 use {@link #fromLongToTimeStringRounded()} */ @Deprecated + public static Function<Long, String> toTimeStringRounded() { return timeStringRounded; } + @Deprecated + private static Function<Long, String> timeStringRounded = new Function<Long, String>() { + @Override + @Nullable + public String apply(@Nullable Long input) { + if (input == null) return null; + return Time.makeTimeStringRounded(input); + } + }; + + public static Function<Duration, String> fromDurationToTimeStringRounded() { return DURATION_TO_TIME_STRING_ROUNDED; } + private static final Function<Duration, String> DURATION_TO_TIME_STRING_ROUNDED = new FunctionDurationToTimeStringRounded(); + private static final class FunctionDurationToTimeStringRounded implements Function<Duration, String> { + @Override @Nullable + public String apply(@Nullable Duration input) { + if (input == null) return null; + return Time.makeTimeStringRounded(input); + } + } + + private static String toDecimal(long intPart, double fracPart, int decimalPrecision) { + long powTen = 1; + for (int i=0; i<decimalPrecision; i++) powTen *= 10; + long fpr = Math.round(fracPart * powTen); + if (fpr==powTen) { + intPart++; + fpr = 0; + } + return intPart + "." + Strings.makePaddedString(""+fpr, decimalPrecision, "0", ""); + } + + /** sleep which propagates Interrupted as unchecked */ + public static void sleep(long millis) { + try { + if (millis > 0) Thread.sleep(millis); + } catch (InterruptedException e) { + throw Exceptions.propagate(e); + } + } + + /** as {@link #sleep(long)} */ + public static void sleep(Duration duration) { + Time.sleep(duration.toMillisecondsRoundingUp()); + } + + /** + * Calculates the number of milliseconds past midnight for a given UTC time. + */ + public static long getTimeOfDayFromUtc(long timeUtc) { + GregorianCalendar gregorianCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + gregorianCalendar.setTimeInMillis(timeUtc); + int hour = gregorianCalendar.get(Calendar.HOUR_OF_DAY); + int min = gregorianCalendar.get(Calendar.MINUTE); + int sec = gregorianCalendar.get(Calendar.SECOND); + int millis = gregorianCalendar.get(Calendar.MILLISECOND); + return (((((hour * 60) + min) * 60) + sec) * 1000) + millis; + } + + /** + * Calculates the number of milliseconds past epoch for a given UTC time. + */ + public static long getTimeUtc(TimeZone zone, int year, int month, int date, int hourOfDay, int minute, int second, int millis) { + GregorianCalendar time = new GregorianCalendar(zone); + time.set(year, month, date, hourOfDay, minute, second); + time.set(Calendar.MILLISECOND, millis); + return time.getTimeInMillis(); + } + + public static long roundFromMillis(long millis, TimeUnit units) { + if (units.compareTo(TimeUnit.MILLISECONDS) > 0) { + double result = ((double)millis) / units.toMillis(1); + return Math.round(result); + } else { + return units.convert(millis, TimeUnit.MILLISECONDS); + } + } + + public static long roundFromMillis(long millis, long millisPerUnit) { + double result = ((double)millis) / millisPerUnit; + return Math.round(result); + } + + /** + * Calculates how long until maxTime has passed since the given startTime. + * However, maxTime==0 is a special case (e.g. could mean wait forever), so the result is guaranteed + * to be only 0 if maxTime was 0; otherwise -1 will be returned. + */ + public static long timeRemaining(long startTime, long maxTime) { + if (maxTime == 0) { + return 0; + } + long result = (startTime+maxTime) - System.currentTimeMillis(); + return (result == 0) ? -1 : result; + } + + /** Convenience for {@link Duration#parse(String)}. */ + public static Duration parseDuration(String timeString) { + return Duration.parse(timeString); + } + + /** + * As {@link #parseElapsedTimeAsDouble(String)}. Consider using {@link #parseDuration(String)} for a more usable return type. + * + * @throws NumberFormatException if cannot be parsed (or if null) + */ + public static long parseElapsedTime(String timeString) { + return (long) parseElapsedTimeAsDouble(timeString); + } + /** @deprecated since 0.7.0 see {@link #parseElapsedTime(String)} */ @Deprecated + public static long parseTimeString(String timeString) { + return (long) parseElapsedTime(timeString); + } + /** @deprecated since 0.7.0 see {@link #parseElapsedTimeAsDouble(String)} */ @Deprecated + public static double parseTimeStringAsDouble(String timeString) { + return parseElapsedTimeAsDouble(timeString); + } + + /** + * Parses a string eg '5s' or '20m 22.123ms', returning the number of milliseconds it represents; + * -1 on blank or never or off or false. + * Assumes unit is millisections if no unit is specified. + * + * @throws NumberFormatException if cannot be parsed (or if null) + */ + public static double parseElapsedTimeAsDouble(String timeString) { + if (timeString==null) + throw new NumberFormatException("GeneralHelper.parseTimeString cannot parse a null string"); + try { + double d = Double.parseDouble(timeString); + return d; + } catch (NumberFormatException e) { + //look for a type marker + timeString = timeString.trim(); + String s = Strings.getLastWord(timeString).toLowerCase(); + timeString = timeString.substring(0, timeString.length()-s.length()).trim(); + int i=0; + while (s.length()>i) { + char c = s.charAt(i); + if (c=='.' || Character.isDigit(c)) i++; + else break; + } + String num = s.substring(0, i); + if (i==0) { + num = Strings.getLastWord(timeString).toLowerCase(); + timeString = timeString.substring(0, timeString.length()-num.length()).trim(); + } else { + s = s.substring(i); + } + long multiplier = 0; + if (num.length()==0) { + //must be never or something + if (s.equalsIgnoreCase("never") || s.equalsIgnoreCase("off") || s.equalsIgnoreCase("false")) + return -1; + throw new NumberFormatException("unrecognised word '"+s+"' in time string"); + } + if (s.equalsIgnoreCase("ms") || s.equalsIgnoreCase("milli") || s.equalsIgnoreCase("millis") + || s.equalsIgnoreCase("millisec") || s.equalsIgnoreCase("millisecs") + || s.equalsIgnoreCase("millisecond") || s.equalsIgnoreCase("milliseconds")) + multiplier = 1; + else if (s.equalsIgnoreCase("s") || s.equalsIgnoreCase("sec") || s.equalsIgnoreCase("secs") + || s.equalsIgnoreCase("second") || s.equalsIgnoreCase("seconds")) + multiplier = 1000; + else if (s.equalsIgnoreCase("m") || s.equalsIgnoreCase("min") || s.equalsIgnoreCase("mins") + || s.equalsIgnoreCase("minute") || s.equalsIgnoreCase("minutes")) + multiplier = 60*1000; + else if (s.equalsIgnoreCase("h") || s.equalsIgnoreCase("hr") || s.equalsIgnoreCase("hrs") + || s.equalsIgnoreCase("hour") || s.equalsIgnoreCase("hours")) + multiplier = 60*60*1000; + else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreCase("days")) + multiplier = 24*60*60*1000; + else + throw new NumberFormatException("unknown unit '"+s+"' in time string"); + double d = Double.parseDouble(num); + double dd = 0; + if (timeString.length()>0) { + dd = parseElapsedTimeAsDouble(timeString); + if (dd==-1) { + throw new NumberFormatException("cannot combine '"+timeString+"' with '"+num+" "+s+"'"); + } + } + return d*multiplier + dd; + } + } + + public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) { + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(timestamp); + return cal; + } + + public static Calendar newCalendarFromDate(Date date) { + return newCalendarFromMillisSinceEpochUtc(date.getTime()); + } + + /** As {@link #parseCalendar(String)} but returning a {@link Date}, + * (i.e. a record where the time zone has been applied and forgotten). */ + public static Date parseDate(String input) { + if (input==null) return null; + return parseCalendarMaybe(input).get().getTime(); + } + + /** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html, + * e.g. 2015-06-15 16:00:00 +0000. + * <p> + * Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium, + * but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted. + * (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.) + * <p> + * Other formats including locale-specific variants, e.g. recognising month names, + * are supported but this may vary from platform to platform and may change between versions. */ + public static Calendar parseCalendar(String input) { + if (input==null) return null; + return parseCalendarMaybe(input).get(); + } + + /** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */ + public static Maybe<Calendar> parseCalendarMaybe(String input) { + if (input==null) return Maybe.absent("value is null"); + input = input.trim(); + Maybe<Calendar> result; + + result = parseCalendarUtc(input); + if (result.isPresent()) return result; + + result = parseCalendarSimpleFlexibleFormatParser(input); + if (result.isPresent()) return result; + // return the error from this method + Maybe<Calendar> returnResult = result; + + result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING, Locale.ROOT)); + if (result.isPresent()) return result; + result = parseCalendarDefaultParse(input); + if (result.isPresent()) return result; + + return returnResult; + } + + @SuppressWarnings("deprecation") + private static Maybe<Calendar> parseCalendarDefaultParse(String input) { + try { + long ms = Date.parse(input); + if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) { + // accept default date parse for this century and next + GregorianCalendar c = new GregorianCalendar(); + c.setTimeInMillis(ms); + return Maybe.of((Calendar)c); + } + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + } + return Maybe.absent(); + } + + private static Maybe<Calendar> parseCalendarUtc(String input) { + input = input.trim(); + if (input.matches("\\d+")) { + if ("0".equals(input)) { + // accept 0 as epoch UTC + return Maybe.of(newCalendarFromMillisSinceEpochUtc(0)); + } + Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input))); + if (result.isPresent()) { + int year = result.get().get(Calendar.YEAR); + if (year >= 2000 && year < 2200) { + // only applicable for dates in this century + return result; + } else { + return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range"); + } + } + } + return Maybe.absent("not long millis since epoch UTC"); + } + + private final static String DIGIT = "\\d"; + private final static String LETTER = "\\p{L}"; + private final static String COMMON_SEPARATORS = "-\\."; + private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":"; + private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ "; + private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ "; + + private final static String DATE_ONLY_WITH_INNER_SEPARATORS = + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT) + + anyChar(DATE_SEPARATOR) + + namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+")) + + anyChar(DATE_SEPARATOR) + + namedGroup("day", optionally(DIGIT)+DIGIT); + private final static String DATE_WORDS_2 = + namedGroup("month", anyChar(LETTER)+"+") + + anyChar(DATE_SEPARATOR) + + namedGroup("day", optionally(DIGIT)+DIGIT) + + ",?"+anyChar(DATE_SEPARATOR)+"+" + + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT); + // we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named + private final static String DATE_WORDS_3 = + namedGroup("day", optionally(DIGIT)+DIGIT) + + anyChar(DATE_SEPARATOR) + + namedGroup("month", anyChar(LETTER)+"+") + + ",?"+anyChar(DATE_SEPARATOR)+"+" + + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT); + + private final static String DATE_ONLY_NO_SEPARATORS = + namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT) + + namedGroup("month", DIGIT+DIGIT) + + namedGroup("day", DIGIT+DIGIT); + + private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM")); + private final static String TIME_ONLY_WITH_INNER_SEPARATORS = + namedGroup("hours", optionally(DIGIT)+DIGIT)+ + optionally( + anyChar(TIME_SEPARATOR)+ + namedGroup("mins", DIGIT+DIGIT)+ + optionally( + anyChar(TIME_SEPARATOR)+ + namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+ + optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN)); + private final static String TIME_ONLY_NO_SEPARATORS = + namedGroup("hours", DIGIT+DIGIT)+ + namedGroup("mins", DIGIT+DIGIT)+ + optionally( + namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+ + namedGroup("meridian", ""); + + private final static String TZ_CODE = + namedGroup("tzCode", + notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) + // not AM or PM + anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*"); + private final static String TIME_ZONE_SIGNED_OFFSET = + namedGroup("tz", + options( + namedGroup("tzOffset", options("\\+", "-")+ + DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)), + optionally("\\+")+TZ_CODE)); + private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET = + namedGroup("tz", + options( + namedGroup("tzOffset", options("\\+", "-", " ")+ + options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)), + TZ_CODE)); + + private static String getDateTimeSeparatorPattern(String extraChars) { + return + options( + " +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")), + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")) + + anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*"; + } + + @SuppressWarnings("deprecation") + // we have written our own parsing because the alternatives were either too specific or too general + // java and apache and even joda-time are too specific, and would require explosion of patterns to be flexible; + // Natty - https://github.com/joestelmach/natty - is very cool, but it drags in ANTLR, + // it doesn't support dashes between date and time, and + // it encourages relative time which would be awesome but only if we resolved it on read + // (however there is natty code to parseDateNatty in the git history if we did want to use it) + private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) { + input = input.trim(); + + String[] DATE_PATTERNS = new String[] { + DATE_ONLY_WITH_INNER_SEPARATORS, + DATE_ONLY_NO_SEPARATORS, + DATE_WORDS_2, + DATE_WORDS_3, + }; + String[] TIME_PATTERNS = new String[] { + TIME_ONLY_WITH_INNER_SEPARATORS, + TIME_ONLY_NO_SEPARATORS + }; + String[] TZ_PATTERNS = new String[] { + // space then time zone with sign (+-) or code is preferred + optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET, + // then no TZ - but declare the named groups + namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")), + // then any separator then offset with sign + getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET, + + // try parsing with enforced separators before TZ first + // (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone) + // then relax below (e.g. in the case of DATE-TIME+0100) + + // finally match DATE-TIME-1000 as time zone -1000 + // or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web + // (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year) + optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET + }; + + List<String> basePatterns = MutableList.of(); + + // patterns with date first + String[] DATE_PATTERNS_UNCLOSED = new String[] { + // separator before time *required* if date had separators + DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("Tt"), + // separator before time optional if date did not have separators + DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("Tt")), + // separator before time required if date has words + DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("Tt"), + DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("Tt"), + }; + for (String tzP: TZ_PATTERNS) + for (String dateP: DATE_PATTERNS_UNCLOSED) + for (String timeP: TIME_PATTERNS) + basePatterns.add(dateP + timeP+")?" + tzP); + + // also allow time first, with TZ after, then before + for (String tzP: TZ_PATTERNS) + for (String dateP: DATE_PATTERNS) + for (String timeP: TIME_PATTERNS) + basePatterns.add(timeP + getDateTimeSeparatorPattern("") + dateP + tzP); + // also allow time first, with TZ after, then before + for (String tzP: TZ_PATTERNS) + for (String dateP: DATE_PATTERNS) + for (String timeP: TIME_PATTERNS) + basePatterns.add(timeP + tzP + getDateTimeSeparatorPattern("") + dateP); + + Maybe<Matcher> mm = Maybe.absent(); + for (String p: basePatterns) { + mm = match(p, input); + if (mm.isPresent()) break; + } + if (mm.isPresent()) { + Matcher m = mm.get(); + Calendar result; + + String tz = m.group("tz"); + + int year = Integer.parseInt(m.group("year")); + int day = Integer.parseInt(m.group("day")); + + String monthS = m.group("month"); + int month; + if (monthS.matches(DIGIT+"+")) { + month = Integer.parseInt(monthS)-1; + } else { + try { + month = new SimpleDateFormat("yyyy-MMM-dd", Locale.ROOT).parse("2015-"+monthS+"-15").getMonth(); + } catch (ParseException e) { + return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000"); + } + } + + if (Strings.isNonBlank(tz)) { + TimeZone tzz = null; + String tzCode = m.group("tzCode"); + if (Strings.isNonBlank(tzCode)) { + tz = tzCode; + } + if (tz.matches(DIGIT+"+")) { + // stick a plus in front in case it was submitted by a web form and turned into a space + tz = "+"+tz; + } else { + tzz = getTimeZone(tz); + } + if (tzz==null) { + Maybe<Matcher> tmm = match(" ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT)), tz); + if (tmm.isAbsent()) { + return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000"); + } + Matcher tm = tmm.get(); + String tzM = tm.group("tzM"); + int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60; + tzz = new SimpleTimeZone(offset*1000, tz); + } + tz = getTimeZoneOffsetString(tzz, year, month, day); + result = new GregorianCalendar(tzz); + } else { + result = new GregorianCalendar(); + } + result.clear(); + + result.set(Calendar.YEAR, year); + result.set(Calendar.MONTH, month); + result.set(Calendar.DAY_OF_MONTH, day); + if (m.group("hours")!=null) { + int hours = Integer.parseInt(m.group("hours")); + String meridian = m.group("meridian"); + if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) { + if (hours>12) { + return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000"); + } + hours += 12; + } + result.set(Calendar.HOUR_OF_DAY, hours); + String minsS = m.group("mins"); + if (Strings.isNonBlank(minsS)) { + result.set(Calendar.MINUTE, Integer.parseInt(minsS)); + } + String secsS = m.group("secs"); + if (Strings.isBlank(secsS)) { + // leave at zero + } else if (secsS.matches(DIGIT+DIGIT+"?")) { + result.set(Calendar.SECOND, Integer.parseInt(secsS)); + } else { + double s = Double.parseDouble(secsS); + if (secsS.indexOf('.')>=0) { + // accept + } else if (secsS.length()==5) { + // allow ssSSS with no punctuation + s = s/=1000; + } else { + return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000"); + } + result.set(Calendar.SECOND, (int)s); + result.set(Calendar.MILLISECOND, (int)((s*1000) % 1000)); + } + } + + return Maybe.of(result); + } + return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000"); + } + + public static TimeZone getTimeZone(String code) { + if (code.indexOf('/')==-1) { + if ("Z".equals(code)) return TIME_ZONE_UTC; + if ("UTC".equals(code)) return TIME_ZONE_UTC; + if ("GMT".equals(code)) return TIME_ZONE_UTC; + + // get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes + if ("EST".equals(code)) return getTimeZone("America/New_York"); + if ("EDT".equals(code)) return getTimeZone("America/New_York"); + if ("PST".equals(code)) return getTimeZone("America/Los_Angeles"); + if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles"); + if ("CST".equals(code)) return getTimeZone("America/Chicago"); + if ("CDT".equals(code)) return getTimeZone("America/Chicago"); + if ("MST".equals(code)) return getTimeZone("America/Denver"); + if ("MDT".equals(code)) return getTimeZone("America/Denver"); + + if ("BST".equals(code)) return getTimeZone("Europe/London"); // otherwise BST is Bangladesh! + if ("CEST".equals(code)) return getTimeZone("Europe/Paris"); + // IST falls through to below, where it is treated as India (not Irish); IDT not recognised + } + + TimeZone tz = TimeZone.getTimeZone(code); + if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) { + // recognized + return tz; + } + // possibly unrecognized -- GMT returned if not known, bad TimeZone API! + String timeZones[] = TimeZone.getAvailableIDs(); + for (String tzs: timeZones) { + if (tzs.equals(code)) return tz; + } + // definitely unrecognized + return null; + } + + /** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings, + * absent with nice error if zone unknown */ + public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) { + TimeZone tzz = getTimeZone(tz); + if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz); + return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day)); + } + + /** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */ + @SuppressWarnings("deprecation") + public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) { + int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000; + String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", ""); + return tzStr; + } + + private static String namedGroup(String name, String pattern) { + return "(?<"+name+">"+pattern+")"; + } + private static String anyChar(String charSet) { + return "["+charSet+"]"; + } + private static String optionally(String pattern) { + return "("+pattern+")?"; + } + private static String options(String ...patterns) { + return "("+Strings.join(patterns,"|")+")"; + } + private static String notMatching(String pattern) { + return "(?!"+pattern+")"; + } + + private static Maybe<Matcher> match(String pattern, String input) { + Matcher m = Pattern.compile("^"+pattern+"$").matcher(input); + if (m.find()) return Maybe.of(m); + return Maybe.absent(); + } + + /** + * Parses the given date, accepting either a UTC timestamp (i.e. a long), or a formatted date. + * <p> + * If no time zone supplied, this defaults to the TZ configured at the brooklyn server. + * + * @deprecated since 0.7.0 use {@link #parseCalendar(String)} for general or {@link #parseCalendarFormat(String, DateFormat)} for a format, + * plus {@link #parseCalendarUtc(String)} if you want to accept UTC + */ + public static Date parseDateString(String dateString, DateFormat format) { + Maybe<Calendar> r = parseCalendarFormat(dateString, format); + if (r.isPresent()) return r.get().getTime(); + + r = parseCalendarUtc(dateString); + if (r.isPresent()) return r.get().getTime(); + + throw new IllegalArgumentException("Date " + dateString + " cannot be parsed as UTC millis or using format " + format); + } + public static Maybe<Calendar> parseCalendarFormat(String dateString, String format) { + return parseCalendarFormat(dateString, new SimpleDateFormat(format, Locale.ROOT)); + } + public static Maybe<Calendar> parseCalendarFormat(String dateString, DateFormat format) { + if (dateString == null) { + throw new NumberFormatException("GeneralHelper.parseDateString cannot parse a null string"); + } + Preconditions.checkNotNull(format, "date format"); + dateString = dateString.trim(); + + ParsePosition p = new ParsePosition(0); + Date result = format.parse(dateString, p); + if (result!=null) { + // accept results even if the entire thing wasn't parsed, as enough was to match the requested format + return Maybe.of(newCalendarFromDate(result)); + } + if (log.isTraceEnabled()) log.trace("Could not parse date "+dateString+" using format "+format+": "+p); + return Maybe.absent(); + } + + /** removes milliseconds from the date object; needed if serializing to ISO-8601 format + * and want to serialize back and get the same data */ + public static Date dropMilliseconds(Date date) { + return date==null ? null : date.getTime()%1000!=0 ? new Date(date.getTime() - (date.getTime()%1000)) : date; + } + + /** returns the duration elapsed since the given timestamp (UTC) */ + public static Duration elapsedSince(long timestamp) { + return Duration.millis(System.currentTimeMillis() - timestamp); + } + + /** true iff it has been longer than the given duration since the given timestamp */ + public static boolean hasElapsedSince(long timestamp, Duration duration) { + return elapsedSince(timestamp).compareTo(duration) > 0; + } + + /** more readable and shorter convenience for System.currentTimeMillis() */ + public static long now() { + return System.currentTimeMillis(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/cf2f7a93/utils/common/src/main/java/org/apache/brooklyn/util/yaml/Yamls.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yaml/Yamls.java b/utils/common/src/main/java/org/apache/brooklyn/util/yaml/Yamls.java new file mode 100644 index 0000000..1697097 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yaml/Yamls.java @@ -0,0 +1,553 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yaml; + +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.exceptions.UserFacingException; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yaml.Yamls; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.Mark; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; + +import com.google.common.annotations.Beta; +import com.google.common.collect.Iterables; + +public class Yamls { + + private static final Logger log = LoggerFactory.getLogger(Yamls.class); + + /** returns the given (yaml-parsed) object as the given yaml type. + * <p> + * if the object is an iterable or iterator this method will fully expand it as a list. + * if the requested type is not an iterable or iterator, and the list contains a single item, this will take that single item. + * <p> + * in other cases this method simply does a type-check and cast (no other type coercion). + * <p> + * @throws IllegalArgumentException if the input is an iterable not containing a single element, + * and the cast is requested to a non-iterable type + * @throws ClassCastException if cannot be casted */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static <T> T getAs(Object x, Class<T> type) { + if (x==null) return null; + if (x instanceof Iterable || x instanceof Iterator) { + List result = new ArrayList(); + Iterator xi; + if (Iterator.class.isAssignableFrom(x.getClass())) { + xi = (Iterator)x; + } else { + xi = ((Iterable)x).iterator(); + } + while (xi.hasNext()) { + result.add( xi.next() ); + } + if (type.isAssignableFrom(List.class)) return (T)result; + if (type.isAssignableFrom(Iterator.class)) return (T)result.iterator(); + x = Iterables.getOnlyElement(result); + } + if (type.isInstance(x)) return (T)x; + throw new ClassCastException("Cannot convert "+x+" ("+x.getClass()+") to "+type); + } + + /** + * Parses the given yaml, and walks the given path to return the referenced object. + * + * @see #getAt(Object, List) + */ + @Beta + public static Object getAt(String yaml, List<String> path) { + Iterable<Object> result = new org.yaml.snakeyaml.Yaml().loadAll(yaml); + Object current = result.iterator().next(); + return getAtPreParsed(current, path); + } + + /** + * For pre-parsed yaml, walks the maps/lists to return the given sub-item. + * In the given path: + * <ul> + * <li>A vanilla string is assumed to be a key into a map. + * <li>A string in the form like "[0]" is assumed to be an index into a list + * </ul> + * + * Also see {@link Jsonya}, such as {@code Jsonya.of(current).at(path).get()}. + * + * @return The object at the given path, or {@code null} if that path does not exist. + */ + @Beta + @SuppressWarnings("unchecked") + public static Object getAtPreParsed(Object current, List<String> path) { + for (String pathPart : path) { + if (pathPart.startsWith("[") && pathPart.endsWith("]")) { + String index = pathPart.substring(1, pathPart.length()-1); + try { + current = Iterables.get((Iterable<?>)current, Integer.parseInt(index)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid index '"+index+"', in path "+path); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException("Invalid index '"+index+"', in path "+path); + } + } else { + current = ((Map<String, ?>)current).get(pathPart); + } + if (current == null) return null; + } + return current; + } + + @SuppressWarnings("rawtypes") + public static void dump(int depth, Object r) { + if (r instanceof Iterable) { + for (Object ri : ((Iterable)r)) + dump(depth+1, ri); + } else if (r instanceof Map) { + for (Object re: ((Map)r).entrySet()) { + for (int i=0; i<depth; i++) System.out.print(" "); + System.out.println(((Entry)re).getKey()+":"); + dump(depth+1, ((Entry)re).getValue()); + } + } else { + for (int i=0; i<depth; i++) System.out.print(" "); + if (r==null) System.out.println("<null>"); + else System.out.println("<"+r.getClass().getSimpleName()+">"+" "+r); + } + } + + /** simplifies new Yaml().loadAll, and converts to list to prevent single-use iterable bug in yaml */ + @SuppressWarnings("unchecked") + public static Iterable<Object> parseAll(String yaml) { + Iterable<Object> result = new org.yaml.snakeyaml.Yaml().loadAll(yaml); + return (List<Object>) getAs(result, List.class); + } + + /** as {@link #parseAll(String)} */ + @SuppressWarnings("unchecked") + public static Iterable<Object> parseAll(Reader yaml) { + Iterable<Object> result = new org.yaml.snakeyaml.Yaml().loadAll(yaml); + return (List<Object>) getAs(result, List.class); + } + + public static Object removeMultinameAttribute(Map<String,Object> obj, String ...equivalentNames) { + Object result = null; + for (String name: equivalentNames) { + Object candidate = obj.remove(name); + if (candidate!=null) { + if (result==null) result = candidate; + else if (!result.equals(candidate)) { + log.warn("Different values for attributes "+Arrays.toString(equivalentNames)+"; " + + "preferring '"+result+"' to '"+candidate+"'"); + } + } + } + return result; + } + + public static Object getMultinameAttribute(Map<String,Object> obj, String ...equivalentNames) { + Object result = null; + for (String name: equivalentNames) { + Object candidate = obj.get(name); + if (candidate!=null) { + if (result==null) result = candidate; + else if (!result.equals(candidate)) { + log.warn("Different values for attributes "+Arrays.toString(equivalentNames)+"; " + + "preferring '"+result+"' to '"+candidate+"'"); + } + } + } + return result; + } + + @Beta + public static class YamlExtract { + String yaml; + NodeTuple focusTuple; + Node prev, key, focus, next; + Exception error; + boolean includeKey = false, includePrecedingComments = true, includeOriginalIndentation = false; + + private int indexStart(Node node, boolean defaultIsYamlEnd) { + if (node==null) return defaultIsYamlEnd ? yaml.length() : 0; + return index(node.getStartMark()); + } + private int indexEnd(Node node, boolean defaultIsYamlEnd) { + if (!found() || node==null) return defaultIsYamlEnd ? yaml.length() : 0; + return index(node.getEndMark()); + } + private int index(Mark mark) { + try { + return mark.getIndex(); + } catch (NoSuchMethodError e) { + try { + getClass().getClassLoader().loadClass("org.testng.TestNG"); + } catch (ClassNotFoundException e1) { + // not using TestNG + Exceptions.propagateIfFatal(e1); + throw e; + } + if (!LOGGED_TESTNG_WARNING.getAndSet(true)) { + log.warn("Detected TestNG/SnakeYAML version incompatibilities: " + + "some YAML source reconstruction will be unavailable. " + + "This can happen with TestNG plugins which force an older version of SnakeYAML " + + "which does not support Mark.getIndex. " + + "It should not occur from maven CLI runs. " + + "(Subsequent occurrences will be silently dropped, and source code reconstructed from YAML.)"); + } + // using TestNG + throw new KnownClassVersionException(e); + } + } + + static AtomicBoolean LOGGED_TESTNG_WARNING = new AtomicBoolean(); + static class KnownClassVersionException extends IllegalStateException { + private static final long serialVersionUID = -1620847775786753301L; + public KnownClassVersionException(Throwable e) { + super("Class version error. This can happen if using a TestNG plugin in your IDE " + + "which is an older version, dragging in an older version of SnakeYAML which does not support Mark.getIndex.", e); + } + } + + public int getEndOfPrevious() { + return indexEnd(prev, false); + } + @Nullable public Node getKey() { + return key; + } + public Node getResult() { + return focus; + } + public int getStartOfThis() { + if (includeKey && focusTuple!=null) return indexStart(focusTuple.getKeyNode(), false); + return indexStart(focus, false); + } + private int getStartColumnOfThis() { + if (includeKey && focusTuple!=null) return focusTuple.getKeyNode().getStartMark().getColumn(); + return focus.getStartMark().getColumn(); + } + public int getEndOfThis() { + return getEndOfThis(false); + } + private int getEndOfThis(boolean goToEndIfNoNext) { + if (next==null && goToEndIfNoNext) return yaml.length(); + return indexEnd(focus, false); + } + public int getStartOfNext() { + return indexStart(next, true); + } + + private static int initialWhitespaceLength(String x) { + int i=0; + while (i < x.length() && x.charAt(i)==' ') i++; + return i; + } + + public String getFullYamlTextOriginal() { + return yaml; + } + + /** Returns the original YAML with the found item replaced by the given replacement YAML. + * @param replacement YAML to put in for the found item; + * this YAML typically should not have any special indentation -- if required when replacing it will be inserted. + * <p> + * if replacing an inline map entry, the supplied entry must follow the structure being replaced; + * for example, if replacing the value in <code>key: value</code> with a map, + * supplying a replacement <code>subkey: value</code> would result in invalid yaml; + * the replacement must be supplied with a newline, either before the subkey or after. + * (if unsure we believe it is always valid to include an initial newline or comment with newline.) + */ + public String getFullYamlTextWithExtractReplaced(String replacement) { + if (!found()) throw new IllegalStateException("Cannot perform replacement when item was not matched."); + String result = yaml.substring(0, getStartOfThis()); + + String[] newLines = replacement.split("\n"); + for (int i=1; i<newLines.length; i++) + newLines[i] = Strings.makePaddedString("", getStartColumnOfThis(), "", " ") + newLines[i]; + result += Strings.lines(newLines); + if (replacement.endsWith("\n")) result += "\n"; + + int end = getEndOfThis(); + result += yaml.substring(end); + + return result; + } + + /** Specifies whether the key should be included in the found text, + * when calling {@link #getMatchedYamlText()} or {@link #getFullYamlTextWithExtractReplaced(String)}, + * if the found item is a map entry. + * Defaults to false. + * @return this object, for use in a fluent constructions + */ + public YamlExtract withKeyIncluded(boolean includeKey) { + this.includeKey = includeKey; + return this; + } + + /** Specifies whether comments preceding the found item should be included, + * when calling {@link #getMatchedYamlText()} or {@link #getFullYamlTextWithExtractReplaced(String)}. + * This will not include comments which are indented further than the item, + * as those will typically be aligned with the previous item + * (whereas comments whose indentation is the same or less than the found item + * will typically be aligned with this item). + * Defaults to true. + * @return this object, for use in a fluent constructions + */ + public YamlExtract withPrecedingCommentsIncluded(boolean includePrecedingComments) { + this.includePrecedingComments = includePrecedingComments; + return this; + } + + /** Specifies whether the original indentation should be preserved + * (and in the case of the first line, whether whitespace should be inserted so its start column is preserved), + * when calling {@link #getMatchedYamlText()}. + * Defaults to false, the returned text will be outdented as far as possible. + * @return this object, for use in a fluent constructions + */ + public YamlExtract withOriginalIndentation(boolean includeOriginalIndentation) { + this.includeOriginalIndentation = includeOriginalIndentation; + return this; + } + + @Beta + public String getMatchedYamlTextOrWarn() { + try { + return getMatchedYamlText(); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + if (e instanceof KnownClassVersionException) { + log.debug("Known class version exception; no yaml text being matched for "+this+": "+e); + } else { + if (e instanceof UserFacingException) { + log.warn("Unable to match yaml text in "+this+": "+e.getMessage()); + } else { + log.warn("Unable to match yaml text in "+this+": "+e, e); + } + } + return null; + } + } + + @Beta + public String getMatchedYamlText() { + if (!found()) return null; + + String[] body = yaml.substring(getStartOfThis(), getEndOfThis(true)).split("\n", -1); + + int firstLineIndentationOfFirstThing; + if (focusTuple!=null) { + firstLineIndentationOfFirstThing = focusTuple.getKeyNode().getStartMark().getColumn(); + } else { + firstLineIndentationOfFirstThing = focus.getStartMark().getColumn(); + } + int firstLineIndentationToAdd; + if (focusTuple!=null && (includeKey || body.length==1)) { + firstLineIndentationToAdd = focusTuple.getKeyNode().getStartMark().getColumn(); + } else { + firstLineIndentationToAdd = focus.getStartMark().getColumn(); + } + + + String firstLineIndentationToAddS = Strings.makePaddedString("", firstLineIndentationToAdd, "", " "); + String subsequentLineIndentationToRemoveS = firstLineIndentationToAddS; + +/* complexities of indentation: + +x: a + bc + +should become + +a + bc + +whereas + +- a: 0 + b: 1 + +selecting 0 should give + +a: 0 +b: 1 + + */ + List<String> result = MutableList.of(); + if (includePrecedingComments) { + if (getEndOfPrevious() > getStartOfThis()) { + throw new UserFacingException("YAML not in expected format; when scanning, previous end "+getEndOfPrevious()+" exceeds this start "+getStartOfThis()); + } + String[] preceding = yaml.substring(getEndOfPrevious(), getStartOfThis()).split("\n"); + // suppress comments which are on the same line as the previous item or indented more than firstLineIndentation, + // ensuring appropriate whitespace is added to preceding[0] if it starts mid-line + if (preceding.length>0 && prev!=null) { + preceding[0] = Strings.makePaddedString("", prev.getEndMark().getColumn(), "", " ") + preceding[0]; + } + for (String p: preceding) { + int w = initialWhitespaceLength(p); + p = p.substring(w); + if (p.startsWith("#")) { + // only add if the hash is not indented further than the first line + if (w <= firstLineIndentationOfFirstThing) { + if (includeOriginalIndentation) p = firstLineIndentationToAddS + p; + result.add(p); + } + } + } + } + + boolean doneFirst = false; + for (String p: body) { + if (!doneFirst) { + if (includeOriginalIndentation) { + // have to insert the right amount of spacing + p = firstLineIndentationToAddS + p; + } + result.add(p); + doneFirst = true; + } else { + if (includeOriginalIndentation) { + result.add(p); + } else { + result.add(Strings.removeFromStart(p, subsequentLineIndentationToRemoveS)); + } + } + } + return Strings.join(result, "\n"); + } + + boolean found() { + return focus != null; + } + + public Exception getError() { + return error; + } + + @Override + public String toString() { + return "Extract["+focus+";prev="+prev+";key="+key+";next="+next+"]"; + } + } + + private static void findTextOfYamlAtPath(YamlExtract result, int pathIndex, Object ...path) { + if (pathIndex>=path.length) { + // we're done + return; + } + + Object pathItem = path[pathIndex]; + Node node = result.focus; + + if (node.getNodeId()==NodeId.mapping && pathItem instanceof String) { + // find key + Iterator<NodeTuple> ti = ((MappingNode)node).getValue().iterator(); + while (ti.hasNext()) { + NodeTuple t = ti.next(); + Node key = t.getKeyNode(); + if (key.getNodeId()==NodeId.scalar) { + if (pathItem.equals( ((ScalarNode)key).getValue() )) { + result.key = key; + result.focus = t.getValueNode(); + if (pathIndex+1<path.length) { + // there are more path items, so the key here is a previous node + result.prev = key; + } else { + result.focusTuple = t; + } + findTextOfYamlAtPath(result, pathIndex+1, path); + if (result.next==null) { + if (ti.hasNext()) result.next = ti.next().getKeyNode(); + } + return; + } else { + result.prev = t.getValueNode(); + } + } else { + throw new IllegalStateException("Key "+key+" is not a scalar, searching for "+pathItem+" at depth "+pathIndex+" of "+Arrays.asList(path)); + } + } + throw new IllegalStateException("Did not find "+pathItem+" in "+node+" at depth "+pathIndex+" of "+Arrays.asList(path)); + + } else if (node.getNodeId()==NodeId.sequence && pathItem instanceof Number) { + // find list item + List<Node> nl = ((SequenceNode)node).getValue(); + int i = ((Number)pathItem).intValue(); + if (i>=nl.size()) + throw new IllegalStateException("Index "+i+" is out of bounds in "+node+", searching for "+pathItem+" at depth "+pathIndex+" of "+Arrays.asList(path)); + if (i>0) result.prev = nl.get(i-1); + result.key = null; + result.focus = nl.get(i); + findTextOfYamlAtPath(result, pathIndex+1, path); + if (result.next==null) { + if (nl.size()>i+1) result.next = nl.get(i+1); + } + return; + + } else { + throw new IllegalStateException("Node "+node+" does not match selector "+pathItem+" at depth "+pathIndex+" of "+Arrays.asList(path)); + } + + // unreachable + } + + + /** Given a path, where each segment consists of a string (key) or number (element in list), + * this will find the YAML text for that element + * <p> + * If not found this will return a {@link YamlExtract} + * where {@link YamlExtract#isMatch()} is false and {@link YamlExtract#getError()} is set. */ + public static YamlExtract getTextOfYamlAtPath(String yaml, Object ...path) { + YamlExtract result = new YamlExtract(); + if (yaml==null) return result; + try { + int pathIndex = 0; + result.yaml = yaml; + result.focus = new Yaml().compose(new StringReader(yaml)); + + findTextOfYamlAtPath(result, pathIndex, path); + return result; + } catch (NoSuchMethodError e) { + throw new IllegalStateException("Class version error. This can happen if using a TestNG plugin in your IDE " + + "which is an older version, dragging in an older version of SnakeYAML which does not support Mark.getIndex.", e); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.debug("Unable to find element in yaml (setting in result): "+e); + result.error = e; + return result; + } + } +}
