From 021f0bf38cbb2a723cdbe59941859f3eae84fbf5 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Wed, 29 Jan 2020 00:55:33 -0500 Subject: [PATCH] support for multiple sort orders --- .../wizard/model/ldap/LdapEntity.java | 6 +- .../wizard/model/search/SearchQuery.java | 70 +++++++------------ .../wizard/model/search/SearchSort.java | 30 ++++++++ .../wizard/model/search/SortOrder.java | 16 +++++ .../org/cobbzilla/wizard/dao/AbstractDAO.java | 3 +- .../wizard/dao/SqlViewSearchHelper.java | 42 ++++++----- .../wizard/ldap/LdapServiceBase.java | 16 +++-- 7 files changed, 110 insertions(+), 73 deletions(-) create mode 100644 wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchSort.java create mode 100644 wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SortOrder.java diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/ldap/LdapEntity.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/ldap/LdapEntity.java index 9b0bcc9..33e7626 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/ldap/LdapEntity.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/ldap/LdapEntity.java @@ -6,8 +6,8 @@ import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.collection.SingletonSet; import org.cobbzilla.util.reflect.ReflectionUtil; -import org.cobbzilla.wizard.model.search.SearchQuery; import org.cobbzilla.wizard.model.UniquelyNamedEntity; +import org.cobbzilla.wizard.model.search.SortOrder; import javax.persistence.Transient; import java.util.*; @@ -195,8 +195,8 @@ public abstract class LdapEntity extends UniquelyNamedEntity { private static Map> comparatorCache = new ConcurrentHashMap<>(); - public static Comparator comparator (final String field, SearchQuery.SortOrder order) { - final SearchQuery.SortOrder sort = order == null ? SearchQuery.SortOrder.ASC : order; + public static Comparator comparator (final String field, SortOrder order) { + final SortOrder sort = order == null ? SortOrder.ASC : order; final String cacheKey = field + ":" + order; Comparator comp = comparatorCache.get(cacheKey); if (comp == null) { diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchQuery.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchQuery.java index 6f009cd..cc29329 100644 --- a/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchQuery.java +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchQuery.java @@ -1,6 +1,5 @@ package org.cobbzilla.wizard.model.search; -import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,12 +10,9 @@ import org.cobbzilla.util.collection.ArrayUtil; import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.util.json.JsonUtil; import org.cobbzilla.util.string.StringUtil; -import org.cobbzilla.wizard.model.BasicConstraintConstants; -import org.cobbzilla.wizard.validation.ValidEnum; import java.util.Arrays; -import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.wizard.model.Identifiable.CTIME; @@ -38,14 +34,6 @@ public class SearchQuery { public static final int MAX_SORTFIELD_LENGTH = 50; public static final String DEFAULT_SORT_FIELD = CTIME; - public enum SortOrder { - ASC, DESC; - @JsonCreator public static SortOrder create(String val) { return valueOf(val.toUpperCase()); } - public boolean isAscending () { return this == ASC; } - public boolean isDescending () { return this == DESC; } - } - public static final String DEFAULT_SORT = SortOrder.DESC.name(); - public static final SearchQuery DEFAULT_PAGE = new SearchQuery(); public static final SearchQuery FIRST_RESULT = new SearchQuery(1, 1); public static final int INFINITE = Integer.MAX_VALUE; @@ -63,16 +51,16 @@ public class SearchQuery { this.setPageNumber(other.getPageNumber()); this.setPageSize(other.getPageSize()); this.setFilter(other.getFilter()); - this.setSortField(other.getSortField()); - this.setSortOrder(other.getSortOrder()); + this.setSorts(other.getSorts()); this.setBounds(other.getBounds()); } public SearchQuery(Integer pageNumber, Integer pageSize, String sortField, String sortOrder, String filter, NameAndValue[] bounds) { if (pageNumber != null) setPageNumber(pageNumber); if (pageSize != null) setPageSize(pageSize); - if (sortField != null) this.sortField = sortField; - if (sortOrder != null) this.sortOrder = SortOrder.valueOf(sortOrder).name(); + if (sortField != null) { + addSort(new SearchSort(sortField, SortOrder.fromString(sortOrder))); + } if (filter != null) this.filter = filter; this.bounds = bounds; } @@ -94,7 +82,7 @@ public class SearchQuery { } private static String normalizeSortOrder(SortOrder sortOrder) { - return (sortOrder == null) ? SearchQuery.DEFAULT_SORT : sortOrder.name(); + return (sortOrder == null) ? SortOrder.DEFAULT_SORT : sortOrder.name(); } public static SearchQuery singleResult (String sortField, SortOrder sortOrder) { @@ -126,36 +114,18 @@ public class SearchQuery { return isInfinitePage() || pageSize > MAX_PAGE_BUFFER ? MAX_PAGE_BUFFER : pageSize; } - @Setter private String sortField; - public String getSortField() { - if (empty(sortField)) return null; - if (sortField.contains(";")) die("invalid sort: "+sortField); - - // only return the first several chars, to thwart a hypothetical injection attack - // more sophisticated than the classic 'add a semi-colon then do something nefarious' - final String sort = empty(sortField) ? DEFAULT_SORT_FIELD : sortField; - return StringUtil.prefix(sort, MAX_SORTFIELD_LENGTH); - } - @JsonIgnore public boolean getHasSortField () { return sortField != null; } - - @ValidEnum(type=SortOrder.class, emptyOk=true, message= BasicConstraintConstants.ERR_SORT_ORDER_INVALID) - @Getter private String sortOrder = SearchQuery.DEFAULT_SORT; - public SearchQuery setSortOrder(Object thing) { - if (thing == null) { - sortOrder = null; - } else if (thing instanceof SortOrder) { - sortOrder = ((SortOrder) thing).name(); + @Getter @Setter private SearchSort[] sorts; + @JsonIgnore public boolean hasSorts() { return !empty(sorts); } + public boolean hasSort(String field) { return !empty(sorts) && Arrays.stream(sorts).anyMatch(s -> s.getSortField().equals(field)); } + public SearchQuery addSort(SearchSort sort) { + if (sorts == null) { + sorts = new SearchSort[] {sort}; } else { - sortOrder = thing.toString(); + sorts = ArrayUtil.append(sorts, sort); } return this; } - @JsonIgnore public SortOrder getSortType () { return sortOrder == null ? null : SortOrder.valueOf(sortOrder); } - - public SearchQuery sortAscending () { sortOrder = SortOrder.ASC.name(); return this; } - public SearchQuery sortDescending () { sortOrder = SortOrder.DESC.name(); return this; } - @Setter private String filter = null; public String getFilter() { // only return the first several chars, to thwart a hypothetical injection attack. @@ -194,8 +164,7 @@ public class SearchQuery { if (getPageSize() != that.getPageSize()) return false; if (!Arrays.equals(that.bounds, bounds)) return false; if (filter != null ? !filter.equals(that.filter) : that.filter != null) return false; - if (sortField != null ? !sortField.equals(that.sortField) : that.sortField != null) return false; - if (sortOrder != null ? !sortOrder.equals(that.sortOrder) : that.sortOrder != null) return false; + if (!Arrays.equals(that.sorts, sorts)) return false; if (!Arrays.equals(that.fields, fields)) return false; return true; } @@ -203,11 +172,20 @@ public class SearchQuery { @Override public int hashCode() { int result = getPageNumber(); result = 31 * result + getPageSize(); - result = 31 * result + (sortField != null ? sortField.hashCode() : 0); - result = 31 * result + (sortOrder != null ? sortOrder.hashCode() : 0); + result = 31 * result + (hasSorts() ? Arrays.deepHashCode(this.sorts) : 0); result = 31 * result + (filter != null ? filter.hashCode() : 0); result = 31 * result + (bounds != null ? Arrays.hashCode(bounds) : 0); result = 31 * result + (fields != null ? Arrays.hashCode(fields) : 0); return result; } + + public String hsqlSortClause(String entityAlias) { + if (!hasSorts()) return null; + final StringBuilder b = new StringBuilder(); + for (SearchSort s : sorts) { + if (b.length() > 0) b.append(", "); + b.append(entityAlias).append(".").append(s.getSortField()).append(" ").append(s.getSortOrder().name()); + } + return b.toString(); + } } diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchSort.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchSort.java new file mode 100644 index 0000000..91c5315 --- /dev/null +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SearchSort.java @@ -0,0 +1,30 @@ +package org.cobbzilla.wizard.model.search; + +import lombok.*; + +@NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of={"sortField", "sortOrder"}) +public class SearchSort { + + @Getter @Setter private String sortField; + @Getter @Setter private SortOrder sortOrder = SortOrder.ASC; + + public SearchSort(String sort) { + if (sort.startsWith("+") || sort.startsWith(" ")) { + sortField = sort.substring(1).trim(); + sortOrder = SortOrder.ASC; + } else if (sort.startsWith("-")) { + sortField = sort.substring(1).trim(); + sortOrder = SortOrder.DESC; + } else if (sort.endsWith("+") || sort.endsWith(" ")) { + sortField = sort.substring(0, sort.length()-1).trim(); + sortOrder = SortOrder.ASC; + } else if (sort.endsWith("-")) { + sortField = sort.substring(0, sort.length()-1).trim(); + sortOrder = SortOrder.DESC; + } else { + sortField = sort.trim(); + sortOrder = SortOrder.ASC; + } + } + +} diff --git a/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SortOrder.java b/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SortOrder.java new file mode 100644 index 0000000..c3a05ca --- /dev/null +++ b/wizard-common/src/main/java/org/cobbzilla/wizard/model/search/SortOrder.java @@ -0,0 +1,16 @@ +package org.cobbzilla.wizard.model.search; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum SortOrder { + + ASC, DESC; + + public static final String DEFAULT_SORT = DESC.name(); + + @JsonCreator public static SortOrder fromString(String val) { return valueOf(val.toUpperCase()); } + + public boolean isAscending () { return this == ASC; } + public boolean isDescending () { return this == DESC; } + +} diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java index 1f51085..d255d24 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/AbstractDAO.java @@ -303,7 +303,8 @@ public abstract class AbstractDAO implements DAO { .append(filterClause); final String countQuery = "select count(*) " + qBuilder.toString(); - final String query = qBuilder.append(" order by ").append(entityAlias).append(".").append(searchQuery.getSortField()).append(" ").append(searchQuery.getSortType().name()).toString(); + if (searchQuery.hasSorts()) qBuilder.append(searchQuery.hsqlSortClause(entityAlias)); + final String query = qBuilder.toString(); List results = query(query, searchQuery, params, values); final int totalCount = Integer.valueOf(""+query(countQuery, SearchQuery.INFINITE_PAGE, params, values).get(0)); diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java index bfd9326..a2e8762 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/dao/SqlViewSearchHelper.java @@ -4,8 +4,10 @@ import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.util.jdbc.ResultSetBean; import org.cobbzilla.util.reflect.ReflectionUtil; +import org.cobbzilla.util.string.StringUtil; import org.cobbzilla.wizard.model.Identifiable; import org.cobbzilla.wizard.model.search.SearchQuery; +import org.cobbzilla.wizard.model.search.SearchSort; import org.cobbzilla.wizard.model.search.SqlViewField; import org.cobbzilla.wizard.model.search.SqlViewSearchResult; import org.cobbzilla.wizard.server.config.PgRestServerConfiguration; @@ -23,7 +25,7 @@ import static org.cobbzilla.util.daemon.DaemonThreadFactory.fixedPool; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; -import static org.cobbzilla.wizard.model.search.SearchQuery.DEFAULT_SORT; +import static org.cobbzilla.wizard.model.search.SortOrder.ASC; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Slf4j @@ -57,14 +59,19 @@ public class SqlViewSearchHelper { } } - final String sort; - final String sortedField; - if (searchQuery.getHasSortField()) { - sortedField = dao.getSortField(searchQuery.getSortField()); - sort = sortedField + " " + searchQuery.getSortOrder(); + final StringBuilder sort = new StringBuilder(); + final List sortedFields = new ArrayList<>(); + if (searchQuery.hasSorts()) { + for (SearchSort s : searchQuery.getSorts()) { + final String sortField = dao.getSortField(s.getSortField()); + sortedFields.add(sortField); + if (sort.length() > 0) sort.append(", "); + sort.append(sortField).append(" ").append(s.getSortOrder().name()); + } } else { - sort = dao.getDefaultSort(); - sortedField = sort.split(" ")[0]; + final String defaultSort = dao.getDefaultSort(); + sort.append(defaultSort); + sortedFields.add(defaultSort.split("\\s+")[0]); } final String offset; @@ -81,7 +88,7 @@ public class SqlViewSearchHelper { } final String query = "select " + dao.getSelectClause(searchQuery) + " " + sql.toString() + sortClause + limit + offset; - log.debug("search: SQL = "+query); + log.debug("search: SQL = "+query+" with params: "+StringUtil.toString(params)); Integer totalCount = null; final ArrayList thingsList = new ArrayList<>(); @@ -149,15 +156,18 @@ public class SqlViewSearchHelper { }); // manually sort and apply offset + limit - final SqlViewField sqlViewField = Arrays.stream(fields).filter(a -> a.getName().equals(sortedField)).findFirst().orElse(null); - if (sqlViewField == null) return die("search: sort field not defined/mapped: "+sortedField); + for (int i=0; i a.getName().equals(sortField)).findFirst().orElse(null); + if (sqlViewField == null) return die("search: sort field not defined/mapped: " + sortField); - final Comparator comparator = (E o1, E o2) -> compareSelectedItems(o1, o2, sqlViewField); + final Comparator comparator = (E o1, E o2) -> compareSelectedItems(o1, o2, sqlViewField); - if (!searchQuery.getSortOrder().equals(DEFAULT_SORT)) { - matched.sort(comparator); - } else { - matched.sort(comparator.reversed()); + if (searchQuery.getSorts()[i].getSortOrder() == ASC) { + matched.sort(comparator); + } else { + matched.sort(comparator.reversed()); + } } totalCount = matched.size(); diff --git a/wizard-server/src/main/java/org/cobbzilla/wizard/ldap/LdapServiceBase.java b/wizard-server/src/main/java/org/cobbzilla/wizard/ldap/LdapServiceBase.java index bac4e0b..68caa87 100644 --- a/wizard-server/src/main/java/org/cobbzilla/wizard/ldap/LdapServiceBase.java +++ b/wizard-server/src/main/java/org/cobbzilla/wizard/ldap/LdapServiceBase.java @@ -1,5 +1,6 @@ package org.cobbzilla.wizard.ldap; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.exec.CommandLine; import org.cobbzilla.util.collection.NameAndValue; import org.cobbzilla.util.system.Command; @@ -7,6 +8,7 @@ import org.cobbzilla.util.system.CommandResult; import org.cobbzilla.util.system.CommandShell; import org.cobbzilla.wizard.model.ldap.LdapBindException; import org.cobbzilla.wizard.model.search.SearchQuery; +import org.cobbzilla.wizard.model.search.SortOrder; import org.cobbzilla.wizard.server.config.LdapConfiguration; import java.util.Map; @@ -15,6 +17,7 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.system.CommandShell.okResult; +@Slf4j public abstract class LdapServiceBase implements LdapService { private LdapConfiguration config() { return getConfiguration(); } @@ -62,13 +65,12 @@ public abstract class LdapServiceBase implements LdapService { command.addArgument("-b").addArgument(dn, false); } else { if (!empty(filter) || !empty(bounds)) command.addArgument(ldapFilter(base, filter, bounds)); - if (page.getHasSortField()) { - final SearchQuery.SortOrder sortOrder = page.getSortType(); - final String sort = page.getSortField(); - if (sort != null) { - final String sortArg = ((sortOrder != null && sortOrder == SearchQuery.SortOrder.DESC) ? "-" : ""); - command.addArgument("-E").addArgument("!sss=" + sortArg + sort); - } + if (page.hasSorts()) { + if (page.getSorts().length > 1) log.warn("ldapsearch: only one sort order is supported"); + final SortOrder sortOrder = page.getSorts()[0].getSortOrder(); + final String sort = page.getSorts()[0].getSortField(); + final String sortArg = ((sortOrder != null && sortOrder == SortOrder.DESC) ? "-" : ""); + command.addArgument("-E").addArgument("!sss=" + sortArg + sort); } } final CommandResult result = exec(command);