package com.psddev.cms.tool; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import com.psddev.dari.db.CompoundPredicate; import com.psddev.dari.util.JspUtils; import org.joda.time.DateTime; import com.psddev.cms.db.Directory; import com.psddev.cms.db.Renderer; import com.psddev.cms.db.Site; import com.psddev.cms.db.Taxon; import com.psddev.cms.db.ToolUi; import com.psddev.dari.db.Database; import com.psddev.dari.db.Metric; import com.psddev.dari.db.MetricInterval; import com.psddev.dari.db.ObjectField; 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.Recordable; import com.psddev.dari.db.State; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.PaginatedResult; import com.psddev.dari.util.StorageItem; import com.psddev.dari.util.StringUtils; public class SearchResultRenderer { public static final String TAXON_LEVEL_PARAMETER = "taxonLevel"; private static final String ATTRIBUTE_PREFIX = SearchResultRenderer.class.getName() + "."; private static final String PREVIOUS_DATE_ATTRIBUTE = ATTRIBUTE_PREFIX + "previousDate"; private static final String MAX_SUM_ATTRIBUTE = ATTRIBUTE_PREFIX + ".maximumSum"; private static final String TAXON_PARENT_ID_PARAMETER = "taxonParentId"; private static final String SORT_SETTING_PREFIX = "sort/"; protected final ToolPageContext page; @Deprecated protected final PageWriter writer; protected final Search search; protected final ObjectField sortField; protected final boolean showSiteLabel; protected final boolean showTypeLabel; protected final PaginatedResult<?> result; protected final Exception queryError; @SuppressWarnings("deprecation") public SearchResultRenderer(ToolPageContext page, Search search) throws IOException { this.page = page; this.writer = page.getWriter(); this.search = search; ObjectType selectedType = search.getSelectedType(); ToolUi ui = selectedType == null ? null : selectedType.as(ToolUi.class); PaginatedResult<?> result = null; if (search.getSort() == null) { if (ui != null && ui.getDefaultSortField() != null) { search.setSort(ui.getDefaultSortField()); } else if (!ObjectUtils.isBlank(search.getQueryString())) { search.setSort(Search.RELEVANT_SORT_VALUE); } else { Map<String, String> f = search.getFieldFilters().get("cms.content.publishDate"); if (f != null && (f.get("") != null || f.get("x") != null)) { search.setSort("cms.content.publishDate"); } else { search.setSort("cms.content.updateDate"); } } } showSiteLabel = Query.from(CmsTool.class).first().isDisplaySiteInSearchResult() && page.getSite() == null && Query.from(Site.class).hasMoreThan(0); Exception queryError = null; if (selectedType != null) { this.sortField = selectedType.getFieldGlobally(search.getSort()); this.showTypeLabel = selectedType.as(ToolUi.class).findDisplayTypes().size() != 1; if (ObjectType.getInstance(ObjectType.class).equals(selectedType)) { List<ObjectType> types = new ArrayList<ObjectType>(); try { Predicate predicate = search.toQuery(page.getSite()).getPredicate(); for (ObjectType t : Database.Static.getDefault().getEnvironment().getTypes()) { if (t.is(predicate)) { types.add(t); } } result = new PaginatedResult<ObjectType>(search.getOffset(), search.getLimit(), types); } catch (IllegalArgumentException | Query.NoFieldException error) { queryError = error; } } } else { this.sortField = Database.Static.getDefault().getEnvironment().getField(search.getSort()); this.showTypeLabel = search.findValidTypes().size() != 1; } if (result == null) { try { result = search.toQuery(page.getSite()).select(search.getOffset(), search.getLimit()); } catch (IllegalArgumentException | Query.NoFieldException error) { queryError = error; } } this.result = result; this.queryError = queryError; } @SuppressWarnings("unchecked") public void render() throws IOException { if (queryError != null) { page.writeStart("div", "class", "message message-error"); page.writeHtml("Invalid advanced query: "); page.writeHtml(queryError.getMessage()); page.writeEnd(); return; } boolean resultsDisplayed = false; int level = page.paramOrDefault(int.class, TAXON_LEVEL_PARAMETER, 1); if (level == 1) { List<Object> newItems = Query.fromAll() .where("_id = ?", search.getNewItemIds()) .selectAll(); if (!newItems.isEmpty()) { page.writeStart("h2").writeHtml("New").writeEnd(); renderList(newItems); } page.writeStart("h2").writeHtml("Result").writeEnd(); } // check if the Taxon UI should be displayed ObjectType taxonType = null; if (ObjectUtils.isBlank(search.getQueryString()) && search.getVisibilities().isEmpty()) { if (search.getSelectedType() != null) { if (search.getSelectedType().getGroups().contains(Taxon.class.getName())) { taxonType = search.getSelectedType(); } } else if (search.getTypes() != null && search.getTypes().size() == 1) { ObjectType searchType = search.getTypes().iterator().next(); if (searchType.getGroups().contains(Taxon.class.getName())) { taxonType = searchType; } } } // display the taxon UI if the type is not null if (taxonType != null) { search.setSuggestions(false); int nextLevel = level + 1; Collection<Taxon> taxonResults = null; UUID taxonParentUuid = page.paramOrDefault(UUID.class, TAXON_PARENT_ID_PARAMETER, null); Site site = page.getSite(); Predicate predicate = search.toQuery(page.getSite()).getPredicate(); if (!ObjectUtils.isBlank(taxonParentUuid)) { Taxon parent = Query.findById(Taxon.class, taxonParentUuid); Predicate filterPredicate = (site != null && predicate != null) ? CompoundPredicate.combine(PredicateParser.AND_OPERATOR, predicate, site.itemsPredicate()) : site != null ? site.itemsPredicate() : predicate != null ? predicate : null; taxonResults = (Collection<Taxon>) Taxon.Static.getChildren(parent, filterPredicate); } else { taxonResults = Taxon.Static.getRoots((Class<Taxon>) taxonType.getObjectClass(), site, predicate); } if (!ObjectUtils.isBlank(taxonResults)) { resultsDisplayed = true; if (level == 1) { page.writeStart("div", "class", "searchResultTaxonomy"); } renderTaxonList(taxonResults, nextLevel); if (level == 1) { page.writeEnd(); writeSuggestions(); } } } if (!resultsDisplayed) { if (search.findSorts().size() > 1) { page.writeStart("div", "class", "searchSorter"); renderSorter(); page.writeEnd(); } page.writeStart("div", "class", "searchPagination"); renderPagination(); page.writeEnd(); page.writeStart("div", "class", "searchResultList"); if (result.hasPages()) { renderList(result.getItems()); } else { renderEmpty(); } writeSuggestions(); page.writeEnd(); } } private void writeSuggestions() throws IOException { if (search.isSuggestions() && ObjectUtils.isBlank(search.getQueryString())) { String frameName = page.createId(); page.writeStart("div", "class", "frame", "name", frameName); page.writeEnd(); page.writeStart("form", "class", "searchSuggestionsForm", "method", "post", "action", page.url("/content/suggestions"), "target", frameName); page.writeElement("input", "type", "hidden", "name", "search", "value", ObjectUtils.toJson(search.getState().getSimpleValues())); page.writeEnd(); } } private void writeTaxon(Taxon taxon, int nextLevel, String target) throws IOException { page.writeStart("li"); if (taxon.as(Taxon.Data.class).isSelectable()) { renderBeforeItem(taxon); writeTaxonLabel(taxon); renderAfterItem(taxon); } else { writeTaxonLabel(taxon); } Predicate predicate = search.toQuery(page.getSite()).getPredicate(); Collection<? extends Taxon> children = Taxon.Static.getChildren(taxon, predicate); if (children != null && !children.isEmpty()) { page.writeStart("a", "href", page.url("", TAXON_PARENT_ID_PARAMETER, taxon.as(Taxon.Data.class).getId(), TAXON_LEVEL_PARAMETER, nextLevel), "class", "searchResultTaxonomyExpand", "target", target); page.writeEnd(); } page.writeEnd(); } private void writeTaxonLabel(Taxon taxon) throws IOException { if (taxon == null) { page.writeHtml("N/A"); } String altLabel = taxon.as(Taxon.Data.class).getAltLabel(); if (ObjectUtils.isBlank(altLabel)) { page.writeObjectLabel(taxon); } else { String visibilityLabel = taxon.getState().getVisibilityLabel(); if (!ObjectUtils.isBlank(visibilityLabel)) { page.writeStart("span", "class", "visibilityLabel"); page.writeHtml(visibilityLabel); page.writeEnd(); page.writeHtml(" "); } page.writeHtml(altLabel); } } public void renderSorter() throws IOException { page.writeStart("form", "data-bsp-autosubmit", "", "method", "get", "action", page.url(null)); for (Map.Entry<String, List<String>> entry : StringUtils.getQueryParameterMap(page.url("", Search.SORT_PARAMETER, null, Search.SHOW_MISSING_PARAMETER, null, Search.OFFSET_PARAMETER, null)).entrySet()) { String name = entry.getKey(); for (String value : entry.getValue()) { page.writeElement("input", "type", "hidden", "name", name, "value", value); } } page.writeStart("select", "name", Search.SORT_PARAMETER); for (Map.Entry<String, String> entry : search.findSorts().entrySet()) { String label = entry.getValue(); String value = entry.getKey(); page.writeStart("option", "value", value, "selected", value.equals(search.getSort()) ? "selected" : null); page.writeHtml("Sort: ").writeHtml(label); page.writeEnd(); } page.writeEnd(); page.writeEnd(); } public void renderPagination() throws IOException { page.writeStart("ul", "class", "pagination"); if (result.hasPrevious()) { page.writeStart("li", "class", "previous"); page.writeStart("a", "href", page.url("", Search.OFFSET_PARAMETER, result.getPreviousOffset())); page.writeHtml("Previous "); page.writeHtml(result.getLimit()); page.writeEnd(); page.writeEnd(); } page.writeStart("li"); page.writeHtml(result.getFirstItemIndex()); page.writeHtml(" to "); page.writeHtml(result.getLastItemIndex()); page.writeHtml(" of "); page.writeStart("strong").writeHtml(result.getCount()).writeEnd(); page.writeEnd(); if (result.hasNext()) { page.writeStart("li", "class", "next"); page.writeStart("a", "href", page.url("", Search.OFFSET_PARAMETER, result.getNextOffset())); page.writeHtml("Next "); page.writeHtml(result.getLimit()); page.writeEnd(); page.writeEnd(); } page.writeEnd(); } public void renderList(Collection<?> listItems) throws IOException { List<Object> items = new ArrayList<Object>(listItems); Map<Object, StorageItem> previews = new LinkedHashMap<Object, StorageItem>(); ITEM: for (ListIterator<Object> i = items.listIterator(); i.hasNext();) { Object item = i.next(); for (Tool tool : Query.from(Tool.class).selectAll()) { if (!tool.isDisplaySearchResultItem(search, item)) { continue ITEM; } } State itemState = State.getInstance(item); StorageItem preview = itemState.getPreview(); if (preview != null) { String contentType = preview.getContentType(); if (contentType != null && contentType.startsWith("image/")) { i.remove(); previews.put(item, preview); } } } if (!previews.isEmpty()) { page.writeStart("div", "class", "searchResultImages"); for (Map.Entry<Object, StorageItem> entry : previews.entrySet()) { renderImage(entry.getKey(), entry.getValue()); } page.writeEnd(); } if (!items.isEmpty()) { page.writeStart("table", "class", "searchResultTable links table-striped pageThumbnails"); page.writeStart("tbody"); for (Object item : items) { renderRow(item); } page.writeEnd(); page.writeEnd(); } } public void renderTaxonList(Collection<?> listItems, int nextLevel) throws IOException { String target = "t" + UUID.randomUUID().toString(); page.writeStart("div", "class", "searchResultTaxonomyColumn"); page.writeStart("ul"); for (Taxon taxon : (Collection<Taxon>) listItems) { writeTaxon(taxon, nextLevel, target); } page.writeEnd(); page.writeEnd(); page.writeStart("div", "class", "frame searchResultTaxonomyChildren", "name", target); page.writeEnd(); } public void renderImage(Object item, StorageItem image) throws IOException { renderBeforeItem(item); page.writeStart("figure"); page.writeElement("img", "src", page.getPreviewThumbnailUrl(item), "alt", (showSiteLabel ? page.getObjectLabel(State.getInstance(item).as(Site.ObjectModification.class).getOwner()) + ": " : "") + (showTypeLabel ? page.getTypeLabel(item) + ": " : "") + page.getObjectLabel(item)); page.writeStart("figcaption"); if (showSiteLabel) { page.writeObjectLabel(State.getInstance(item).as(Site.ObjectModification.class).getOwner()); page.writeHtml(": "); } if (showTypeLabel) { page.writeTypeLabel(item); page.writeHtml(": "); } page.writeObjectLabel(item); page.writeEnd(); page.writeEnd(); renderAfterItem(item); } public void renderRow(Object item) throws IOException { HttpServletRequest request = page.getRequest(); State itemState = State.getInstance(item); String permalink = itemState.as(Directory.ObjectModification.class).getPermalink(); Integer embedWidth = null; if (ObjectUtils.isBlank(permalink)) { ObjectType type = itemState.getType(); if (type != null) { Renderer.TypeModification rendererData = type.as(Renderer.TypeModification.class); int previewWidth = rendererData.getEmbedPreviewWidth(); if (previewWidth > 0 && page.isEmbeddable(item)) { permalink = JspUtils.getAbsolutePath(page.getRequest(), "/_preview", "_embed", "true", "_cms.db.previewId", itemState.getId()); embedWidth = previewWidth; } } } page.writeStart("tr", "data-preview-url", permalink, "data-preview-embed-width", embedWidth, "class", State.getInstance(item).getId().equals(page.param(UUID.class, "id")) ? "selected" : null); if (sortField != null && ObjectField.DATE_TYPE.equals(sortField.getInternalType())) { DateTime dateTime = page.toUserDateTime(itemState.getByPath(sortField.getInternalName())); if (dateTime == null) { page.writeStart("td", "colspan", 2); page.writeHtml("N/A"); page.writeEnd(); } else { String date = page.formatUserDate(dateTime); page.writeStart("td", "class", "date"); if (!ObjectUtils.equals(date, request.getAttribute(PREVIOUS_DATE_ATTRIBUTE))) { request.setAttribute(PREVIOUS_DATE_ATTRIBUTE, date); page.writeHtml(date); } page.writeEnd(); page.writeStart("td", "class", "time"); page.writeHtml(page.formatUserTime(dateTime)); page.writeEnd(); } } if (showSiteLabel) { page.writeStart("td"); page.writeObjectLabel(itemState.as(Site.ObjectModification.class).getOwner()); page.writeEnd(); } if (showTypeLabel) { page.writeStart("td"); page.writeTypeLabel(item); page.writeEnd(); } page.writeStart("td"); renderBeforeItem(item); page.writeObjectLabel(item); renderAfterItem(item); page.writeEnd(); if (sortField != null && !ObjectField.DATE_TYPE.equals(sortField.getInternalType())) { String sortFieldName = sortField.getInternalName(); Object value = itemState.getByPath(sortFieldName); page.writeStart("td"); if (value instanceof Metric) { page.writeStart("span", "style", page.cssString("white-space", "nowrap")); Double maxSum = (Double) request.getAttribute(MAX_SUM_ATTRIBUTE); if (maxSum == null) { Object maxObject = search.toQuery(page.getSite()).sortDescending(sortFieldName).first(); maxSum = maxObject != null ? ((Metric) State.getInstance(maxObject).get(sortFieldName)).getSum() : 1.0; request.setAttribute(MAX_SUM_ATTRIBUTE, maxSum); } Metric valueMetric = (Metric) value; Map<DateTime, Double> sumEntries = valueMetric.groupSumByDate( new MetricInterval.Daily(), new DateTime().dayOfMonth().roundFloorCopy().minusDays(7), null); double sum = valueMetric.getSum(); long sumLong = (long) sum; if (sumLong == sum) { page.writeHtml(String.format("%,2d ", sumLong)); } else { page.writeHtml(String.format("%,2.2f ", sum)); } if (!sumEntries.isEmpty()) { long minMillis = Long.MAX_VALUE; long maxMillis = Long.MIN_VALUE; for (Map.Entry<DateTime, Double> sumEntry : sumEntries.entrySet()) { long sumMillis = sumEntry.getKey().getMillis(); if (sumMillis < minMillis) { minMillis = sumMillis; } if (sumMillis > maxMillis) { maxMillis = sumMillis; } } double cumulativeSum = 0.0; StringBuilder path = new StringBuilder(); double xRange = maxMillis - minMillis; int width = 35; int height = 18; for (Map.Entry<DateTime, Double> sumEntry : sumEntries.entrySet()) { cumulativeSum += sumEntry.getValue(); path.append('L'); path.append((sumEntry.getKey().getMillis() - minMillis) / xRange * width); path.append(','); path.append(height - cumulativeSum / maxSum * height); } path.setCharAt(0, 'M'); page.writeStart("svg", "xmlns", "http://www.w3.org/2000/svg", "width", width, "height", height, "style", page.cssString( "display", "inline-block", "vertical-align", "middle")); page.writeStart("path", "fill", "none", "stroke", "#444444", "d", path.toString()); page.writeEnd(); page.writeEnd(); } page.writeEnd(); } else if (value instanceof Recordable) { page.writeHtml(((Recordable) value).getState().getLabel()); } else { page.writeHtml(value); } page.writeEnd(); } page.writeEnd(); } public void renderBeforeItem(Object item) throws IOException { page.writeStart("a", "href", page.toolUrl(CmsTool.class, "/content/edit.jsp", "id", State.getInstance(item).getId()), "data-objectId", State.getInstance(item).getId(), "target", "_top"); } public void renderAfterItem(Object item) throws IOException { page.writeEnd(); } public void renderEmpty() throws IOException { page.writeStart("div", "class", "message message-warning"); page.writeStart("p"); page.writeHtml("No matching items!"); page.writeEnd(); page.writeEnd(); } }