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 android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import com.jbirdvegas.mgerrit.objects.ServerVersion;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public abstract class SearchKeyword implements Parcelable {
public static final String TAG = "SearchKeyword";
private final String mOpName;
private final String mOpParam;
private String mOperator;
// Initialise the map of search keywords supported
private static final Set<Class<? extends SearchKeyword>> _CLASSES;
private static Map<String, Class<? extends SearchKeyword>> _KEYWORDS;
static {
_KEYWORDS = new HashMap<>();
_CLASSES = new HashSet<>();
// Add each search keyword here
_CLASSES.add(ChangeSearch.class);
_CLASSES.add(SubjectSearch.class);
_CLASSES.add(ProjectSearch.class);
_CLASSES.add(OwnerSearch.class);
_CLASSES.add(TopicSearch.class);
_CLASSES.add(BranchSearch.class);
_CLASSES.add(AgeSearch.class);
_CLASSES.add(NumberSearch.class);
// This will load the class calling the class's static block
for (Class<? extends SearchKeyword> clazz : _CLASSES) {
try {
Class.forName(clazz.getName());
} catch (ClassNotFoundException e) {
Log.e(TAG, String.format("Could not load class '%s'", clazz.getSimpleName()));
}
}
}
// TODO: All keywords are currently getting these operators. Make an overridable method
// to determine whether a keyword supports an operator. Or assume '=' and ignore it
// if it doesn't.
/** Supported searching operators - these are used directly in the SQL query */
protected static String[] operators = { "=", "<", ">", "<=", ">=" };
public SearchKeyword(String name, String param) {
this(name, null, param);
}
// We can allow nulls for the parameter but not the name
public SearchKeyword(String name, String operator, String param) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException(String.format("The keyword name of %s was not valid", name));
}
mOpName = name;
mOperator = operator;
mOpParam = param;
}
protected static SearchKeyword getInstance(Parcel in) {
return buildToken(in.readString());
}
protected static void registerKeyword(String opName, Class<? extends SearchKeyword> clazz) {
_KEYWORDS.put(opName, clazz);
}
public String getName() { return mOpName; }
public String getParam() { return mOpParam; }
public String getOperator() { return mOperator; }
@Override
public String toString() {
// Keywords with empty parameters are ignored
if (mOpParam == null || mOpParam.isEmpty()) return "";
StringBuilder builder = new StringBuilder().append(mOpName).append(":\"");
if (mOperator != null) builder.append(mOperator);
builder.append(mOpParam).append("\"");
return builder.toString();
}
@Contract("null -> false")
protected static boolean isParameterValid(String param) {
return param != null && !param.isEmpty();
}
/**
* Build a search keyword given a name and its parameter
* @param name The name of the keyword (a key of _KEYWORDS)
* @param param Arguments for the token - will not be processed
* @return A search keyword matching name:param
*/
private static SearchKeyword buildToken(@NotNull String name, String param) {
for (Entry<String, Class<? extends SearchKeyword>> entry : _KEYWORDS.entrySet()) {
if (name.equalsIgnoreCase(entry.getKey())) {
Constructor<? extends SearchKeyword> constructor;
try {
constructor = entry.getValue().getDeclaredConstructor(String.class);
return constructor.newInstance(param);
} catch (Exception e) {
Log.e(TAG, "Could not call constructor for " + name, e);
}
}
}
return null;
}
@Nullable
private static SearchKeyword buildToken(@NotNull String tokenStr) {
SearchKeyword keyword = null;
String[] s = tokenStr.split(":", 2);
if (s.length == 2) {
// Remove the beginning and ending double quotes
s[1] = s[1].replaceAll("^\"|\"$", "");
keyword = buildToken(s[0], s[1]);
} else if (tokenStr.startsWith("#")) {
keyword = buildToken("#", tokenStr.substring(1));
}
if (keyword == null) keyword = buildToken(SubjectSearch.OP_NAME, tokenStr.replaceAll("^\"|\"$", ""));
return keyword;
}
/**
* Given a raw search query, this will attempt to process it
* into the categories to search for (keywords) and their
* arguments.
* @param query A raw search query in the form that Gerrit uses
* @return A set of SearchKeywords that can be used to construct
* the database query
*/
public static Set<SearchKeyword> constructTokens(String query) {
Set<SearchKeyword> set = new HashSet<>();
StringBuilder currentToken = new StringBuilder();
for (int i = 0, n = query.length(); i < n; i++) {
char c = query.charAt(i);
if (Character.isWhitespace(c)) {
if (currentToken.length() > 0) {
addToSetIfNotNull(buildToken(currentToken.toString()), set);
currentToken.setLength(0);
}
} else if (c == '"') {
i = processTo(query, currentToken, i, '"');
} else if (c == '{') {
i = processTo(query, currentToken, i, '}');
} else {
currentToken.append(c);
}
}
// Have to check if a token was terminated by end of string
if (currentToken.length() > 0) {
addToSetIfNotNull(buildToken(currentToken.toString()), set);
}
return set;
}
public static String getQuery(Set<SearchKeyword> tokens) {
String query = "";
for (SearchKeyword token : tokens) {
query += token.toString() + " ";
}
return query.trim();
}
private static void addToSetIfNotNull(SearchKeyword token, Set<SearchKeyword> set) {
if (token != null) set.add(token);
}
public static String constructDbSearchQuery(Set<SearchKeyword> tokens) {
StringBuilder whereQuery = new StringBuilder();
Iterator<SearchKeyword> it = tokens.iterator();
while (it.hasNext()) {
SearchKeyword token = it.next();
if (token == null) continue;
whereQuery.append(token.buildSearch());
if (it.hasNext()) whereQuery.append(" AND ");
}
return whereQuery.toString();
}
public abstract String buildSearch();
/**
* Formats the bind argument for query binding.
* May be overriden to include wildcards in the parameter for like queries
*/
public String[] getEscapeArgument() {
return new String[] { getParam() };
}
/**
* Get the Gerrit search query that this keyword corresponds to.
* Some keywords do not have corresponding queries supported by Gerrit, so
* it is safe to return an empty string in that case. The default implementation
* returns an empty string.
* @param serverVersion The version of the Gerrit instance running on the server
*/
public String getGerritQuery(ServerVersion serverVersion) {
return "";
}
public static String replaceKeyword(final String query, final SearchKeyword keyword) {
Set<SearchKeyword> tokens = SearchKeyword.constructTokens(query);
tokens = replaceKeyword(tokens, keyword);
return SearchKeyword.getQuery(tokens);
}
public static Set<SearchKeyword> replaceKeyword(final Set<SearchKeyword> tokens,
SearchKeyword keyword) {
Set<SearchKeyword> retVal = removeKeyword(tokens, keyword.getClass());
if (isParameterValid(keyword.getParam())) {
retVal.add(keyword);
}
return retVal;
}
/**
* @param tokens A list of search keywords
* @param keyword An additional age search keyword to be added to the list
* @return A new set of search keywords, retaining only the oldest AgeSearch keyword
*/
public static Set<SearchKeyword> retainOldest(final Set<SearchKeyword> tokens,
@NotNull AgeSearch keyword) {
List<AgeSearch> ageSearches = new ArrayList<>();
List<SearchKeyword> otherSearches = new ArrayList<>();
ageSearches.add(keyword);
if (tokens.size() > 0) {
for (SearchKeyword o : tokens) {
if (o instanceof AgeSearch) ageSearches.add((AgeSearch) o);
else otherSearches.add(o);
}
Collections.sort(ageSearches);
otherSearches.add(ageSearches.get(0));
}
return new HashSet<>(otherSearches);
}
public static String addKeyword(String query, SearchKeyword keyword) {
if (keyword != null && isParameterValid(keyword.getParam())) {
Set<SearchKeyword> tokens = SearchKeyword.constructTokens(query);
tokens.add(keyword);
return SearchKeyword.getQuery(tokens);
}
return query;
}
public static Set<SearchKeyword> removeKeyword(Set<SearchKeyword> tokens,
Class<? extends SearchKeyword> clazz) {
Iterator<SearchKeyword> it = tokens.iterator();
while (it.hasNext()) {
Object token = it.next();
if (token.getClass().equals(clazz)) {
it.remove();
}
}
return tokens;
}
public static int findKeyword(Set<SearchKeyword> tokens, Class<? extends SearchKeyword> clazz) {
return findKeyword(tokens, clazz, 0);
}
public static int findKeyword(Set<SearchKeyword> tokens, Class<? extends SearchKeyword> clazz,
int start) {
if (start < 0) start = 0;
int i = 0;
for (Object token : tokens) {
if (i < start) i++;
else if (token.getClass().equals(clazz)) return i;
else i++;
}
return -1;
}
protected static String extractOperator(String param) {
String op = "=";
for (String operator : operators) {
if (param.startsWith(operator)) op = operator;
}
// '==' also refers to '='
if (param.startsWith("==")) op = "=";
return op;
}
private static int processTo(String query, StringBuilder currentToken, int i, char token) {
if (i + 1 >= query.length()) {
return i + 1;
} else {
int index = query.indexOf(token, i + 1);
if (index < 0) return i + 1;
// We don't want to store these braces
currentToken.append(query.substring(i + 1, index));
return index;
}
}
/* Whether this query can return more than one result */
public boolean multipleResults() {
return false;
}
// --- Parcelable methods
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(toString());
}
public static final Parcelable.Creator<SearchKeyword> CREATOR
= new Parcelable.Creator<SearchKeyword>() {
public SearchKeyword createFromParcel(Parcel source) {
return getInstance(source);
}
public SearchKeyword[] newArray(int size) {
return new SearchKeyword[size];
}
};
}