package com.jbirdvegas.mgerrit.search;
/*
* Copyright (C) 2013 Android Open Kang Project (AOKP)
* Author: Evan Conway (P4R4N01D), 2013
*
* 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.
*/
import com.jbirdvegas.mgerrit.database.UserChanges;
import com.jbirdvegas.mgerrit.objects.ServerVersion;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.DurationFieldType;
import org.joda.time.Instant;
import org.joda.time.Period;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AgeSearch extends SearchKeyword implements Comparable<AgeSearch> {
public static final String OP_NAME = "age";
// Set only if a relative time period was given. Should not be set if mInstant is set
private Period mPeriod;
// Set only if an absolute time was given. Should not be set if mPeriod is set
private Instant mInstant;
/**
* An array of supported time units and their corresponding meaning
* as a DurationFieldType. This is used in the parsing of the query
* parameter.
*/
private static final HashMap<String, DurationFieldType> replacers;
static {
replacers = new HashMap<>();
replacers.put("s", DurationFieldType.seconds());
replacers.put("sec", DurationFieldType.seconds());
replacers.put("secs", DurationFieldType.seconds());
replacers.put("second", DurationFieldType.seconds());
replacers.put("seconds", DurationFieldType.seconds());
replacers.put("m", DurationFieldType.minutes());
replacers.put("min", DurationFieldType.minutes());
replacers.put("mins", DurationFieldType.minutes());
replacers.put("minute", DurationFieldType.minutes());
replacers.put("minutes", DurationFieldType.minutes());
replacers.put("h", DurationFieldType.hours());
replacers.put("hr", DurationFieldType.hours());
replacers.put("hrs", DurationFieldType.hours());
replacers.put("hour", DurationFieldType.hours());
replacers.put("hours", DurationFieldType.hours());
replacers.put("d", DurationFieldType.days());
replacers.put("day", DurationFieldType.days());
replacers.put("days", DurationFieldType.days());
replacers.put("w", DurationFieldType.weeks());
replacers.put("week", DurationFieldType.weeks());
replacers.put("weeks", DurationFieldType.weeks());
replacers.put("mon", DurationFieldType.months());
replacers.put("mons", DurationFieldType.months());
replacers.put("mth", DurationFieldType.months());
replacers.put("mths", DurationFieldType.months());
replacers.put("month", DurationFieldType.months());
replacers.put("months", DurationFieldType.months());
replacers.put("y", DurationFieldType.years());
replacers.put("yr", DurationFieldType.years());
replacers.put("yrs", DurationFieldType.years());
replacers.put("year", DurationFieldType.years());
replacers.put("years", DurationFieldType.years());
}
/** Used for serialising the period into a string and must be output
* in a format that can be re-parsed later */
protected static PeriodFormatter periodParser = new PeriodFormatterBuilder()
.appendYears().appendSuffix(" years ")
.appendMonths().appendSuffix(" months ")
.appendWeeks().appendSuffix(" weeks ")
.appendDays().appendSuffix(" days ")
.appendHours().appendSuffix(" hours ")
.appendMinutes().appendSuffix(" minutes ")
.appendSeconds().appendSuffix(" seconds")
.toFormatter();
protected static final DateTimeFormatter sInstantFormatter;
static {
registerKeyword(OP_NAME, AgeSearch.class);
sInstantFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").withLocale(Locale.US);
}
public AgeSearch(String param, String operator) {
super(OP_NAME, operator, param);
parseDate(param);
}
public AgeSearch(String param) {
// We need to extract the operator and the parameter from the string
this(extractParameter(param), extractOperator(param));
}
public AgeSearch(long timestamp, String operator) {
super(OP_NAME, operator, String.valueOf(timestamp));
mInstant = new Instant(timestamp);
mPeriod = null;
}
@Override
public String buildSearch() {
String operator = getOperator();
if ("=".equals(operator)) {
/* Note that since datetime is an SQLite function it must be included
* directly in the query */
return UserChanges.C_UPDATED + " BETWEEN datetime(?) AND datetime(?)";
} else {
return UserChanges.C_UPDATED + " " + operator + " datetime(?)";
}
}
@Override
public String[] getEscapeArgument() {
DateTime now = new DateTime();
Period period = mPeriod;
// Equals: we need to do some arithmetic to get a range from the period
if ("=".equals(getOperator())) {
if (period == null) {
period = new Period(mInstant, Instant.now());
}
DateTime earlier = now.minus(adjust(period, +1));
DateTime later = now.minus(period);
return new String[] { earlier.toString(), later.toString() };
} else {
if (period == null) {
return new String[] { mInstant.toString() };
}
return new String[] { now.minus(period).toString() };
}
}
private static String extractParameter(String param) {
return param.replaceFirst("[=<>]+", "");
}
private void parseDate(String param) {
try {
if (param.endsWith("Z")) {
/* The string representation of an instant includes a Z at the end, but this is not
* a valid format for the parser. */
String newParam = param.substring(0, param.length() - 1);
mInstant = Instant.parse(newParam, ISODateTimeFormat.localDateOptionalTimeParser());
} else {
mInstant = Instant.parse(param, ISODateTimeFormat.localDateOptionalTimeParser());
}
mPeriod = null;
} catch (IllegalArgumentException ignored) {
mPeriod = toPeriod(param);
mInstant = null;
}
}
@Override
public String toString() {
return toString(OP_NAME);
}
public String toString(String keywordName) {
/* Use the same format as the one initially provided. I.e. if a
* relative time period was set initially, we want a relative period
* to come back out. Otherwise it is a different search */
String operator = getOperator() ;
if ("=".equals(operator)) operator = "";
String string = keywordName + ":\"" + operator;
if (mPeriod != null) {
return string + periodParser.print(mPeriod) + '"';
} else {
return string + mInstant.toString() + '"';
}
}
/**
* Parses a string into a Period object according to the replacers
* mapping. This allows for duplicate fields (e.g. seconds being
* declared twice as in "2s 3 sec") with the duplicate fields being
* added together (the above example would be the same as "5 seconds").
* The order of the fields is not important.
*
* @param dateOffset The parameter without the operator. If the operator
* is passed in it will be ignored
* @return A period corresponding to the parsed input string
*/
private Period toPeriod(final String dateOffset) {
String regexp = "(\\d+) *([a-zA-z]+)";
Period period = new Period();
if (dateOffset == null || dateOffset.isEmpty())
return period;
Pattern pattern = Pattern.compile(regexp);
Matcher matcher = pattern.matcher(dateOffset);
while (matcher.find()) {
String svalue = matcher.toMatchResult().group(1);
DurationFieldType fieldType = replacers.get(matcher.toMatchResult().group(2));
if (fieldType != null) {
// Note that both these methods do not modify their objects
period = period.withFieldAdded(fieldType, Integer.parseInt(svalue));
}
}
return period;
}
/**
* Adds adjustment to the shortest set time range in period. E.g.
* period("5 days 3 hours", 1) -> "5 days 4 hours". This will fall
* back to adjusting years if no field in the period is set.
* @param period The period to be adjusted
* @param adjustment The adjustment. Note that positive values will result
* in larger periods and an earlier time
* @return The adjusted period
*/
private Period adjust(final Period period, int adjustment) {
if (adjustment == 0) return period;
// Order is VERY important here
LinkedHashMap<Integer, DurationFieldType> map = new LinkedHashMap<>();
map.put(period.getSeconds(), DurationFieldType.seconds());
map.put(period.getMinutes(), DurationFieldType.minutes());
map.put(period.getHours(), DurationFieldType.hours());
map.put(period.getDays(), DurationFieldType.days());
map.put(period.getWeeks(), DurationFieldType.weeks());
map.put(period.getMonths(), DurationFieldType.months());
map.put(period.getYears(), DurationFieldType.years());
for (Map.Entry<Integer, DurationFieldType> entry : map.entrySet()) {
if (entry.getKey() > 0) {
return period.withFieldAdded(entry.getValue(), adjustment);
}
}
// Fall back to modifying years
return period.withFieldAdded(DurationFieldType.years(), adjustment);
}
/**
* Calculates the number of days spanned in a period assuming 365 days per year, 30 days per
* month, 7 days per week, 24 hours per day, 60 minutes per hour and 60 seconds per minute.
* @param period A period to retrieve the number of standard days for
* @return The number of days spanned by the period.
*/
protected static int getDaysInPeriod(final Period period) {
int totalDays = 0;
Period temp = new Period(period);
if (period.getYears() > 0) {
int years = period.getYears();
totalDays += 365*years;
temp = temp.minusYears(years);
}
if (period.getMonths() > 0) {
int months = period.getMonths();
totalDays += 30*period.getMonths();
temp = temp.minusMonths(months);
}
return totalDays + temp.toStandardDays().getDays();
}
@Override
public String getGerritQuery(ServerVersion serverVersion) {
String operator = getOperator();
if ("<=".equals(operator) || "<".equals(operator)) {
return BeforeSearch._getGerritQuery(this, serverVersion);
} else if (">=".equals(operator) || ">".equals(operator)) {
return AfterSearch._getGerritQuery(this, serverVersion);
}
if (serverVersion != null &&
serverVersion.isFeatureSupported(ServerVersion.VERSION_BEFORE_SEARCH) &&
mInstant != null) {
// Use a combination of before and after to get an interval
if (mPeriod == null) {
mPeriod = new Period(mInstant, Instant.now());
}
DateTime now = new DateTime();
DateTime earlier = now.minus(adjust(mPeriod, +1));
DateTime later = now.minus(mPeriod);
SearchKeyword newer = new AfterSearch(earlier.toString());
SearchKeyword older = new BeforeSearch(later.toString());
return newer.getGerritQuery(serverVersion) + "+" + older.getGerritQuery(serverVersion);
} else {
// Need to leave off the operator and make sure we are using relative format
/* Gerrit only supports specifying one time unit, so we will normalize the period
* into days. */
return OP_NAME + ":" + String.valueOf(toDays()) + "d";
}
}
protected int toDays() {
// Need to leave off the operator and make sure we are using relative format
Period period = mPeriod;
if (period == null) {
period = new Period(mInstant, Instant.now());
}
/* Gerrit only supports specifying one time unit, so we will normalize the period
* into days. */
return getDaysInPeriod(period);
}
protected Period getPeriod() { return mPeriod; }
protected Instant getInstant() { return mInstant; }
protected static Instant getInstantFromPeriod(Period period) {
Instant now = new Instant();
Duration duration = period.toDurationTo(now);
return now.minus(duration);
}
@Override
public int compareTo(AgeSearch rhs) {
if (this.equals(rhs)) return 0;
else if (mInstant != null && rhs.mInstant != null) {
return mInstant.compareTo(rhs.mInstant);
} else {
// Compare the normalised period format (i.e. the period in days)
return toDays() - rhs.toDays();
}
}
}