@@ -6,8 +6,8 @@ import lombok.experimental.Accessors; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.collection.SingletonSet; | import org.cobbzilla.util.collection.SingletonSet; | ||||
import org.cobbzilla.util.reflect.ReflectionUtil; | import org.cobbzilla.util.reflect.ReflectionUtil; | ||||
import org.cobbzilla.wizard.model.search.SearchQuery; | |||||
import org.cobbzilla.wizard.model.UniquelyNamedEntity; | import org.cobbzilla.wizard.model.UniquelyNamedEntity; | ||||
import org.cobbzilla.wizard.model.search.SortOrder; | |||||
import javax.persistence.Transient; | import javax.persistence.Transient; | ||||
import java.util.*; | import java.util.*; | ||||
@@ -195,8 +195,8 @@ public abstract class LdapEntity extends UniquelyNamedEntity { | |||||
private static Map<String, Comparator<LdapEntity>> comparatorCache = new ConcurrentHashMap<>(); | private static Map<String, Comparator<LdapEntity>> comparatorCache = new ConcurrentHashMap<>(); | ||||
public static Comparator<LdapEntity> comparator (final String field, SearchQuery.SortOrder order) { | |||||
final SearchQuery.SortOrder sort = order == null ? SearchQuery.SortOrder.ASC : order; | |||||
public static Comparator<LdapEntity> comparator (final String field, SortOrder order) { | |||||
final SortOrder sort = order == null ? SortOrder.ASC : order; | |||||
final String cacheKey = field + ":" + order; | final String cacheKey = field + ":" + order; | ||||
Comparator<LdapEntity> comp = comparatorCache.get(cacheKey); | Comparator<LdapEntity> comp = comparatorCache.get(cacheKey); | ||||
if (comp == null) { | if (comp == null) { | ||||
@@ -1,6 +1,5 @@ | |||||
package org.cobbzilla.wizard.model.search; | package org.cobbzilla.wizard.model.search; | ||||
import com.fasterxml.jackson.annotation.JsonCreator; | |||||
import com.fasterxml.jackson.annotation.JsonIgnore; | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.NoArgsConstructor; | import lombok.NoArgsConstructor; | ||||
@@ -11,12 +10,9 @@ import org.cobbzilla.util.collection.ArrayUtil; | |||||
import org.cobbzilla.util.collection.NameAndValue; | import org.cobbzilla.util.collection.NameAndValue; | ||||
import org.cobbzilla.util.json.JsonUtil; | import org.cobbzilla.util.json.JsonUtil; | ||||
import org.cobbzilla.util.string.StringUtil; | import org.cobbzilla.util.string.StringUtil; | ||||
import org.cobbzilla.wizard.model.BasicConstraintConstants; | |||||
import org.cobbzilla.wizard.validation.ValidEnum; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | ||||
import static org.cobbzilla.wizard.model.Identifiable.CTIME; | 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 int MAX_SORTFIELD_LENGTH = 50; | ||||
public static final String DEFAULT_SORT_FIELD = CTIME; | 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 DEFAULT_PAGE = new SearchQuery(); | ||||
public static final SearchQuery FIRST_RESULT = new SearchQuery(1, 1); | public static final SearchQuery FIRST_RESULT = new SearchQuery(1, 1); | ||||
public static final int INFINITE = Integer.MAX_VALUE; | public static final int INFINITE = Integer.MAX_VALUE; | ||||
@@ -63,16 +51,16 @@ public class SearchQuery { | |||||
this.setPageNumber(other.getPageNumber()); | this.setPageNumber(other.getPageNumber()); | ||||
this.setPageSize(other.getPageSize()); | this.setPageSize(other.getPageSize()); | ||||
this.setFilter(other.getFilter()); | this.setFilter(other.getFilter()); | ||||
this.setSortField(other.getSortField()); | |||||
this.setSortOrder(other.getSortOrder()); | |||||
this.setSorts(other.getSorts()); | |||||
this.setBounds(other.getBounds()); | this.setBounds(other.getBounds()); | ||||
} | } | ||||
public SearchQuery(Integer pageNumber, Integer pageSize, String sortField, String sortOrder, String filter, NameAndValue[] bounds) { | public SearchQuery(Integer pageNumber, Integer pageSize, String sortField, String sortOrder, String filter, NameAndValue[] bounds) { | ||||
if (pageNumber != null) setPageNumber(pageNumber); | if (pageNumber != null) setPageNumber(pageNumber); | ||||
if (pageSize != null) setPageSize(pageSize); | 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; | if (filter != null) this.filter = filter; | ||||
this.bounds = bounds; | this.bounds = bounds; | ||||
} | } | ||||
@@ -94,7 +82,7 @@ public class SearchQuery { | |||||
} | } | ||||
private static String normalizeSortOrder(SortOrder sortOrder) { | 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) { | 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; | 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 { | } else { | ||||
sortOrder = thing.toString(); | |||||
sorts = ArrayUtil.append(sorts, sort); | |||||
} | } | ||||
return this; | 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; | @Setter private String filter = null; | ||||
public String getFilter() { | public String getFilter() { | ||||
// only return the first several chars, to thwart a hypothetical injection attack. | // 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 (getPageSize() != that.getPageSize()) return false; | ||||
if (!Arrays.equals(that.bounds, bounds)) return false; | if (!Arrays.equals(that.bounds, bounds)) return false; | ||||
if (filter != null ? !filter.equals(that.filter) : that.filter != null) 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; | if (!Arrays.equals(that.fields, fields)) return false; | ||||
return true; | return true; | ||||
} | } | ||||
@@ -203,11 +172,20 @@ public class SearchQuery { | |||||
@Override public int hashCode() { | @Override public int hashCode() { | ||||
int result = getPageNumber(); | int result = getPageNumber(); | ||||
result = 31 * result + getPageSize(); | 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 + (filter != null ? filter.hashCode() : 0); | ||||
result = 31 * result + (bounds != null ? Arrays.hashCode(bounds) : 0); | result = 31 * result + (bounds != null ? Arrays.hashCode(bounds) : 0); | ||||
result = 31 * result + (fields != null ? Arrays.hashCode(fields) : 0); | result = 31 * result + (fields != null ? Arrays.hashCode(fields) : 0); | ||||
return result; | 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(); | |||||
} | |||||
} | } |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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; } | |||||
} |
@@ -303,7 +303,8 @@ public abstract class AbstractDAO<E extends Identifiable> implements DAO<E> { | |||||
.append(filterClause); | .append(filterClause); | ||||
final String countQuery = "select count(*) " + qBuilder.toString(); | 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<E> results = query(query, searchQuery, params, values); | List<E> results = query(query, searchQuery, params, values); | ||||
final int totalCount = Integer.valueOf(""+query(countQuery, SearchQuery.INFINITE_PAGE, params, values).get(0)); | final int totalCount = Integer.valueOf(""+query(countQuery, SearchQuery.INFINITE_PAGE, params, values).get(0)); | ||||
@@ -4,8 +4,10 @@ import lombok.extern.slf4j.Slf4j; | |||||
import org.cobbzilla.util.collection.NameAndValue; | import org.cobbzilla.util.collection.NameAndValue; | ||||
import org.cobbzilla.util.jdbc.ResultSetBean; | import org.cobbzilla.util.jdbc.ResultSetBean; | ||||
import org.cobbzilla.util.reflect.ReflectionUtil; | import org.cobbzilla.util.reflect.ReflectionUtil; | ||||
import org.cobbzilla.util.string.StringUtil; | |||||
import org.cobbzilla.wizard.model.Identifiable; | import org.cobbzilla.wizard.model.Identifiable; | ||||
import org.cobbzilla.wizard.model.search.SearchQuery; | 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.SqlViewField; | ||||
import org.cobbzilla.wizard.model.search.SqlViewSearchResult; | import org.cobbzilla.wizard.model.search.SqlViewSearchResult; | ||||
import org.cobbzilla.wizard.server.config.PgRestServerConfiguration; | 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.die; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | ||||
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; | 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; | import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | ||||
@Slf4j | @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<String> 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 { | } 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; | final String offset; | ||||
@@ -81,7 +88,7 @@ public class SqlViewSearchHelper { | |||||
} | } | ||||
final String query = "select " + dao.getSelectClause(searchQuery) + " " + sql.toString() + sortClause + limit + offset; | 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; | Integer totalCount = null; | ||||
final ArrayList<E> thingsList = new ArrayList<>(); | final ArrayList<E> thingsList = new ArrayList<>(); | ||||
@@ -149,15 +156,18 @@ public class SqlViewSearchHelper { | |||||
}); | }); | ||||
// manually sort and apply offset + limit | // 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<sortedFields.size(); i++) { | |||||
final String sortField = sortedFields.get(i); | |||||
final SqlViewField sqlViewField = Arrays.stream(fields).filter(a -> a.getName().equals(sortField)).findFirst().orElse(null); | |||||
if (sqlViewField == null) return die("search: sort field not defined/mapped: " + sortField); | |||||
final Comparator<E> comparator = (E o1, E o2) -> compareSelectedItems(o1, o2, sqlViewField); | |||||
final Comparator<E> 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(); | totalCount = matched.size(); | ||||
@@ -1,5 +1,6 @@ | |||||
package org.cobbzilla.wizard.ldap; | package org.cobbzilla.wizard.ldap; | ||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.apache.commons.exec.CommandLine; | import org.apache.commons.exec.CommandLine; | ||||
import org.cobbzilla.util.collection.NameAndValue; | import org.cobbzilla.util.collection.NameAndValue; | ||||
import org.cobbzilla.util.system.Command; | 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.util.system.CommandShell; | ||||
import org.cobbzilla.wizard.model.ldap.LdapBindException; | import org.cobbzilla.wizard.model.ldap.LdapBindException; | ||||
import org.cobbzilla.wizard.model.search.SearchQuery; | import org.cobbzilla.wizard.model.search.SearchQuery; | ||||
import org.cobbzilla.wizard.model.search.SortOrder; | |||||
import org.cobbzilla.wizard.server.config.LdapConfiguration; | import org.cobbzilla.wizard.server.config.LdapConfiguration; | ||||
import java.util.Map; | 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.daemon.ZillaRuntime.empty; | ||||
import static org.cobbzilla.util.system.CommandShell.okResult; | import static org.cobbzilla.util.system.CommandShell.okResult; | ||||
@Slf4j | |||||
public abstract class LdapServiceBase implements LdapService { | public abstract class LdapServiceBase implements LdapService { | ||||
private LdapConfiguration config() { return getConfiguration(); } | private LdapConfiguration config() { return getConfiguration(); } | ||||
@@ -62,13 +65,12 @@ public abstract class LdapServiceBase implements LdapService { | |||||
command.addArgument("-b").addArgument(dn, false); | command.addArgument("-b").addArgument(dn, false); | ||||
} else { | } else { | ||||
if (!empty(filter) || !empty(bounds)) command.addArgument(ldapFilter(base, filter, bounds)); | 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); | final CommandResult result = exec(command); | ||||