package com.psddev.cms.tool.page; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.UUID; import com.psddev.cms.db.Taxon; import com.psddev.cms.db.ToolUi; import com.psddev.cms.tool.Search; import com.psddev.cms.tool.ToolPageContext; import com.psddev.dari.db.ComparisonPredicate; import com.psddev.dari.db.CompoundPredicate; import com.psddev.dari.db.Database; import com.psddev.dari.db.DatabaseEnvironment; import com.psddev.dari.db.ObjectField; import com.psddev.dari.db.ObjectFieldComparator; import com.psddev.dari.db.ObjectIndex; import com.psddev.dari.db.ObjectStruct; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Predicate; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.db.Singleton; import com.psddev.dari.db.State; import com.psddev.dari.util.ObjectUtils; @ToolUi.Hidden public abstract class SearchAdvancedPredicate extends Record implements Singleton { @Indexed(unique = true) @Required protected String parameterValue; public String getParameterValue() { return parameterValue; } public abstract String getLabel(); public abstract Predicate writeInputs( SearchAdvancedQuery servlet, ToolPageContext page, List<String> paramNames, String predicateParam, String paramPrefix) throws IOException; public abstract static class Compound extends SearchAdvancedPredicate { public abstract String getOperator(); @Override public Predicate writeInputs( SearchAdvancedQuery servlet, ToolPageContext page, List<String> paramNames, String predicateParam, String paramPrefix) throws IOException { Predicate predicate = null; String subPredicateIndexParam = paramPrefix + ".p"; int lastSubPredicateIndex = -1; for (String paramName : paramNames) { if (paramName.startsWith(subPredicateIndexParam)) { Integer subPredicateIndex = ObjectUtils.to(Integer.class, paramName.substring(subPredicateIndexParam.length())); if (subPredicateIndex != null) { if (lastSubPredicateIndex < subPredicateIndex) { lastSubPredicateIndex = subPredicateIndex; } } } } page.writeStart("button", "class", "icon icon-action-add link", "name", paramPrefix + ".p" + (lastSubPredicateIndex + 1), "value", 1); page.writeHtml("Add Another "); page.writeHtml(getLabel()); page.writeEnd(); page.writeStart("ul"); for (String paramName : paramNames) { if (paramName.startsWith(subPredicateIndexParam)) { Integer subPredicateIndex = ObjectUtils.to(Integer.class, paramName.substring(subPredicateIndexParam.length())); if (subPredicateIndex != null) { page.writeStart("li"); predicate = CompoundPredicate.combine( getOperator(), predicate, servlet.writeSearchAdvancedPredicate(page, paramNames, paramName, paramPrefix + "." + subPredicateIndex)); page.writeEnd(); } } } page.writeEnd(); return predicate; } } public static class And extends Compound { public And() { this.parameterValue = "A"; } @Override public String getLabel() { return "Match All (AND)"; } @Override public String getOperator() { return "AND"; } } public static class Or extends Compound { public Or() { this.parameterValue = "O"; } @Override public String getLabel() { return "Match Any (OR)"; } @Override public String getOperator() { return "OR"; } } public static class Not extends Compound { public Not() { this.parameterValue = "N"; } @Override public String getLabel() { return "Match None (NOT)"; } @Override public String getOperator() { return "NOT"; } } public static class Comparison extends SearchAdvancedPredicate { public Comparison() { this.parameterValue = "C"; } @Override public String getLabel() { return "Comparison"; } @Override public Predicate writeInputs( SearchAdvancedQuery servlet, ToolPageContext page, List<String> paramNames, String predicateParam, String paramPrefix) throws IOException { String comparisonTypeParam = paramPrefix + ".ct"; String comparisonPathParam = paramPrefix + ".cp"; String comparisonOperatorParam = paramPrefix + ".co"; String comparisonValueParam = paramPrefix + ".cv"; DatabaseEnvironment environment = Database.Static.getDefault().getEnvironment(); Set<UUID> comparisonTypeIds = new HashSet<UUID>(); Set<ObjectType> comparisonTypes = new HashSet<ObjectType>(); for (UUID id : page.params(UUID.class, comparisonTypeParam)) { comparisonTypeIds.add(id); comparisonTypes.add(ObjectType.getInstance(id)); } String comparisonPath = page.param(String.class, comparisonPathParam); ComparisonOperator comparisonOperator = page.param(ComparisonOperator.class, comparisonOperatorParam); PathedField comparisonPathedField = null; ObjectField comparisonField = null; page.writeHtml(" "); page.writeMultipleTypeSelect( null, comparisonTypes, "name", comparisonTypeParam, "placeholder", "Any Types", "data-bsp-autosubmit", "", "data-searchable", true); page.writeHtml(" "); page.writeStart("select", "name", comparisonPathParam, "data-bsp-autosubmit", "", "data-searchable", true); page.writeStart("option", "value", ""); page.writeHtml("Any Fields"); page.writeEnd(); if (!comparisonTypes.isEmpty()) { Set<PathedField> pathedFields = null; for (ObjectType t : comparisonTypes) { Set<PathedField> pf = getPathedFields(t); if (pathedFields == null) { pathedFields = pf; } else { pathedFields.retainAll(pf); } } if (!pathedFields.isEmpty()) { page.writeStart("optgroup", "label", "Type-Specific Fields"); for (PathedField pf : pathedFields) { String path = pf.getPath(); if (comparisonField == null && path.equals(comparisonPath)) { List<ObjectField> pfs = pf.getFields(); if (!pfs.isEmpty()) { comparisonPathedField = pf; comparisonField = pfs.get(pfs.size() - 1); } } page.writeStart("option", "selected", path.equals(comparisonPath) ? "selected" : null, "value", path); page.writeHtml(pf.getDisplayName()); page.writeEnd(); } page.writeEnd(); } } page.writeStart("optgroup", "label", "Global Fields"); for (PathedField pf : getPathedFields(environment)) { String path = pf.getPath(); if (comparisonField == null && path.equals(comparisonPath)) { List<ObjectField> pfs = pf.getFields(); if (!pfs.isEmpty()) { comparisonPathedField = pf; comparisonField = pfs.get(pfs.size() - 1); } } page.writeStart("option", "selected", path.equals(comparisonPath) ? "selected" : null, "value", path); page.writeHtml(pf.getDisplayName()); page.writeEnd(); } page.writeEnd(); page.writeEnd(); page.writeHtml(" "); page.writeStart("select", "data-bsp-autosubmit", "", "name", comparisonOperatorParam); for (ComparisonOperator op : ComparisonOperator.values()) { if ((ObjectUtils.isBlank(comparisonPath) && !op.equals(ComparisonOperator.M)) || (!ObjectUtils.isBlank(comparisonPath) && !op.isDisplayedFor(comparisonField))) { if (op.equals(comparisonOperator)) { comparisonOperator = null; } continue; } if (comparisonOperator == null) { comparisonOperator = op; } page.writeStart("option", "selected", op.equals(comparisonOperator) ? "selected" : null, "value", op.name()); page.writeHtml(op.getLabel()); page.writeHtml(":"); page.writeEnd(); } page.writeEnd(); page.writeHtml(" "); comparisonOperator.writeValueInputs(page, comparisonValueParam, comparisonField); return CompoundPredicate.combine( "AND", comparisonTypeIds.isEmpty() ? null : new ComparisonPredicate("=", false, "_type", comparisonTypeIds), comparisonOperator.createPredicate(page, comparisonValueParam, comparisonPathedField)); } private Set<PathedField> getPathedFields(ObjectStruct struct) { Set<PathedField> pathedFields = new TreeSet<PathedField>(); addPathedFields(pathedFields, null, struct); return pathedFields; } private void addPathedFields(Set<PathedField> pathedFields, List<ObjectField> prefix, ObjectStruct struct) { List<ObjectField> fields = struct.getFields(); Set<String> indexedFields = new HashSet<String>(); for (ObjectIndex index : struct.getIndexes()) { indexedFields.addAll(index.getFields()); } for (Iterator<ObjectField> i = fields.iterator(); i.hasNext();) { ObjectField field = i.next(); String declaring = field.getJavaDeclaringClassName(); if (declaring != null && declaring.startsWith("com.psddev.dari.db.")) { continue; } String fieldName = field.getInternalName(); boolean embedded = field.isEmbedded(); if (!embedded && ObjectField.RECORD_TYPE.equals(field.getInternalItemType())) { embedded = true; for (ObjectType t : field.getTypes()) { if (!t.isEmbedded()) { embedded = false; break; } } } if (embedded) { for (ObjectType t : field.getTypes()) { addPathedFields(pathedFields, copyConcatenate(prefix, field), t); } } else if (indexedFields.contains(fieldName) && !field.isDeprecated() && !field.as(ToolUi.class).isHidden()) { pathedFields.add(new PathedField(copyConcatenate(prefix, field))); } } } private static <T> List<T> copyConcatenate(List<T> list, T item) { list = list != null ? new ArrayList<T>(list) : new ArrayList<T>(); list.add(item); return list; } private static class PathedField implements Comparable<PathedField> { private final List<ObjectField> fields; private final String path; private final String displayName; public PathedField(List<ObjectField> fields) { this.fields = Collections.unmodifiableList(fields); StringBuilder path = new StringBuilder(); for (ObjectField f : getFields()) { path.append(f.getInternalName()); path.append('/'); } path.setLength(path.length() - 1); this.path = path.toString(); StringBuilder displayName = new StringBuilder(); for (ObjectField f : getFields()) { displayName.append(f.getDisplayName()); displayName.append(" \u2192 "); } displayName.setLength(displayName.length() - 3); this.displayName = displayName.toString(); } public List<ObjectField> getFields() { return fields; } public String getPath() { return path; } public String getDisplayName() { return displayName; } @Override public int compareTo(PathedField other) { return getDisplayName().compareTo(other.getDisplayName()); } @Override public int hashCode() { return getPath().hashCode(); } @Override public boolean equals(Object other) { if (this == other) { return true; } else if (other instanceof PathedField) { return getPath().equals(((PathedField) other).getPath()); } else { return false; } } } private enum TaxonOption { C("Or Its Children"), D("Or Its Descendants"); private final String label; private TaxonOption(String label) { this.label = label; } public String getLabel() { return label; } } private enum ComparisonOperator { M("Contains Text") { @Override public boolean isDisplayedFor(ObjectField field) { String t = field.getInternalItemType(); return ObjectField.REFERENTIAL_TEXT_TYPE.equals(t) || ObjectField.TEXT_TYPE.equals(t); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { page.writeElement("input", "type", "text", "name", valueParam, "value", page.param(String.class, valueParam)); } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { return createPredicateWithOperatorAndValue( pathedField, "matches", page.param(String.class, valueParam)); } }, I("Is") { @Override public boolean isDisplayedFor(ObjectField field) { String t = field.getInternalItemType(); return ObjectField.NUMBER_TYPE.equals(t) || ObjectField.RECORD_TYPE.equals(t) || ObjectField.TEXT_TYPE.equals(t); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { if (ObjectField.RECORD_TYPE.equals(field.getInternalItemType())) { List<Object> selected = Query .fromAll() .where("_id = ?", page.params(UUID.class, valueParam)) .selectAll(); if (page.isObjectSelectDropDown(field)) { List<?> items = new Search(field).toQuery(page.getSite()).selectAll(); Collections.sort(items, new ObjectFieldComparator("_label", false)); page.writeStart("select", "name", valueParam, "multiple", "multiple", "data-searchable", "true"); for (Object item : items) { State itemState = State.getInstance(item); page.writeStart("option", "selected", selected.contains(item) ? "selected" : null, "value", itemState.getId()); page.writeObjectLabel(item); page.writeEnd(); } page.writeEnd(); } else { if (selected.isEmpty()) { selected.add(null); } String taxonParam = valueParam + "x"; List<TaxonOption> taxonOptions = page.params(TaxonOption.class, taxonParam); boolean taxon = false; for (ObjectType t : field.getTypes()) { if (t.getGroups().contains(Taxon.class.getName())) { taxon = true; break; } } page.writeStart("div", "class", "repeatableObjectId", "style", "overflow:hidden;"); page.writeStart("ul"); for (int i = 0, size = selected.size(); i < size; ++ i) { Object item = selected.get(i); page.writeStart("li"); page.writeObjectSelect( field, item, "name", valueParam); if (taxon) { TaxonOption taxonOption = i < taxonOptions.size() ? taxonOptions.get(i) : null; page.writeHtml(" "); page.writeStart("select", "name", taxonParam); page.writeStart("option"); page.writeHtml("Only"); page.writeEnd(); for (TaxonOption o : TaxonOption.values()) { page.writeStart("option", "selected", o.equals(taxonOption) ? "selected" : null, "value", o.name()); page.writeHtml(o.getLabel()); page.writeEnd(); } page.writeEnd(); } page.writeEnd(); } page.writeStart("script", "type", "text/template"); page.writeStart("li"); page.writeObjectSelect( field, null, "name", valueParam); if (taxon) { page.writeHtml(" "); page.writeStart("select", "name", taxonParam); page.writeStart("option"); page.writeHtml("Only"); page.writeEnd(); for (TaxonOption o : TaxonOption.values()) { page.writeStart("option", "value", o.name()); page.writeHtml(o.getLabel()); page.writeEnd(); } page.writeEnd(); } page.writeEnd(); page.writeEnd(); page.writeEnd(); page.writeEnd(); } } else { page.writeElement("input", "type", "text", "name", valueParam, "value", page.param(String.class, valueParam)); } } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { TaxonOption taxonOption = page.param(TaxonOption.class, valueParam + "x"); if (taxonOption != null) { Taxon top = Query.from(Taxon.class).where("_id = ?", page.param(UUID.class, valueParam)).first(); Set<UUID> values = new HashSet<UUID>(); if (top != null) { values.add(top.getState().getId()); if (taxonOption.equals(TaxonOption.D)) { addChildren(values, top); } else { for (Taxon c : top.getChildren()) { values.add(c.getState().getId()); } } } return createPredicateWithOperatorAndValue( pathedField, "=", values); } else { return createPredicateWithOperatorAndValue( pathedField, "=", page.params(String.class, valueParam)); } } private void addChildren(Set<UUID> values, Taxon parent) { for (Taxon c : parent.getChildren()) { values.add(c.getState().getId()); addChildren(values, c); } } }, N("Is Not") { @Override public boolean isDisplayedFor(ObjectField field) { return I.isDisplayedFor(field); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { I.writeValueInputs(page, valueParam, field); } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { return createPredicateWithOperatorAndValue( pathedField, "!=", page.param(String.class, valueParam)); } }, T("Is Set") { @Override public boolean isDisplayedFor(ObjectField field) { return ObjectField.BOOLEAN_TYPE.equals(field.getInternalType()); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { return createPredicateWithOperatorAndValue( pathedField, "=", Boolean.TRUE); } }, F("Is Not Set") { @Override public boolean isDisplayedFor(ObjectField field) { return T.isDisplayedFor(field); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { return createPredicateWithOperatorAndValue( pathedField, "!=", Boolean.TRUE); } }, S("Is Missing") { @Override public boolean isDisplayedFor(ObjectField field) { return !ObjectField.BOOLEAN_TYPE.equals(field.getInternalType()); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { return createPredicateWithOperatorAndValue( pathedField, "=", Query.MISSING_VALUE); } }, G("Is Not Missing") { @Override public boolean isDisplayedFor(ObjectField field) { return S.isDisplayedFor(field); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { return createPredicateWithOperatorAndValue( pathedField, "!=", Query.MISSING_VALUE); } }, B("Between") { @Override public boolean isDisplayedFor(ObjectField field) { String t = field.getInternalItemType(); return ObjectField.DATE_TYPE.equals(t) || ObjectField.NUMBER_TYPE.equals(t); } @Override public void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException { boolean date = ObjectField.DATE_TYPE.equals(field.getInternalItemType()); String valueToParam = valueParam + "t"; page.writeElement("input", "type", "text", "class", date ? "date" : null, "name", valueParam, "value", page.param(String.class, valueParam)); page.writeHtml(" "); page.writeElement("input", "type", "text", "class", date ? "date" : null, "name", valueToParam, "value", page.param(String.class, valueToParam)); } @Override public Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField) { List<ObjectField> fields = pathedField.getFields(); boolean date = ObjectField.DATE_TYPE.equals(fields.get(fields.size() - 1).getInternalItemType()); Object from = page.param(date ? Date.class : Double.class, valueParam); Object to = page.param(date ? Date.class : Double.class, valueParam + "t"); return CompoundPredicate.combine( "AND", from == null ? null : createPredicateWithOperatorAndValue(pathedField, ">=", from), to == null ? null : createPredicateWithOperatorAndValue(pathedField, "<=", to)); } }; private final String label; private ComparisonOperator(String label) { this.label = label; } public abstract boolean isDisplayedFor(ObjectField field); public abstract void writeValueInputs( ToolPageContext page, String valueParam, ObjectField field) throws IOException; public abstract Predicate createPredicate( ToolPageContext page, String valueParam, PathedField pathedField); public String getLabel() { return label; } public Predicate createPredicateWithOperatorAndValue( PathedField pathedField, String operator, Object value) { if (value == null) { return null; } else { StringBuilder key = new StringBuilder(); if (pathedField == null) { key.append("_any"); } else { for (ObjectField f : pathedField.getFields()) { if (key.length() == 0) { key.append(f.getUniqueName()); } else { key.append('/'); key.append(f.getInternalName()); } } } return new ComparisonPredicate( operator, false, key.toString(), value instanceof Iterable ? (Iterable<?>) value : Arrays.asList(value)); } } } } }