package org.ovirt.engine.core.searchbackend; import java.math.BigDecimal; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.ovirt.engine.core.common.businessentities.DateEnumForSearch; import org.ovirt.engine.core.common.businessentities.Tags; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.interfaces.ITagsHandler; import org.ovirt.engine.core.common.utils.Pair; import org.ovirt.engine.core.compat.DateTime; import org.ovirt.engine.core.compat.DayOfWeek; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.compat.IntegerCompat; import org.ovirt.engine.core.compat.Regex; import org.ovirt.engine.core.compat.StringFormat; import org.ovirt.engine.core.compat.StringHelper; import org.ovirt.engine.core.compat.TimeSpan; /** * A base class for all condition field auto completers */ public class BaseConditionFieldAutoCompleter extends BaseAutoCompleter implements IConditionFieldAutoCompleter { protected final Map<String, List<ValueValidationFunction>> validationDict = new HashMap<>(); private final Map<String, Class<?>> typeDict = new HashMap<>(); protected final Map<String, String> columnNameDict = new HashMap<>(); /** * key: UI column identifier * value: list of 'ORDER BY' elements */ protected final Map<String, List<SyntaxChecker.SortByElement>> sortableFieldDict = new HashMap<>(); protected final List<String> notFreeTextSearchableFieldsList = new ArrayList<>(); protected Set<String> verbsWithMultipleValues = new HashSet<>(); /** * Gets the LIKE clause syntax for non case-sensitive search * * @return the LIKE syntax according to current DBEngine. */ public static String getLikeSyntax(boolean caseSensitive) { // for tests we don't have the Config class initialized // also if caseSensitive flag is set we will use LIKE if (Config.getConfigUtils() == null || caseSensitive) { return "LIKE"; } else { return Config.<String>getValue(ConfigValues.DBLikeSyntax); } } public String getMatchingSyntax(String fieldName, boolean positive, boolean caseSensitive) { return verbsWithMultipleValues.contains(fieldName) ? new StringBuilder(positive ? "" : " !").append(caseSensitive ? "~" : "~*").toString() : new StringBuilder(positive ? "" : " NOT ").append(getLikeSyntax(caseSensitive)).toString(); } /** * Gets the I18N prefix used for value compare. */ public static String getI18NPrefix() { // for tests we don't have the Config class initialized if (Config.getConfigUtils() == null) { return ""; } else { return Config.<String>getValue(ConfigValues.DBI18NPrefix); } } public static ITagsHandler tagsHandler = null; public Map<String, Class<?>> getTypeDictionary() { return typeDict; } protected void buildBasicValidationTable() { for (String key : verbs) { final List<ValueValidationFunction> curList = new ArrayList<>(); final Class<?> curType = typeDict.get(key); if (curType == BigDecimal.class) { curList.add(validDecimal); } else if (curType == Integer.class) { curList.add(validInteger); } else if (curType == Date.class) { curList.add(validDateTime); } else if (curType == TimeSpan.class) { curList.add(validTimeSpan); } final IConditionValueAutoCompleter tmp = getFieldValueAutoCompleter(key); if (tmp != null) { if (tmp.getClass() == DateEnumValueAutoCompleter.class) { curList.add(validateDateEnumValueByValueAC); } else { curList.add(validateFieldValueByValueAC); } } validationDict.put(key, curList); } } @Override public boolean validateFieldValue(String fieldName, String fieldValue) { if (validationDict.containsKey(fieldName)) { final List<ValueValidationFunction> validationList = validationDict.get(fieldName); for (ValueValidationFunction curValidationFunc : validationList) { if (!curValidationFunc.isValid(fieldName, fieldValue)) { return false; } } } return true; } @Override public String getDbFieldName(String fieldName) { String retval = null; if (columnNameDict.containsKey(fieldName)) { retval = columnNameDict.get(fieldName); } return retval; } @Override public List<SyntaxChecker.SortByElement> getSortByElements(String fieldName) { if (sortableFieldDict.containsKey(fieldName)) { return sortableFieldDict.get(fieldName); } return Collections.singletonList( new SyntaxChecker.SortByElement(getDbFieldName(fieldName), true)); } @Override public Class<?> getDbFieldType(String fieldName) { Class<?> retval = null; if (typeDict.containsKey(fieldName)) { retval = typeDict.get(fieldName); } return retval; } // FIXME Probably Not Hibernate Friendly @Override public final String buildFreeTextConditionSql(String tableName, String relations, String value, boolean caseSensitive) { StringBuilder sb = new StringBuilder(" ( "); boolean firstTime = true; if (!StringHelper.isNullOrEmpty(value) && !"''".equals(value)) { value = StringFormat.format(getI18NPrefix() + "'%%%1$s%%'", StringHelper.trim(value, '\'')); } if ("=".equals(relations)) { relations = getLikeSyntax(caseSensitive); } else if ("!=".equals(relations)) { relations = "NOT " + getLikeSyntax(caseSensitive); } // Sort according to the value (real column name) in order not to rely on random access from map SortedSet<Map.Entry<String, String>> sortedEntrySet = new TreeSet<>(new ColNameMapEntryComparator()); for (Map.Entry<String, String> entry : columnNameDict.entrySet()) { sortedEntrySet.add(entry); } for (Map.Entry<String, String> columnNameEntry : sortedEntrySet) { if (typeDict.get(columnNameEntry.getKey()) == String.class && !notFreeTextSearchableFieldsList.contains(columnNameEntry.getKey())) { if (firstTime) { firstTime = false; } else { sb.append(" OR "); } sb.append(StringFormat.format(" %1$s.%2$s %3$s %4$s", tableName, columnNameEntry.getValue(), relations, value)); } } sb.append(" ) "); return sb.toString(); } static final Regex validChar = new Regex("^[^\\<\\>&^!']*$"); public static final ValueValidationFunction validCharacters = (field, value) -> validChar.isMatch(value); public static final ValueValidationFunction validDateTime = (field, value) -> { Date test = DateUtils.parse(value); if (test != null) { return true; } else { // check for enum for (DateEnumForSearch val : DateEnumForSearch.values()) { if (val.name().equalsIgnoreCase(value)) { return true; } } // check for week before for (DayOfWeek day : DayOfWeek.class.getEnumConstants()) { if (day.toString().equalsIgnoreCase(value)) { return true; } } } return false; }; public static final ValueValidationFunction validTimeSpan = (field, value) -> TimeSpan.tryParse(value) != null; public static final ValueValidationFunction validInteger = (field, value) -> IntegerCompat.tryParse(value) != null; public static final ValueValidationFunction validDecimal = (field, value) -> { try { new BigDecimal(value); // No assignment, we just want to create a new instance and to see if there's an // Exception return true; } catch (NumberFormatException e) { return false; } }; public final ValueValidationFunction validateDateEnumValueByValueAC = (field, value) -> { boolean retval = true; IConditionValueAutoCompleter vlaueAc = getFieldValueAutoCompleter(field); if (vlaueAc != null) { // check if this enum first retval = vlaueAc.validate(value); } if (!retval) { // check for week before for (DayOfWeek day : DayOfWeek.values()) { if (day.toString().equalsIgnoreCase(value)) { return true; } } } if (!retval) { // check for free date retval = DateUtils.parse(StringHelper.trim(value, '\'')) != null; } return retval; }; public final ValueValidationFunction validateFieldValueByValueAC = (field, value) -> { boolean retval = true; IConditionValueAutoCompleter vlaueAc = getFieldValueAutoCompleter(field); if (vlaueAc != null) { retval = vlaueAc.validate(value); } return retval; }; @Override public IAutoCompleter getFieldRelationshipAutoCompleter(String fieldName) { return null; } @Override public IConditionValueAutoCompleter getFieldValueAutoCompleter(String fieldName) { return null; } @Override public void formatValue(String fieldName, Pair<String, String> pair, boolean caseSensitive) { if (fieldName == null) { return; } if ("TIME".equals(fieldName) || "CREATIONDATE".equals(fieldName)) { Date temp = DateUtils.parse(StringHelper.trim(pair.getSecond(), '\'')); DateTime result; if (temp == null) { result = dealWithDateEnum(pair.getSecond()); } else { result = new DateTime(temp); } if (pair.getFirst() != null && pair.getFirst().equals("=")) { pair.setFirst("between"); DateTime nextDay = result.addDays(1); pair.setSecond(StringFormat.format("'%1$s' and '%2$s'", result.toString(DateUtils.getFormat(DateFormat.DEFAULT, DateFormat.SHORT)), nextDay.toString(DateUtils.getFormat(DateFormat.DEFAULT, DateFormat.SHORT)))); } else { // ">" or "<" // value.argvalue = String.format("'%1$s'", result); pair.setSecond(StringFormat.format("'%1$s'", result.toString(DateUtils.getFormat(DateFormat.DEFAULT, DateFormat.SHORT)))); } } else if ("TAG".equals(fieldName)) { pair.setSecond(pair.getSecond().startsWith("N'") ? pair.getSecond().substring(2) : pair.getSecond()); if (pair.getFirst() != null && pair.getFirst().equals("=")) { pair.setFirst("IN"); pair.setSecond(StringHelper.trim(pair.getSecond(), '\'')); Tags tag = tagsHandler.getTagByTagName(pair.getSecond()); if (tag != null) { pair.setSecond( StringFormat.format("(%1$s)", tagsHandler.getTagNameAndChildrenNames(tag.getTagId()))); } else { pair.setSecond(StringFormat.format("('%1$s')", Guid.Empty)); } } else if (pair.getFirst() != null && (pair.getFirst().equals("LIKE") || pair.getFirst().equals("ILIKE"))) { pair.setFirst("IN"); pair.setSecond(StringHelper.trim(pair.getSecond(), '\'').replace("%", "*")); String IDs = tagsHandler.getTagNamesAndChildrenNamesByRegExp(pair.getSecond()); if (StringHelper.isNullOrEmpty(IDs)) { pair.setSecond(StringFormat.format("('%1$s')", Guid.Empty)); } else { pair.setSecond(StringFormat.format("(%1$s)", IDs)); } } } } // private static final String DATE_FORMAT = "MMM dd,yyyy"; private static DateTime dealWithDateEnum(String value) { DateTime formatedValue = new DateTime(); final Integer result = IntegerCompat.tryParse(StringHelper.trim(value, '\'')); if (result != null) { DateEnumForSearch dateEnumVal = DateEnumForSearch.forValue(result); switch (dateEnumVal) { case Today: formatedValue = DateTime.getNow(); break; case Yesterday: formatedValue = DateTime.getNow().addDays(-1); break; default: break; } } else { for (int i = -2; i > -8; i--) { if (DateUtils.getDayOfWeek(i).equalsIgnoreCase(StringHelper.trim(value, '\''))) { formatedValue = DateTime.getNow().addDays(i); return formatedValue.resetToMidnight(); } } } return formatedValue.resetToMidnight(); } @Override public String buildConditionSql(String objName, String fieldName, String customizedValue, String customizedRelation, String tableName, boolean caseSensitive) { Pair<String, String> pair = new Pair<>(); pair.setFirst(customizedRelation); pair.setSecond(customizedValue); formatValue(fieldName, pair, caseSensitive); if (("''".equals(pair.getSecond()) || "'null'".equalsIgnoreCase(pair.getSecond())) && ("=".equals(pair.getFirst()) || "!=".equals(pair.getFirst()))) { String nullRelation = "=".equals(pair.getFirst()) ? "IS" : "IS NOT"; return StringFormat.format("(%1$s.%2$s %3$s NULL)", tableName, getDbFieldName(fieldName), nullRelation); } else { SearchObjectAutoCompleter.EntitySearchInfo info = SearchObjectAutoCompleter.getEntitySearchInfo(objName); // Check if value is comma delimited list, this apply for example to shared disk or shared ISO domain if (info.commaDelimitedListColumns != null && info.commaDelimitedListColumns.contains(fieldName.toLowerCase())) { return StringFormat.format("%1$s %2$s ANY(string_to_array(%3$s.%4$s, ','))", pair.getSecond(), pair.getFirst(), tableName, getDbFieldName(fieldName)); } String formatString; if (pair.getFirst().equalsIgnoreCase("LIKE") || pair.getFirst().equalsIgnoreCase("ILIKE")) { formatString = " (%1$s.%2$s IS NULL OR %1$s.%2$s %3$s %4$s) "; } else { formatString = " %1$s.%2$s %3$s %4$s "; } return StringFormat.format(formatString, tableName, getDbFieldName(fieldName), pair.getFirst(), SyntaxChecker.escapeUnderScore(pair.getSecond(), customizedRelation)); } } @Override public String getWildcard(String fieldName) { return verbsWithMultipleValues.contains(fieldName) ? ".*" : "%"; } public static class ColNameMapEntryComparator implements Comparator<Map.Entry<String, String>> { @Override public int compare(Map.Entry<String, String> e1, Map.Entry<String, String> e2) { return e1.getValue().compareTo(e2.getValue()); } } }