package com.psddev.cms.tool.page; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.stream.Collectors; import javax.servlet.ServletException; import com.google.common.collect.ImmutableMap; import com.psddev.cms.db.ImageTag; import com.psddev.cms.db.Localization; import com.psddev.cms.db.ResizeOption; import com.psddev.cms.db.ToolUser; import com.psddev.cms.tool.PageServlet; import com.psddev.cms.tool.Search; import com.psddev.cms.tool.SearchResultAction; import com.psddev.cms.tool.SearchResultSelection; import com.psddev.cms.tool.ToolPageContext; import com.psddev.dari.db.Query; import com.psddev.dari.db.State; import com.psddev.dari.util.ClassFinder; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.RoutingFilter; import com.psddev.dari.util.StorageItem; import com.psddev.dari.util.StringUtils; import com.psddev.dari.util.TypeDefinition; import com.psddev.dari.util.UrlBuilder; @RoutingFilter.Path(application = "cms", value = "/searchResultActions") public class SearchResultActions extends PageServlet { private static final long serialVersionUID = 1L; public static final String SELECTION_ID_PARAMETER = "selectionId"; public static final String ITEM_ID_PARAMETER = "id"; public static final String ACTION_PARAMETER = "action"; public static final String ACTION_ADD = "item-add"; public static final String ACTION_REMOVE = "item-remove"; public static final String ACTION_CLEAR = "clear"; public static final String ACTION_ACTIVATE = "activate"; @Override protected String getPermissionId() { return null; } @Override protected void doService(ToolPageContext page) throws IOException, ServletException { ToolUser user = page.getUser(); SearchResultSelection currentSelection = user.getCurrentSearchResultSelection(); if (currentSelection == null) { throw new IllegalArgumentException(); } Search search = new Search(); @SuppressWarnings("unchecked") Map<String, Object> searchValues = (Map<String, Object>) ObjectUtils.fromJson(page.param(String.class, "search")); search.getState().setValues(searchValues); String action = page.param(String.class, ACTION_PARAMETER); UUID selectionId = page.param(UUID.class, SELECTION_ID_PARAMETER); if (ACTION_ADD.equals(action)) { for (UUID item : page.params(UUID.class, ITEM_ID_PARAMETER)) { currentSelection.addItem(item); } } else if (ACTION_REMOVE.equals(action)) { for (UUID item : page.params(UUID.class, ITEM_ID_PARAMETER)) { currentSelection.removeItem(item); } } else if (ACTION_CLEAR.equals(action) && user.isSavedSearchResultSelection(currentSelection)) { // delete the saved selection currentSelection.delete(); // reset the current selection currentSelection = user.resetCurrentSelection(); } else if (ACTION_CLEAR.equals(action) || (ACTION_ACTIVATE.equals(action) && selectionId == null)) { // deactivate the current selection currentSelection = user.deactivateSelection(currentSelection); page.writeStart("div", "id", page.createId()); page.writeEnd(); page.writeStart("script", "type", "text/javascript"); page.writeRaw("$('#" + page.getId() + "').closest('.searchResult').find('.searchResult-list :checkbox').prop('checked', false);"); page.writeEnd(); } else if (ACTION_ACTIVATE.equals(action)) { // activate the specified selection currentSelection = user.activateSelection(Query.from(SearchResultSelection.class).where("_id = ?", selectionId).first()); page.writeStart("div", "id", page.createId()); page.writeEnd(); page.writeStart("script", "type", "text/javascript"); page.writeRaw("$('#" + page.getId() + "').closest('.searchForm').find('.searchFiltersRest').submit()"); page.writeEnd(); } long count = currentSelection.size(); List<SearchResultSelection> own = SearchResultSelection.findOwnSelections(user); if (own != null && own.contains(currentSelection) && !user.isSavedSearchResultSelection(currentSelection)) { own.remove(currentSelection); } if (own != null && own.size() > 0) { page.writeStart("form", "method", "get", "action", page.cmsUrl("/searchResultActions")); page.writeTag("input", "type", "hidden", "name", ACTION_PARAMETER, "value", ACTION_ACTIVATE); page.writeTag("input", "type", "hidden", "name", "search", "value", ObjectUtils.toJson(new Search(page, (Set<UUID>) null).getState().getSimpleValues())); page.writeStart("select", "data-searchable", "true", "data-bsp-autosubmit", "", "name", SELECTION_ID_PARAMETER); page.writeStart("option", "value", ""); page.writeHtml(page.localize( SearchResultActions.class, "option.newSelection")); page.writeEnd(); for (SearchResultSelection ownSelection : own) { page.writeStart("option", "selected", ownSelection.equals(currentSelection) ? "selected" : null, "value", ownSelection.getId()); page.writeObjectLabel(ownSelection); page.writeEnd(); } page.writeEnd(); page.writeEnd(); page.writeStart("div", "class", "searchResult-action-simple"); writeDeleteAction(page, count); page.writeEnd(); } else if (count > 0) { page.writeStart("h2"); page.writeHtml(page.localize(SearchResultActions.class, "subtitle.selection")); writeDeleteAction(page, count); page.writeEnd(); } if (count > 0) { writeSelectionPreview(page, currentSelection); } page.writeStart("a", "class", "reload", "href", new UrlBuilder(page.getRequest()) .currentScheme() .currentHost() .currentPath() .parameter("search", ObjectUtils.toJson(new Search(page, (Set<UUID>) null).getState().getSimpleValues()))).writeEnd(); if (count == 0) { page.writeStart("h2"); page.writeHtml(page.localize(SearchResultActions.class, "subtitle.all")); page.writeEnd(); } page.writeStart("div", "class", "searchResult-actions-body"); for (Map.Entry<String, List<SearchResultAction>> entry : ClassFinder .findConcreteClasses(SearchResultAction.class) .stream() .map(c -> (SearchResultAction) TypeDefinition.getInstance(c).newInstance()) .collect(Collectors.groupingBy( a -> { String group = a.getGroup(); return StringUtils.isBlank(group) ? "" : group; }, TreeMap::new, Collectors.toList())) .entrySet()) { List<? extends SearchResultAction> actions = entry.getValue(); Collections.sort(actions, Comparator .comparing(SearchResultAction::getPosition) .thenComparing(a -> a.getClass().getSimpleName()) .thenComparing(a -> a.getClass().getName())); // Don't write to the page directly so that the group heading can // be omitted if there isn't any output from any of the actions. ToolPageContext outputPage = new ToolPageContext(page.getServletContext(), page.getRequest(), page.getResponse()); StringWriter outputWriter = new StringWriter(); outputPage.setDelegate(outputWriter); for (SearchResultAction a : actions) { a.writeHtml(outputPage, search, count > 0 ? currentSelection : null); } String output = outputWriter.toString(); if (!StringUtils.isBlank(output)) { String group = entry.getKey(); if (!StringUtils.isBlank(group)) { page.writeStart("h2"); page.writeHtml(Localization.currentUserText(SearchResultActions.class, group, group)); page.writeEnd(); } page.write(output); } } page.writeEnd(); } private void writeDeleteAction(ToolPageContext page, long count) throws IOException { if (page.getUser().isSavedSearchResultSelection(page.getUser().getCurrentSearchResultSelection())) { page.writeStart("a", "class", "searchResult-selectionReset action action-delete", "href", page.url("", ACTION_PARAMETER, ACTION_CLEAR, SELECTION_ID_PARAMETER, null)); page.writeHtml(page.localize(SearchResultActions.class, "action.delete")); page.writeEnd(); } else if (count > 0) { page.writeStart("a", "class", "searchResult-selectionReset action action-cancel", "href", page.url("", ACTION_PARAMETER, ACTION_CLEAR, SELECTION_ID_PARAMETER, null)); page.writeHtml(page.localize(SearchResultActions.class, "action.clear")); page.writeEnd(); } } private void writeSelectionPreview(ToolPageContext page, SearchResultSelection selection) throws IOException { page.writeStart("div", "class", "searchResult-selectionPreview"); Map<Object, StorageItem> previews = new LinkedHashMap<>(); List<Object> items = new ArrayList<>(selection.createItemsQuery().selectAll()); int count = 0; // store up to the first 100 previewable States with state.getPreview() != null for (ListIterator<Object> i = items.listIterator(); i.hasNext() && count < 100;) { Object item = i.next(); 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); count ++; } } } // render the preview thumbnails, max height 30, dynamic resize for (Map.Entry<Object, StorageItem> entry : previews.entrySet()) { Object item = entry.getKey(); String previewUrl = new ImageTag.Builder(entry.getValue()) .setHeight(30) .setResizeOption(ResizeOption.ONLY_SHRINK_LARGER) .toUrl(); page.writeStart("figure"); page.writeElement("img", "src", previewUrl, "alt", page.getObjectLabel(item)); page.writeEnd(); // end figure } page.writeEnd(); // end .searchResult-selectionPreview // write out the number of items not shown in the preview thumbnails. if (items.size() > 0) { page.writeStart("p"); if (count > 0) { page.writeHtml(page.localize( SearchResultActions.class, ImmutableMap.of("size", items.size()), "message.moreItemsSelected")); } else { page.writeHtml(page.localize( SearchResultActions.class, ImmutableMap.of("size", items.size()), "message.itemsSelected")); } page.writeEnd(); } } }