@@ -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<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; | |||
Comparator<LdapEntity> comp = comparatorCache.get(cacheKey); | |||
if (comp == null) { | |||
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
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); | |||
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.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<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 { | |||
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<E> 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<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(); | |||
@@ -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); | |||