/* * Copyright 2000-2018 JetBrains s.r.o. * * Licensed 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 jetbrains.buildServer.server.rest.data; import com.intellij.openapi.diagnostic.Logger; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import jetbrains.buildServer.server.rest.errors.BadRequestException; import jetbrains.buildServer.server.rest.model.Constants; import jetbrains.buildServer.serverSide.TeamCityProperties; import jetbrains.buildServer.util.TimeService; import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.Instant; import org.joda.time.LocalTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.ISODateTimeFormat; /** * @author Yegor.Yarko * Date: 15/02/2016 */ public class TimeWithPrecision { private static Logger LOG = Logger.getInstance(TimeWithPrecision.class.getName()); @NotNull private final Date myTime; private final boolean mySecondsPrecision; public TimeWithPrecision(@NotNull final Date time, final boolean secondsPrecision) { myTime = time; mySecondsPrecision = secondsPrecision; } @NotNull public Date getTime() { return myTime; } public boolean isSecondsPrecision() { return mySecondsPrecision; } @NotNull public static TimeWithPrecision parse(@NotNull String timeString, @NotNull final TimeService timeService) { Locator.processHelpRequest(timeString, "Date/time string in format '" + DateTimeFormat.forPattern(Constants.TIME_FORMAT).withLocale(Locale.ENGLISH).print(Instant.now()) + "'" + " or offset in format '-4w2d5h30m5s'"); if (timeString.contains(" ")) { throw new BadRequestException("Invalid date/time string '" + timeString + "'. Note that '+' should be escaped in URL as '%2B'."); } boolean useSecondsPrecision = TeamCityProperties.getBoolean("rest.timeMatching.roundEntityTimeToSeconds"); //compatibility switch to mimic pre-2018.2 behavior if (timeString.startsWith("-")) { return new TimeWithPrecision(new Date(timeService.now() - TimeWithPrecision.getMsFromRelativeTime(timeString.substring("-".length()))), useSecondsPrecision); } else if (timeString.startsWith("+")) { return new TimeWithPrecision(new Date(timeService.now() + TimeWithPrecision.getMsFromRelativeTime(timeString.substring("+".length()))), useSecondsPrecision); } // try different parsers // the order can be important as SimpleDateFormat will parse the beginning of the string ignoring any unparsed trailing characters BadRequestException firstError; if (TeamCityProperties.getBoolean("rest.compatibilityDateParsing")) { try { return new TimeWithPrecision(new SimpleDateFormat(Constants.TIME_FORMAT, Locale.ENGLISH).parse(timeString), true); } catch (ParseException e) { firstError = new BadRequestException("Was not able to parse date '" + timeString + "' using format '" + Constants.TIME_FORMAT + "' and others." + " Supported format example: '" + new SimpleDateFormat(Constants.TIME_FORMAT, Locale.ENGLISH).format(new Date()) + "'." + " Error: " + e.toString(), e); } } else { try { return new TimeWithPrecision(DateTimeFormat.forPattern(Constants.TIME_FORMAT).withLocale(Locale.ENGLISH).parseDateTime(timeString).toDate(), useSecondsPrecision); } catch (Exception e) { firstError = new BadRequestException("Was not able to parse date '" + timeString + "' using format '" + Constants.TIME_FORMAT + "' and others." + " Supported format example: '" + DateTimeFormat.forPattern(Constants.TIME_FORMAT).withLocale(Locale.ENGLISH).withZone(DateTimeZone.getDefault()).print(Instant.now()) + "'. Error: " + e.toString(), e); } } try { return new TimeWithPrecision(ISODateTimeFormat.localTimeParser().withLocale(Locale.ENGLISH). parseLocalTime(timeString).toDateTime(new DateTime(timeService.now())).toDate(), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using ISODateTimeFormat.localTimeParser. Error: " + e.toString(), e); } try { return new TimeWithPrecision(ISODateTimeFormat.dateTimeParser().withLocale(Locale.ENGLISH).parseDateTime(timeString).toDate(), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using ISODateTimeFormat.dateTimeParser. Error: " + e.toString(), e); } try { return new TimeWithPrecision(ISODateTimeFormat.basicDateTime().withLocale(Locale.ENGLISH).parseDateTime(timeString).toDate(), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using ISODateTimeFormat.basicDateTime. Error: " + e.toString(), e); } try { return new TimeWithPrecision(ISODateTimeFormat.basicDateTimeNoMillis().withLocale(Locale.ENGLISH).parseDateTime(timeString).toDate(), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using ISODateTimeFormat.basicDateTimeNoMillis. Error: " + e.toString(), e); } try { DateTime dateTime = ISODateTimeFormat.localDateOptionalTimeParser().withLocale(Locale.ENGLISH).parseDateTime(timeString).withZoneRetainFields(DateTimeZone.getDefault()); return new TimeWithPrecision(dateTime.toDate(), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using ISODateTimeFormat.localDateOptionalTimeParser. Error: " + e.toString(), e); } try { return new TimeWithPrecision(LocalTime.parse(timeString).toDateTime(new DateTime(timeService.now()).withMillisOfDay(0)).toDate(), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using LocalTime. Error: " + e.toString(), e); } try { return new TimeWithPrecision(DateTimeFormat.forPattern("yyyyMMdd'T'HHmmss.SSS").withLocale(Locale.ENGLISH).parseLocalDateTime(timeString).toDate(), false); // return new TimeWithPrecision(new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS", Locale.ENGLISH).parse(timeString), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using format '" + "yyyyMMdd'T'HHmmss.SSS" + "'. Error: " + e.toString(), e); } try { return new TimeWithPrecision(DateTimeFormat.forPattern("yyyyMMdd'T'HHmmss").withLocale(Locale.ENGLISH).parseLocalDateTime(timeString).toDate(), false); // return new TimeWithPrecision(new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH).parse(timeString), false); } catch (Exception e) { //ignore if (LOG.isDebugEnabled()) LOG.debug("Was not able to parse date/time '" + timeString + "' using format '" + "yyyyMMdd'T'HHmmss" + "'. Error: " + e.toString(), e); } StringBuilder message = new StringBuilder(); Date exampleTime = new Date(); // throwing original error message.append("Could not parse date from value '").append(timeString).append("'. Supported formats are ISO8601 or for example: "); message.append(new SimpleDateFormat(Constants.TIME_FORMAT, Locale.ENGLISH).format(exampleTime)); message.append(". First reported error: ").append(firstError.getMessage()); throw firstError; } private static class TimeFormat { @NotNull private final String myTimeFormat; private final boolean mySecondsPrecision; public TimeFormat(@NotNull final String timeFormat, final boolean secondsPrecision) { myTimeFormat = timeFormat; mySecondsPrecision = secondsPrecision; } } public static long getMsFromRelativeTime(@NotNull final String relativeTimeString) { ParseResult result = new ParseResult(relativeTimeString); result.processTimeValue("y", 365L * 24 * 60 * 60 * 1000); result.processTimeValue("w", 7L * 24 * 60 * 60 * 1000); result.processTimeValue("mo", 30L * 24 * 60 * 60 * 1000); result.processTimeValue("d", 24L * 60 * 60 * 1000); result.processTimeValue("h", 60L * 60 * 1000); result.processTimeValue("m", 60L * 1000); result.processTimeValue("s", 1000); result.processTimeValue("ms", 1); if (!result.isFullyParsed()) { throw new BadRequestException("Unsupported relative time for the remaining text '" + result.myTimeText + "': supported format example: '-4w2d5h30m5s'"); } return result.myTimeMs; } private static class ParseResult { @NotNull private String myTimeText; private long myTimeMs; private int myNextUnitStartIndex = -1; private int myNextUnitEndIndex; public ParseResult(@NotNull String timeText) { myTimeText = timeText; myTimeMs = 0; } /** * @param dimension * @param dimensionValue * @param timeCondition * @return rest of the parsed relativeTimeString */ @NotNull private ParseResult processTimeValue(@NotNull final String dimension, final long dimensionValue) { if (isFullyParsed() || !dimension.equals(getNextUnit())) { return this; } long firstNumber = getNextNumber(); removeNextSection(); myTimeMs = myTimeMs + firstNumber * dimensionValue; return this; } private long getNextNumber() { if (myNextUnitStartIndex == -1) { parseNext(); } String numberSubstring = myTimeText.substring(0, myNextUnitStartIndex); try { return Long.valueOf(numberSubstring); } catch (NumberFormatException e) { throw new BadRequestException("Could not parse number from '" + numberSubstring + "'"); } } @NotNull private String getNextUnit() { if (myNextUnitStartIndex == -1) { parseNext(); } return myTimeText.substring(myNextUnitStartIndex, myNextUnitEndIndex); } private void removeNextSection() { myTimeText = myTimeText.substring(myNextUnitEndIndex); myNextUnitStartIndex = -1; myNextUnitEndIndex = -1; } private void parseNext() { myNextUnitStartIndex = 0; try { while (Character.isDigit(myTimeText.charAt(myNextUnitStartIndex))) { myNextUnitStartIndex++; } } catch (IndexOutOfBoundsException e) { throw new BadRequestException("Could not parse remaining text '" + myTimeText + "': does not end with a textual unit"); } if (myNextUnitStartIndex == 0) { throw new BadRequestException("Could not parse remaining text '" + myTimeText + "': does not start with pattern "); } myNextUnitEndIndex = myNextUnitStartIndex; try { while (Character.isAlphabetic(myTimeText.charAt(myNextUnitEndIndex))) { myNextUnitEndIndex++; } } catch (IndexOutOfBoundsException e) { //ignore } if (myNextUnitEndIndex == myNextUnitStartIndex) { throw new BadRequestException("Could not parse remaining text '" + myTimeText + "': does not start with pattern "); } } public boolean isFullyParsed() { return myTimeText.isEmpty(); } } }