package com.psddev.cms.tool; import java.awt.Color; import java.io.IOException; import java.lang.reflect.Modifier; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; import com.google.common.base.Preconditions; import com.psddev.cms.db.Content; import com.psddev.cms.db.Directory; import com.psddev.cms.db.Draft; import com.psddev.cms.db.Global; import com.psddev.cms.db.Localization; import com.psddev.cms.db.Site; import com.psddev.cms.db.ToolEntity; import com.psddev.cms.db.ToolRole; import com.psddev.cms.db.ToolUi; import com.psddev.cms.db.ToolUser; import com.psddev.cms.db.ToolUserSearch; import com.psddev.cms.db.Workflow; import com.psddev.cms.db.WorkflowState; import com.psddev.cms.tool.search.ListSearchResultView; 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.ObjectIndex; import com.psddev.dari.db.ObjectStruct; import com.psddev.dari.db.ObjectType; import com.psddev.dari.db.Predicate; import com.psddev.dari.db.PredicateParser; import com.psddev.dari.db.Query; import com.psddev.dari.db.Record; import com.psddev.dari.db.Singleton; import com.psddev.dari.db.Sorter; import com.psddev.dari.util.ClassFinder; import com.psddev.dari.util.HuslColorSpace; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.PaginatedResult; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.TypeDefinition; import com.psddev.dari.util.UuidUtils; import org.apache.http.Header; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpHead; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; public class Search extends Record { public static final String ADDITIONAL_PREDICATE_PARAMETER = "aq"; public static final String ADVANCED_QUERY_PARAMETER = "av"; public static final String ADVANCED_QUERY_EDIT_STRING_PARAMETER = "au"; public static final String CONTEXT_PARAMETER = "cx"; public static final String GLOBAL_FILTER_PARAMETER_PREFIX = "gf."; public static final String IGNORE_SITE_PARAMETER = "is"; public static final String FIELD_FILTER_PARAMETER_PREFIX = "f."; public static final String LIMIT_PARAMETER = "l"; public static final String MISSING_FILTER_PARAMETER_SUFFIX = ".m"; public static final String NAME_PARAMETER = "n"; public static final String OFFSET_PARAMETER = "o"; public static final String COLOR_PARAMETER = "c"; public static final String ONLY_PATHED_PARAMETER = "p"; public static final String PARENT_PARAMETER = "pt"; public static final String PARENT_TYPE_PARAMETER = "py"; public static final String QUERY_STRING_PARAMETER = "q"; public static final String SELECTED_TYPE_PARAMETER = "st"; public static final String SESSION_ID_PARAMETER = "si"; public static final String SHOW_DRAFTS_PARAMETER = "d"; public static final String VISIBILITIES_PARAMETER = "v"; public static final String SHOW_MISSING_PARAMETER = "m"; public static final String SORT_PARAMETER = "s"; public static final String SUGGESTIONS_PARAMETER = "sg"; public static final String TYPES_PARAMETER = "rt"; public static final String NEW_ITEM_IDS_PARAMETER = "ni"; public static final String FRAME_NAME_SUFFIX_PARAMETER = "fns"; public static final String NEWEST_SORT_LABEL = "Newest"; public static final String NEWEST_SORT_VALUE = "_newest"; public static final String RELEVANT_SORT_LABEL = "Relevant"; public static final String RELEVANT_SORT_VALUE = "_relevant"; public static final double RELEVANT_SORT_LABEL_BOOST = 10.0; private String name; private Set<ObjectType> types; private ObjectType selectedType; private String queryString; private String color; private boolean onlyPathed; private String additionalPredicate; private String advancedQuery; private String advancedQueryEditStringParameters; private UUID parentId; private UUID parentTypeId; private Map<String, String> globalFilters; private Map<String, Map<String, String>> fieldFilters; private String sort; private boolean showDrafts; private List<String> visibilities; private boolean showMissing; private boolean suggestions; private long offset; private int limit; private Set<UUID> newItemIds; private boolean ignoreSite; private String frameNameSuffix = generateFrameNameSuffix(); public Search() { } public Search(ObjectField field) { getTypes().addAll(field.getTypes()); setOnlyPathed(ToolUi.isOnlyPathed(field)); setAdditionalPredicate(field.getPredicate()); } public Search(ToolPageContext page, Iterable<UUID> typeIds) { this.page = page; setName(page.param(String.class, NAME_PARAMETER)); if (typeIds != null) { for (UUID typeId : typeIds) { ObjectType type = ObjectType.getInstance(typeId); if (type != null) { getTypes().add(type); } } } for (String name : page.paramNamesList()) { if (name.startsWith(GLOBAL_FILTER_PARAMETER_PREFIX)) { putFilterValues( getGlobalFilters(), name.substring(GLOBAL_FILTER_PARAMETER_PREFIX.length()), page.params(String.class, name)); } else if (name.startsWith(FIELD_FILTER_PARAMETER_PREFIX)) { String filterName = name.substring(FIELD_FILTER_PARAMETER_PREFIX.length()); int dotAt = filterName.lastIndexOf('.'); String filterValueKey; if (dotAt < 0) { filterValueKey = ""; } else { filterValueKey = filterName.substring(dotAt + 1); if (filterValueKey.length() > 1) { filterValueKey = ""; } else { filterName = filterName.substring(0, dotAt); } } Map<String, String> filterValue = getFieldFilters().get(filterName); if (filterValue == null) { filterValue = new HashMap<String, String>(); getFieldFilters().put(filterName, filterValue); } if (filterValueKey.length() == 0) { putFilterValues(filterValue, filterValueKey, page.params(String.class, name)); } else { filterValue.put(filterValueKey, page.param(String.class, name)); } } } setSelectedType(ObjectType.getInstance(page.param(UUID.class, SELECTED_TYPE_PARAMETER))); setQueryString(page.paramOrDefault(String.class, QUERY_STRING_PARAMETER, "").trim()); setColor(page.param(String.class, COLOR_PARAMETER)); setOnlyPathed(page.param(boolean.class, IS_ONLY_PATHED)); setAdditionalPredicate(page.param(String.class, ADDITIONAL_QUERY_PARAMETER)); setAdvancedQuery(page.param(String.class, ADVANCED_QUERY_PARAMETER)); setAdvancedQueryEditStringParameters(page.param(String.class, ADVANCED_QUERY_EDIT_STRING_PARAMETER)); setParentId(page.param(UUID.class, PARENT_PARAMETER)); setParentTypeId(page.param(UUID.class, PARENT_TYPE_PARAMETER)); setSort(page.param(String.class, SORT_PARAMETER)); setShowDrafts(page.param(boolean.class, SHOW_DRAFTS_PARAMETER)); setVisibilities(page.params(String.class, VISIBILITIES_PARAMETER)); setSuggestions(page.param(boolean.class, SUGGESTIONS_PARAMETER)); setOffset(page.param(long.class, OFFSET_PARAMETER)); setLimit(page.paramOrDefault(int.class, LIMIT_PARAMETER, 10)); setNewItemIds(new LinkedHashSet<>(page.params(UUID.class, NEW_ITEM_IDS_PARAMETER))); setIgnoreSite(page.param(boolean.class, IGNORE_SITE_PARAMETER)); setFrameNameSuffix(page.param(String.class, FRAME_NAME_SUFFIX_PARAMETER)); for (Tool tool : Query.from(Tool.class).selectAll()) { tool.initializeSearch(this, page); } } private void putFilterValues(Map<String, String> map, String key, List<String> values) { for (ListIterator<String> i = values.listIterator(); i.hasNext();) { if (ObjectUtils.isBlank(i.next())) { i.remove(); } } int valuesSize = values.size(); map.put(key, valuesSize > 0 ? values.get(0) : null); if (valuesSize > 1) { map.put(key + "#", String.valueOf(valuesSize)); for (int i = 0; i < valuesSize; ++i) { map.put(key + String.valueOf(i), values.get(i)); } } } public Search(ToolPageContext page) { this(page, page.params(UUID.class, TYPES_PARAMETER)); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<ObjectType> getTypes() { if (types == null) { types = new HashSet<ObjectType>(); } return types; } public void setTypes(Set<ObjectType> types) { this.types = types; } public ObjectType getSelectedType() { return selectedType; } public void setSelectedType(ObjectType selectedType) { this.selectedType = selectedType; } public String getQueryString() { return queryString; } public void setQueryString(String queryString) { this.queryString = queryString; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public boolean isOnlyPathed() { return onlyPathed; } public void setOnlyPathed(boolean onlyPathed) { this.onlyPathed = onlyPathed; } public String getAdditionalPredicate() { return additionalPredicate; } public void setAdditionalPredicate(String additionalPredicate) { this.additionalPredicate = additionalPredicate; } public String getAdvancedQuery() { return advancedQuery; } public void setAdvancedQuery(String advancedQuery) { this.advancedQuery = advancedQuery; } public String getAdvancedQueryEditStringParameters() { return advancedQueryEditStringParameters; } public void setAdvancedQueryEditStringParameters(String advancedQueryEditStringParameters) { this.advancedQueryEditStringParameters = advancedQueryEditStringParameters; } public UUID getParentId() { return parentId; } public void setParentId(UUID parentId) { this.parentId = parentId; } public UUID getParentTypeId() { return parentTypeId; } public void setParentTypeId(UUID parentTypeId) { this.parentTypeId = parentTypeId; } public Map<String, String> getGlobalFilters() { if (globalFilters == null) { globalFilters = new HashMap<String, String>(); } return globalFilters; } public void setGlobalFilters(Map<String, String> globalFilters) { this.globalFilters = globalFilters; } public Map<String, Map<String, String>> getFieldFilters() { if (fieldFilters == null) { fieldFilters = new HashMap<String, Map<String, String>>(); } return fieldFilters; } public void setFieldFilters(Map<String, Map<String, String>> fieldFilters) { this.fieldFilters = fieldFilters; } public String getSort() { return sort; } public void setSort(String sort) { this.sort = sort; } public boolean isShowDrafts() { return showDrafts; } public void setShowDrafts(boolean showDrafts) { this.showDrafts = showDrafts; } public List<String> getVisibilities() { if (visibilities == null) { visibilities = new ArrayList<String>(); } return visibilities; } public void setVisibilities(List<String> visibilities) { this.visibilities = visibilities; } @Deprecated public boolean isShowMissing() { return showMissing; } @Deprecated public void setShowMissing(boolean showMissing) { this.showMissing = showMissing; } public boolean isSuggestions() { return suggestions; } public void setSuggestions(boolean suggestions) { this.suggestions = suggestions; } public long getOffset() { return offset; } public void setOffset(long offset) { this.offset = offset; } public int getLimit() { return limit; } public void setLimit(int limit) { this.limit = limit; } public Set<UUID> getNewItemIds() { if (newItemIds == null) { newItemIds = new LinkedHashSet<>(); } return newItemIds; } public void setNewItemIds(Set<UUID> newItemIds) { this.newItemIds = newItemIds; } public boolean isIgnoreSite() { return ignoreSite; } public void setIgnoreSite(boolean ignoreSite) { this.ignoreSite = ignoreSite; } public String getFrameNameSuffix() { return frameNameSuffix; } public void setFrameNameSuffix(String frameNameSuffix) { this.frameNameSuffix = frameNameSuffix; } /** * Creates a unique frame name that starts with the given {@code prefix}. * * @param prefix * Can't be {@code null}. * * @return Never {@code null}. */ public String createFrameName(String prefix) { Preconditions.checkNotNull(prefix); if (ObjectUtils.isBlank(frameNameSuffix)) { frameNameSuffix = generateFrameNameSuffix(); } return prefix + "-" + frameNameSuffix; } public static String generateFrameNameSuffix() { return UUID.randomUUID().toString().replace("-", ""); } public Set<ObjectType> findValidTypes() { Set<ObjectType> types = getTypes(); List<ObjectType> validTypes = new ArrayList<ObjectType>(); if (types.size() == 1) { ObjectType type = types.iterator().next(); if (type != null && Content.class.equals(type.getObjectClass())) { types.clear(); } } for (ObjectType type : types) { validTypes.addAll(type.as(ToolUi.class).findDisplayTypes()); } Collections.sort(validTypes); return new LinkedHashSet<ObjectType>(validTypes); } /** * Finds all sorters that can be applied to this search. * * @return Never {@code null}. The key is the unique ID, and the value * is the label. Sorted by the label. */ public Map<String, String> findSorts() { Map<String, String> sorts = new LinkedHashMap<String, String>(); ObjectType selectedType = getSelectedType(); if (!ObjectUtils.isBlank(getQueryString())) { sorts.put(RELEVANT_SORT_VALUE, RELEVANT_SORT_LABEL); } addSorts(sorts, selectedType); addSorts(sorts, Database.Static.getDefault().getEnvironment()); List<Map.Entry<String, String>> sortsList = new ArrayList<Map.Entry<String, String>>(sorts.entrySet()); Collections.sort(sortsList, new Comparator<Map.Entry<String, String>>() { @Override public int compare(Map.Entry<String, String> x, Map.Entry<String, String> y) { return ObjectUtils.compare(x.getValue(), y.getValue(), false); } }); sorts.clear(); for (Map.Entry<String, String> entry : sortsList) { sorts.put(entry.getKey(), entry.getValue()); } return sorts; } private void addSorts(Map<String, String> sorts, ObjectStruct struct) { if (struct != null) { for (ObjectField field : ObjectStruct.Static.findIndexedFields(struct)) { if (field.as(ToolUi.class).isEffectivelySortable()) { sorts.put(field.getInternalName(), Localization.currentUserText(field, "field." + field.getInternalName())); } } } } public static Predicate getVisibilitiesPredicate(ObjectType selectedType, Collection<String> visibilities, Set<UUID> validTypeIds, boolean showDrafts) { if (visibilities == null || visibilities.isEmpty() || (visibilities.size() == 1 && "".equals(visibilities.stream().findFirst().orElse(null)))) { return getVisibilitiesPredicate(selectedType, Arrays.asList("p"), validTypeIds, showDrafts); } Set<UUID> selectedTypeIds = selectedType != null ? selectedType.findConcreteTypes().stream().map(ObjectType::getId).collect(Collectors.toSet()) : null; Set<UUID> visibilityTypeIds = new HashSet<UUID>(); Predicate visibilitiesPredicate = null; boolean draft = false; for (String visibility : visibilities) { if ("p".equals(visibility)) { Set<String> comparisonKeys = new HashSet<String>(); DatabaseEnvironment environment = Database.Static.getDefault().getEnvironment(); addVisibilityFields(comparisonKeys, environment); for (ObjectType type : environment.getTypes()) { addVisibilityFields(comparisonKeys, type); } Predicate publishedPredicate = null; for (String key : comparisonKeys) { if (showDrafts) { publishedPredicate = CompoundPredicate.combine( PredicateParser.AND_OPERATOR, publishedPredicate, PredicateParser.Static.parse(key + " = missing or " + key + " != missing or " + key + " = true")); } else { publishedPredicate = CompoundPredicate.combine( PredicateParser.AND_OPERATOR, publishedPredicate, PredicateParser.Static.parse(key + " = missing")); } } visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, visibilitiesPredicate, addSelectedTypeIds(publishedPredicate, selectedTypeIds)); } else if ("d".equals(visibility)) { draft = true; Predicate draftPredicate = PredicateParser.Static.parse("_type = ? and com.psddev.cms.db.Draft/newContent != true", Draft.class); if (selectedType != null) { draftPredicate = CompoundPredicate.combine( PredicateParser.AND_OPERATOR, draftPredicate, PredicateParser.Static.parse("com.psddev.cms.db.Draft/objectType = ?", selectedType.findConcreteTypes())); } visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, visibilitiesPredicate, draftPredicate); } else if ("w".equals(visibility)) { for (Workflow w : (selectedType == null ? Query.from(Workflow.class) : Query.from(Workflow.class).where("contentTypes = ?", selectedType)).selectAll()) { for (WorkflowState s : w.getStates()) { visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, visibilitiesPredicate, addSelectedTypeIdsWithVisibility("cms.workflow.currentState", s.getName(), selectedTypeIds)); } } } else if (visibility.startsWith("w.")) { String value = visibility.substring(2); visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, visibilitiesPredicate, addSelectedTypeIdsWithVisibility("cms.workflow.currentState", value, selectedTypeIds)); } else if (visibility.startsWith("b.")) { String field = visibility.substring(2); visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, visibilitiesPredicate, addSelectedTypeIdsWithVisibility(field, true, selectedTypeIds)); } else if (visibility.startsWith("t.")) { visibility = visibility.substring(2); int equalAt = visibility.indexOf('='); if (equalAt > -1) { String field = visibility.substring(0, equalAt); String value = visibility.substring(equalAt + 1); visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, visibilitiesPredicate, addSelectedTypeIdsWithVisibility(field, value, selectedTypeIds)); } } } if (validTypeIds != null) { validTypeIds.addAll(visibilityTypeIds); } if (!draft) { visibilitiesPredicate = CompoundPredicate.combine( PredicateParser.AND_OPERATOR, visibilitiesPredicate, PredicateParser.Static.parse("_type != ?", Draft.class)); } return visibilitiesPredicate; } private static Predicate addSelectedTypeIds(Predicate predicate, Set<UUID> selectedTypeIds) { if (selectedTypeIds != null) { return CompoundPredicate.combine( PredicateParser.AND_OPERATOR, predicate, PredicateParser.Static.parse("_type = ?", selectedTypeIds)); } else { return predicate; } } private static Predicate addSelectedTypeIdsWithVisibility(String field, Object value, Set<UUID> selectedTypeIds) { Predicate predicate = PredicateParser.Static.parse(field + " = ?", value); if (selectedTypeIds != null) { Set<UUID> withVisibility = new HashSet<>(selectedTypeIds); int lastSlashAt = field.lastIndexOf('/'); String fieldNameOnly = lastSlashAt > -1 ? field.substring(lastSlashAt + 1) : field; byte[] md5 = StringUtils.md5(fieldNameOnly + "/" + value.toString().trim().toLowerCase(Locale.ENGLISH)); for (UUID selectedTypeId : selectedTypeIds) { byte[] typeId = UuidUtils.toBytes(selectedTypeId); for (int i = 0, length = typeId.length; i < length; ++ i) { typeId[i] ^= md5[i]; } withVisibility.add(UuidUtils.fromBytes(typeId)); } return CompoundPredicate.combine( PredicateParser.AND_OPERATOR, predicate, PredicateParser.Static.parse("_type = ?", withVisibility)); } else { return predicate; } } public Query<?> toQuery(Site site) { Query<?> query = null; Set<ObjectType> types = getTypes(); ObjectType selectedType = getSelectedType(); Set<ObjectType> validTypes = findValidTypes(); Set<UUID> validTypeIds = null; boolean isAllSearchable = true; Collection<String> visibilities = getVisibilities(); if (selectedType != null) { if (selectedType.isAbstract()) { for (ObjectType t : selectedType.findConcreteTypes()) { if (!Content.Static.isSearchableType(t)) { isAllSearchable = false; break; } } } else { isAllSearchable = Content.Static.isSearchableType(selectedType); } query = visibilities.contains("d") ? Query.fromAll() : Query.fromType(selectedType); } else { for (ObjectType type : validTypes) { if (type.isAbstract()) { for (ObjectType t : type.findConcreteTypes()) { if (!Content.Static.isSearchableType(t)) { isAllSearchable = false; break; } } } else if (!Content.Static.isSearchableType(type)) { isAllSearchable = false; } } if (types.size() == 1) { for (ObjectType type : types) { query = visibilities.contains("d") ? Query.fromAll() : Query.fromType(type); break; } } else { query = isAllSearchable ? Query.fromGroup(Content.SEARCHABLE_GROUP) : Query.fromAll(); if (!validTypes.isEmpty()) { validTypeIds = new HashSet<UUID>(); for (ObjectType t : validTypes) { validTypeIds.add(t.getId()); } } } } // If the query string is an URL, hit it to find the ID. String queryString = getQueryString(); if (!ObjectUtils.isBlank(queryString)) { try { URI qsUri = new URL(queryString.trim()).toURI(); String qsHost = qsUri.getHost(); qsUri = new URI(qsUri.getScheme(), qsUri.getUserInfo(), "localhost", qsUri.getPort(), qsUri.getPath(), qsUri.getQuery(), qsUri.getFragment()); try (CloseableHttpClient client = HttpClients.createDefault()) { HttpHead request = new HttpHead(qsUri); request.setHeader("Host", qsHost); request.setHeader("Brightspot-Main-Object-Id-Query", "true"); request.setConfig(RequestConfig.custom() .setConnectionRequestTimeout(1000) .setConnectTimeout(1000) .setSocketTimeout(1000) .build()); try (CloseableHttpResponse response = client.execute(request)) { EntityUtils.consume(response.getEntity()); Header mainObjectIdHeader = response.getFirstHeader("Brightspot-Main-Object-Id"); if (mainObjectIdHeader != null) { UUID mainObjectId = ObjectUtils.to(UUID.class, mainObjectIdHeader.getValue()); if (mainObjectId != null) { if (query.isFromAll() && !validTypeIds.isEmpty()) { query.and("_type = ?", validTypeIds); } return query .and("_id = ? or * matches ?", mainObjectId, mainObjectId) .sortRelevant(100.0, "_id = ?", mainObjectId); } } } } } catch (IOException | URISyntaxException error) { // Can't connect to the URL in the query string to get the main // object ID, but that's OK to ignore and move on. } } String sort = getSort(); boolean metricSort = false; if (RELEVANT_SORT_VALUE.equals(sort)) { if (isAllSearchable) { query.sortRelevant(100.0, "_label = ?", queryString); query.sortRelevant(10.0, "_label matches ?", queryString); } } else if (sort != null) { ObjectField sortField = selectedType != null ? selectedType.getFieldGlobally(sort) : Database.Static.getDefault().getEnvironment().getField(sort); if (sortField != null) { if (sortField.isMetric()) { metricSort = true; } String sortName = selectedType != null ? selectedType.getInternalName() + "/" + sort : sort; if (ObjectField.TEXT_TYPE.equals(sortField.getInternalType())) { query.sortAscending(sortName); } else { query.sortDescending(sortName); } } } if (metricSort) { // Skip Solr-related operations if sorting by metrics. } else if (ObjectUtils.isBlank(queryString)) { if (isAllSearchable) { query.and("* ~= *"); } } else { // Strip http: or https: from the query for search by path below. if (queryString.length() > 8 && StringUtils.matches(queryString, "(?i)https?://.*")) { int slashAt = queryString.indexOf("/", 8); if (slashAt > -1) { queryString = queryString.substring(slashAt); } } // Search by path. if (isAllSearchable && queryString.startsWith("/") && queryString.length() > 1) { List<String> paths = new ArrayList<String>(); for (Directory directory : Query .from(Directory.class) .where("path ^=[c] ?", queryString) .selectAll()) { paths.add(directory.getRawPath()); } int lastSlashAt = queryString.lastIndexOf("/"); if (lastSlashAt > 0) { for (Directory directory : Query .from(Directory.class) .where("path ^=[c] ?", queryString.substring(0, lastSlashAt)) .selectAll()) { paths.add(directory.getRawPath() + queryString.substring(lastSlashAt + 1)); } } query.and(Directory.PATHS_FIELD + " ^= ?", paths); // Full text search. } else if (isAllSearchable) { int lastSpaceAt = queryString.lastIndexOf(" "); if (lastSpaceAt > -1) { query.and("* ~= ?", Arrays.asList(queryString, queryString.substring(0, lastSpaceAt))); } else { query.and("* ~= ?", queryString); } } else { Predicate predicate = null; Set<ObjectType> predicateTypes = validTypes; if (selectedType != null) { predicateTypes = selectedType.findConcreteTypes(); predicateTypes.retainAll(validTypes); } for (ObjectType type : predicateTypes) { String prefix = type.getInternalName() + "/"; for (String field : type.getLabelFields()) { ObjectIndex objectIndex = type.getIndex(field); if (objectIndex != null) { String comparisonOperator = "contains" + (objectIndex.isCaseSensitive() ? "" : "[c]"); predicate = CompoundPredicate.combine( PredicateParser.OR_OPERATOR, predicate, PredicateParser.Static.parse(prefix + field + " " + comparisonOperator + " ?", queryString)); } } } query.and(predicate); } } if (isOnlyPathed() && !visibilities.contains("d")) { query.and(Directory.Static.hasPathPredicate()); } if (isAllSearchable && selectedType == null) { DatabaseEnvironment environment = Database.Static.getDefault().getEnvironment(); String q = getQueryString(); if (!ObjectUtils.isBlank(q)) { q = q.replaceAll("\\s+", "").toLowerCase(Locale.ENGLISH); for (ObjectType t : environment.getTypes()) { String name = t.getDisplayName(); if (!ObjectUtils.isBlank(name) && q.contains(name.replaceAll("\\s+", "").toLowerCase(Locale.ENGLISH))) { query.sortRelevant(20.0, "_type = ?", t.as(ToolUi.class).findDisplayTypes()); } } } query.sortRelevant(10.0, "_type = ?", environment.getTypeByClass(Singleton.class).as(ToolUi.class).findDisplayTypes()); } String additionalPredicate = getAdditionalPredicate(); if (!ObjectUtils.isBlank(additionalPredicate)) { Object parent = Query .from(Object.class) .where("_id = ?", getParentId()) .first(); if (parent == null && getParentTypeId() != null) { ObjectType parentType = ObjectType.getInstance(getParentTypeId()); parent = parentType.createObject(null); } query.and(additionalPredicate, parent); } String advancedQuery = getAdvancedQuery(); if (!ObjectUtils.isBlank(advancedQuery)) { query.and(advancedQuery); } int filtersCount = 0; Map<String, String> globalFilters = getGlobalFilters(); for (Map.Entry<String, String> entry : globalFilters.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (ObjectUtils.to(UUID.class, key) != null && value != null) { ++ filtersCount; int count = ObjectUtils.to(int.class, globalFilters.get(key + "#")); if (count > 0) { query.and("* matches ?", IntStream.range(0, count) .mapToObj(i -> globalFilters.get(key + i)) .collect(Collectors.toSet())); } else { query.and("* matches ?", value); } } } for (Map.Entry<String, Map<String, String>> entry : getFieldFilters().entrySet()) { Map<String, String> value = entry.getValue(); if (value == null) { continue; } String fieldName = entry.getKey(); if (selectedType != null && !fieldName.contains("/")) { fieldName = selectedType.getInternalName() + "/" + fieldName; } if (ObjectUtils.to(boolean.class, value.get("m"))) { ++ filtersCount; query.and(fieldName + " = missing"); } else { String fieldValue = value.get(""); String queryType = value.get("t"); if ("d".equals(queryType)) { Date start = ObjectUtils.to(Date.class, fieldValue); Date end = ObjectUtils.to(Date.class, value.get("x")); if (start != null || end != null) { ++ filtersCount; } if (start != null) { query.and(fieldName + " >= ?", start); } if (end != null) { query.and(fieldName + " <= ?", end); } } else if ("n".equals(queryType)) { Double minimum = ObjectUtils.to(Double.class, fieldValue); Double maximum = ObjectUtils.to(Double.class, value.get("x")); if (minimum != null && maximum != null) { ++ filtersCount; } if (minimum != null) { query.and(fieldName + " >= ?", fieldValue); } if (maximum != null) { query.and(fieldName + " <= ?", maximum); } } else { if (fieldValue == null) { continue; } ++ filtersCount; if ("t".equals(queryType)) { if (selectedType == null || Content.Static.isSearchableType(selectedType)) { query.and(fieldName + " matches ?", fieldValue); } else { query.and(fieldName + " contains[c] ?", fieldValue); } } else if ("b".equals(queryType)) { query.and(fieldName + ("true".equals(fieldValue) ? " = true" : " != true")); } else { int count = ObjectUtils.to(int.class, value.get("#")); if (count > 0) { query.and(fieldName + " = ?", IntStream.range(0, count) .mapToObj(i -> value.get(String.valueOf(i))) .collect(Collectors.toSet())); } else { query.and(fieldName + " = ?", fieldValue); } } } } } if (!isIgnoreSite() && (selectedType == null || !selectedType.getGroups().contains(Global.class.getName())) && types.stream().anyMatch(t -> !t.getGroups().contains(Global.class.getName())) && site != null && !site.isAllSitesAccessible()) { Set<ObjectType> globalTypes = new HashSet<ObjectType>(); if (selectedType != null) { addGlobalTypes(globalTypes, selectedType); } else { for (ObjectType type : validTypes) { addGlobalTypes(globalTypes, type); } } if (globalTypes.isEmpty()) { query.and(site.itemsPredicate()); } else { query.and(CompoundPredicate.combine( PredicateParser.OR_OPERATOR, site.itemsPredicate(), PredicateParser.Static.parse("_type = ?", globalTypes))); } } query.and(getVisibilitiesPredicate(selectedType, visibilities, validTypeIds, isShowDrafts())); if (validTypeIds != null) { if (page != null) { query.and("_type = ?", validTypeIds.stream() .filter(typeId -> page.hasPermission("type/" + typeId + "/read")) .collect(Collectors.toSet())); } else { query.and("_type = ?", validTypeIds); } } else if (page != null) { ToolUser user = page.getUser(); if (user != null) { ToolRole role = user.getRole(); if (role != null && role.getPermissions().contains("+type/")) { query.and(page.userTypesPredicate()); } } } String color = getColor(); if (color != null && color.startsWith("#")) { ++ filtersCount; int[] husl = HuslColorSpace.Static.toHUSL(new Color(Integer.parseInt(color.substring(1), 16))); int normalized0 = husl[0]; int normalized1 = husl[1]; int normalized2 = husl[2]; if (normalized1 < 30 || normalized2 < 15 || normalized2 > 85) { normalized0 = 0; normalized1 = 0; } Set<String> fieldNames = new HashSet<String>(); int[] binSizes = { 24, 20, 20 }; int[] normalized = new int[] { (((int) Math.round(normalized0 / 24.0)) * 24) % 360, ((int) Math.round(normalized1 / 20.0)) * 20, ((int) Math.round(normalized2 / 20.0)) * 20 }; for (int i = -1; i <= 1; i += 1) { for (int j = -1; j <= 1; j += 1) { for (int k = -1; k <= 1; k += 1) { int h = (normalized[0] + i * binSizes[0]) % 360; int s = normalized[1] + j * binSizes[1]; int l = normalized[2] + k * binSizes[2]; if ((i != 0 || j != 0 || k != 0) && (h >= 0 && h < 360) && (s >= 0 && s <= 100) && s != 20 && (s != 0 || h == 0) && (l >= 0 && l <= 100) && (l < 100 && l > 0 || s == 0)) { fieldNames.add("color.distribution/n_" + h + "_" + s + "_" + l); } } } } String originFieldName = "color.distribution/n_" + normalized[0] + "_" + normalized[1] + "_" + normalized[2]; fieldNames.add(originFieldName); query.and(StringUtils.join(new ArrayList<String>(fieldNames), " > 0 || ") + " > 0"); query.sortDescending(originFieldName); List<Sorter> sorters = query.getSorters(); sorters.add(0, sorters.get(sorters.size() - 1)); } for (Tool tool : Query.from(Tool.class).selectAll()) { tool.updateSearchQuery(this, query); } if (page != null) { QueryRestriction.updateQueryUsingAll(query, page); // Recent searches. String context = page.param(String.class, CONTEXT_PARAMETER); String sessionId = page.param(String.class, SESSION_ID_PARAMETER); if (!ObjectUtils.isBlank(context) && !ObjectUtils.isBlank(sessionId)) { ToolUser user = page.getUser(); if (user != null && (!ObjectUtils.isBlank(queryString) || (selectedType != null && validTypes.size() != 1) || filtersCount > 0)) { // Delete all searches from the current session. String keyPrefix = user.getId().toString() + context; String key = keyPrefix + sessionId; Query.from(ToolUserSearch.class).where("key = ?", key).deleteAll(); // Remember search query string for later. String searchQueryString = page.url("", NAME_PARAMETER, null); int questionAt = searchQueryString.indexOf('?'); if (questionAt > -1) { searchQueryString = searchQueryString.substring(questionAt + 1); } // Save the search for the recent searches list. ToolUserSearch recentSearch = new ToolUserSearch(); recentSearch.setKey(key); recentSearch.setQueryString(queryString); recentSearch.setSelectedType(selectedType != null && validTypes.size() != 1 ? selectedType : null); recentSearch.setFiltersCount(filtersCount); recentSearch.setSearch(searchQueryString); recentSearch.save(); // Only keep up to 5 recent searches. List<ToolUserSearch> recentSearches = Query.from(ToolUserSearch.class) .where("key startsWith ?", keyPrefix) .sortDescending("key") .select(5, 1) .getItems(); if (!recentSearches.isEmpty()) { Query.from(ToolUserSearch.class) .where("key startsWith ?", keyPrefix) .and("key <= ?", recentSearches.get(0).getKey()) .deleteAll(); } } } } return query; } private void addGlobalTypes(Set<ObjectType> globalTypes, ObjectType type) { if (type != null) { Set<String> groups = type.getGroups(); if (groups.contains(ObjectType.class.getName()) || groups.contains(ToolEntity.class.getName())) { globalTypes.add(type); } } } private static void addVisibilityFields(Set<String> comparisonKeys, ObjectStruct struct) { if (struct == null) { return; } for (ObjectIndex index : struct.getIndexes()) { if (index.isVisibility()) { for (String fieldName : index.getFields()) { ObjectField field = struct.getField(fieldName); if (field != null) { comparisonKeys.add(field.getUniqueName()); } } } } } /** * @param itemWriter May be {@code null}. * @throws IOException if unable to write to the given {@code page}. */ public void writeResultHtml(SearchResultItem itemWriter) throws IOException { List<SearchResultView> views = new ArrayList<>(); for (Class<? extends SearchResultView> viewClass : ClassFinder.Static.findClasses(SearchResultView.class)) { if (!viewClass.isInterface() && !Modifier.isAbstract(viewClass.getModifiers())) { SearchResultView view = TypeDefinition.getInstance(viewClass).newInstance(); if (view.isSupported(this)) { views.add(view); } } } String selectedViewClassName = page.param(String.class, "view"); ToolUser user = page.getUser(); if (user != null) { Map<String, String> userViews = user.getSearchViews(); ObjectType selectedType = getSelectedType(); String userViewKey = selectedType != null ? selectedType.getId().toString() : ""; String userViewClassName = userViews.get(userViewKey); if (selectedViewClassName == null) { selectedViewClassName = userViewClassName; } else if (!ObjectUtils.equals(userViewClassName, selectedViewClassName)) { userViews.put(userViewKey, selectedViewClassName); user.save(); } } SearchResultView selectedView = null; for (SearchResultView view : views) { if (view.getClass().getName().equals(selectedViewClassName)) { selectedView = view; break; } } if (selectedView == null) { for (SearchResultView view : views) { if (view.isPreferred(this)) { selectedView = view; break; } } } if (selectedView == null) { selectedView = new ListSearchResultView(); } if (selectedView.isHtmlWrapped(this, page, itemWriter)) { page.writeStart("div", "class", "searchResult-container"); page.writeStart("div", "class", "searchResult-items"); page.writeStart("div", "class", "searchResult-views"); page.writeStart("ul", "class", "piped"); for (SearchResultView view : views) { page.writeStart("li", "class", view.equals(selectedView) ? "selected" : null); page.writeStart("a", "class", "icon icon-" + view.getIconName(), "href", page.url("", "view", view.getClass().getName())); page.writeHtml(view.getDisplayName()); page.writeEnd(); page.writeEnd(); } page.writeEnd(); page.writeEnd(); page.writeStart("div", "class", "searchResult-view"); boolean viewWritten = writeViewHtml(itemWriter, selectedView); page.writeEnd(); page.writeEnd(); if (viewWritten) { page.writeStart("div", "class", "frame searchResult-actions", "name", createFrameName("SearchResultActions")); page.writeStart("a", "href", page.toolUrl(CmsTool.class, "/searchResultActions", "search", ObjectUtils.toJson(getState().getSimpleValues()))); page.writeEnd(); page.writeEnd(); } page.writeEnd(); } else { writeViewHtml(itemWriter, selectedView); } } private boolean writeViewHtml(SearchResultItem itemWriter, SearchResultView view) throws IOException { try { view.writeHtml(this, page, itemWriter != null ? itemWriter : new SearchResultItem()); return true; } catch (IllegalArgumentException | Query.NoFieldException error) { page.writeStart("div", "class", "message message-error"); page.writeHtml("Invalid advanced query: "); page.writeHtml(error.getMessage()); page.writeEnd(); return false; } } /** @deprecated Use {@link #toQuery(Site)} instead. */ @Deprecated public Query<?> toQuery() { return toQuery(null); } // --- Deprecated --- /** @deprecated Use {@link #ADDITIONAL_PREDICATE_PARAMETER} instead. */ @Deprecated public static final String ADDITIONAL_QUERY_PARAMETER = ADDITIONAL_PREDICATE_PARAMETER; /** @deprecated Use {@link #ONLY_PATHED_PARAMETER} instead. */ @Deprecated public static final String IS_ONLY_PATHED = ONLY_PATHED_PARAMETER; /** @deprecated Use {@link #TYPES_PARAMETER} instead. */ @Deprecated public static final String REQUESTED_TYPES_PARAMETER = TYPES_PARAMETER; /** @deprecated Use {@link Search(ToolPageContext, Collection)} instead. */ @Deprecated public Search(ToolPageContext page, UUID... typeIds) { this(page, typeIds != null ? Arrays.asList(typeIds) : null); } /** @deprecated Use {@link #getTypes} instead. */ @Deprecated public Set<ObjectType> getRequestedTypes() { return getTypes(); } /** @deprecated Use {@link #findValidTypes} instead. */ public Set<ObjectType> getValidTypes() { return findValidTypes(); } /** @deprecated Use {@link #toQuery} instead. */ @Deprecated public Query<?> getQuery() { return toQuery(); } @Deprecated private transient ToolPageContext page; @Deprecated private transient PaginatedResult<?> result; /** @deprecated Use {@link #toQuery} instead. */ @Deprecated public PaginatedResult<?> getResult() { if (result == null) { result = toQuery(page != null ? page.getSite() : null).select(getOffset(), getLimit()); } return result; } }