/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Thomas Roger */ package org.nuxeo.search.ui.seam; import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.apache.commons.logging.LogFactory.getLog; import static org.jboss.seam.ScopeType.CONVERSATION; import static org.jboss.seam.annotations.Install.FRAMEWORK; import static org.nuxeo.ecm.webapp.helpers.EventNames.LOCAL_CONFIGURATION_CHANGED; import static org.nuxeo.ecm.webapp.helpers.EventNames.USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import javax.faces.model.SelectItem; import javax.faces.model.SelectItemGroup; import javax.faces.validator.ValidatorException; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.jboss.seam.annotations.Begin; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Install; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Observer; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.intercept.BypassInterceptors; import org.jboss.seam.core.Events; import org.jboss.seam.faces.FacesMessages; import org.jboss.seam.international.StatusMessage; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl; import org.nuxeo.ecm.platform.contentview.jsf.ContentView; import org.nuxeo.ecm.platform.contentview.jsf.ContentViewHeader; import org.nuxeo.ecm.platform.contentview.jsf.ContentViewService; import org.nuxeo.ecm.platform.contentview.jsf.ContentViewState; import org.nuxeo.ecm.platform.contentview.jsf.ContentViewStateImpl; import org.nuxeo.ecm.platform.contentview.json.JSONContentViewState; import org.nuxeo.ecm.platform.contentview.seam.ContentViewActions; import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; import org.nuxeo.ecm.platform.ui.web.api.WebActions; import org.nuxeo.ecm.platform.ui.web.rest.RestHelper; import org.nuxeo.ecm.platform.ui.web.util.BaseURL; import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; import org.nuxeo.ecm.platform.url.DocumentViewImpl; import org.nuxeo.ecm.platform.url.api.DocumentView; import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager; import org.nuxeo.ecm.webapp.action.ActionContextProvider; import org.nuxeo.ecm.webapp.documentsLists.DocumentsListsManager; import org.nuxeo.ecm.webapp.helpers.EventNames; import org.nuxeo.runtime.api.Framework; import org.nuxeo.search.ui.SearchUIService; /** * Seam bean handling Search main tab actions. * * @since 6.0 */ @Name("searchUIActions") @Scope(CONVERSATION) @Install(precedence = FRAMEWORK) public class SearchUIActions implements Serializable { private static final long serialVersionUID = 1L; private static final Log log = getLog(SearchUIActions.class); public static final String SAVED_SEARCHES_LABEL = "label.saved.searches"; public static final String SHARED_SEARCHES_LABEL = "label.shared.searches"; public static final String SEARCH_FILTERS_LABEL = "label.search.filters"; public static final String SEARCH_SAVED_LABEL = "label.search.saved"; public static final String MAIN_TABS_SEARCH = "MAIN_TABS:search"; public static final String SEARCH_VIEW_ID = "/search/search.xhtml"; public static final String SEARCH_CODEC = "docpathsearch"; public static final String SIMPLE_SEARCH_CONTENT_VIEW_NAME = "simple_search"; public static final String NXQL_SEARCH_CONTENT_VIEW_NAME = "nxql_search"; public static final String DEFAULT_NXQL_QUERY = "SELECT * FROM Document" + " WHERE ecm:mixinType != 'HiddenInNavigation'" + " AND ecm:isProxy = 0 AND ecm:isCheckedInVersion = 0" + " AND ecm:currentLifeCycleState != 'deleted'"; public static final String CONTENT_VIEW_NAME_PARAMETER = "contentViewName"; public static final String CURRENT_PAGE_PARAMETER = "currentPage"; public static final String PAGE_SIZE_PARAMETER = "pageSize"; public static final String CONTENT_VIEW_STATE_PARAMETER = "state"; public static final String SEARCH_TERM_PARAMETER = "searchTerm"; /** * Event name for search selection change, raised with selected corresponding content view name. * * @since 8.1 */ public static final String SEARCH_SELECTED_EVENT = "searchSelected"; /** * Event name for search selection change, raised with saved document model. * * @since 8.1 */ public static final String SEARCH_SAVED_EVENT = "searchSaved"; @In(create = true) protected transient NavigationContext navigationContext; @In(create = true, required = false) protected transient CoreSession documentManager; @In(create = true, required = false) protected transient ActionContextProvider actionContextProvider; @In(create = true) protected transient WebActions webActions; @In(create = true) protected RestHelper restHelper; @In(create = true) protected ContentViewActions contentViewActions; @In(create = true) protected ContentViewService contentViewService; @In(create = true) protected DocumentsListsManager documentsListsManager; @In(create = true, required = false) protected FacesMessages facesMessages; @In(create = true) protected Map<String, String> messages; protected String simpleSearchKeywords = ""; protected String nxqlQuery = DEFAULT_NXQL_QUERY; protected List<ContentViewHeader> contentViewHeaders; protected String currentContentViewName; protected String currentSelectedSavedSearchId; protected String currentPage; protected String pageSize; protected String searchTerm; protected String savedSearchTitle; public String getSearchMainTab() { return MAIN_TABS_SEARCH; } public void setSearchMainTab(String tabs) { webActions.setCurrentTabIds(!StringUtils.isBlank(tabs) ? tabs : MAIN_TABS_SEARCH); } public String getSearchViewTitle() { if (currentSelectedSavedSearchId != null) { DocumentModel savedSearch = documentManager.getDocument(new IdRef(currentSelectedSavedSearchId)); return savedSearch.getTitle(); } else if (currentContentViewName != null) { ContentView cv = contentViewActions.getContentView(currentContentViewName); String title = cv.getTranslateTitle() ? messages.get(cv.getTitle()) : cv.getTitle(); return isNotBlank(title) ? title : currentContentViewName; } return null; } /** * Returns true if the user is viewing SEARCH. */ public boolean isOnSearchView() { if (FacesContext.getCurrentInstance() == null) { return false; } UIViewRoot viewRoot = FacesContext.getCurrentInstance().getViewRoot(); if (viewRoot != null) { String viewId = viewRoot.getViewId(); // FIXME find a better way to update the current document only // if we are on SEARCH if (SEARCH_VIEW_ID.equals(viewId)) { return true; } } return false; } public String getJSONContentViewState() throws UnsupportedEncodingException { ContentView contentView = contentViewActions.getContentView(currentContentViewName); ContentViewService contentViewService = Framework.getService(ContentViewService.class); ContentViewState state = contentViewService.saveContentView(contentView); return JSONContentViewState.toJSON(state, true); } public String getCurrentContentViewName() { if (currentContentViewName == null) { List<ContentViewHeader> contentViewHeaders = getContentViewHeaders(); if (!contentViewHeaders.isEmpty()) { currentContentViewName = contentViewHeaders.get(0).getName(); } } return currentContentViewName; } public void setCurrentContentViewName(String contentViewName) { this.currentContentViewName = contentViewName; } public String getCurrentSelectedSavedSearchId() { return currentSelectedSavedSearchId != null ? currentSelectedSavedSearchId : currentContentViewName; } public void setCurrentSelectedSavedSearchId(String selectedSavedSearchId) throws UnsupportedEncodingException { resetCurrentContentViewWorkingList(); for (ContentViewHeader contentViewHeader : contentViewHeaders) { if (contentViewHeader.getName().equals(selectedSavedSearchId)) { contentViewActions.reset(currentContentViewName); currentContentViewName = selectedSavedSearchId; Events.instance().raiseEvent(SEARCH_SELECTED_EVENT, currentContentViewName); currentSelectedSavedSearchId = null; return; } } DocumentModel savedSearch = documentManager.getDocument(new IdRef(selectedSavedSearchId)); loadSavedSearch(savedSearch); } protected void resetCurrentContentViewWorkingList() { if (currentContentViewName != null) { ContentView contentView = contentViewActions.getContentView(currentContentViewName); if (contentView != null) { documentsListsManager.resetWorkingList(contentView.getSelectionListName()); } } } public void loadSavedSearch(DocumentModel searchDocument) throws UnsupportedEncodingException { SearchUIService searchUIService = Framework.getService(SearchUIService.class); ContentViewState contentViewState = searchUIService.loadSearch(searchDocument); if (contentViewState != null) { ContentView contentView = contentViewActions.restoreContentView(contentViewState); currentContentViewName = contentView.getName(); Events.instance().raiseEvent(SEARCH_SELECTED_EVENT, currentContentViewName); } currentSelectedSavedSearchId = searchDocument.getId(); } public List<ContentViewHeader> getContentViewHeaders() { if (contentViewHeaders == null) { SearchUIService searchUIService = Framework.getService(SearchUIService.class); contentViewHeaders = searchUIService.getContentViewHeaders(actionContextProvider.createActionContext(), navigationContext.getCurrentDocument()); } return contentViewHeaders; } public void clearSearch() { if (currentContentViewName != null) { contentViewActions.reset(currentContentViewName); resetCurrentContentViewWorkingList(); } } public void refreshAndRewind() { String contentViewName = getCurrentContentViewName(); if (contentViewName != null) { contentViewActions.refreshAndRewind(contentViewName); resetCurrentContentViewWorkingList(); } } public void refreshAndRewindAndResetAggregates() { contentViewActions.resetAggregates(getCurrentContentViewName()); refreshAndRewind(); } /* * ----- Load / Save searches ----- */ public List<SelectItem> getAllSavedSearchesSelectItems() { List<SelectItem> items = new ArrayList<>(); // Add flagged content views SelectItemGroup flaggedGroup = new SelectItemGroup(messages.get(SEARCH_FILTERS_LABEL)); List<ContentViewHeader> flaggedSavedSearches = getContentViewHeaders(); List<SelectItem> flaggedSavedSearchesItems = convertCVToSelectItems(flaggedSavedSearches); flaggedGroup.setSelectItems( flaggedSavedSearchesItems.toArray(new SelectItem[flaggedSavedSearchesItems.size()])); items.add(flaggedGroup); // Add saved searches List<DocumentModel> userSavedSearches = getSavedSearches(); if (!userSavedSearches.isEmpty()) { SelectItemGroup userGroup = new SelectItemGroup(messages.get(SAVED_SEARCHES_LABEL)); List<SelectItem> userSavedSearchesItems = convertToSelectItems(userSavedSearches); userGroup.setSelectItems(userSavedSearchesItems.toArray(new SelectItem[userSavedSearchesItems.size()])); items.add(userGroup); } // Add shared searches List<DocumentModel> otherUsersSavedFacetedSearches = getSharedSearches(); if (!otherUsersSavedFacetedSearches.isEmpty()) { List<SelectItem> otherUsersSavedSearchesItems = convertToSelectItems(otherUsersSavedFacetedSearches); SelectItemGroup allGroup = new SelectItemGroup(messages.get(SHARED_SEARCHES_LABEL)); allGroup.setSelectItems( otherUsersSavedSearchesItems.toArray(new SelectItem[otherUsersSavedSearchesItems.size()])); items.add(allGroup); } return items; } protected List<DocumentModel> getSavedSearches() { SearchUIService searchUIService = Framework.getService(SearchUIService.class); return searchUIService.getCurrentUserSavedSearches(documentManager); } protected List<DocumentModel> getSharedSearches() { SearchUIService searchUIService = Framework.getService(SearchUIService.class); return searchUIService.getSharedSavedSearches(documentManager); } protected List<SelectItem> convertToSelectItems(List<DocumentModel> docs) { List<SelectItem> items = new ArrayList<>(); for (DocumentModel doc : docs) { items.add(new SelectItem(doc.getId(), doc.getTitle(), "")); } return items; } protected List<SelectItem> convertCVToSelectItems(List<ContentViewHeader> contentViewHeaders) { List<SelectItem> items = new ArrayList<>(); for (ContentViewHeader contentViewHeader : contentViewHeaders) { items.add(new SelectItem(contentViewHeader.getName(), messages.get(contentViewHeader.getTitle()), "")); } return items; } public String getSavedSearchTitle() { return savedSearchTitle; } public void setSavedSearchTitle(String savedSearchTitle) { this.savedSearchTitle = savedSearchTitle; } public String saveSearch() { ContentView contentView = contentViewActions.getContentView(getCurrentContentViewName()); if (contentView != null) { ContentViewState state = contentViewService.saveContentView(contentView); SearchUIService searchUIService = Framework.getService(SearchUIService.class); DocumentModel savedSearch = searchUIService.saveSearch(documentManager, state, savedSearchTitle); currentSelectedSavedSearchId = savedSearch.getId(); Events.instance().raiseEvent(SEARCH_SAVED_EVENT, savedSearch); savedSearchTitle = null; facesMessages.add(StatusMessage.Severity.INFO, messages.get(SEARCH_SAVED_LABEL)); } return null; } /** * Retsurns true if current search can be saved. * <p> * Returns false if current content view is waiting for a first execution. * * @since 7.4 */ public boolean getCanSaveSearch() { ContentView contentView = contentViewActions.getContentView(getCurrentContentViewName()); if (contentView != null) { boolean res = !contentView.isWaitForExecution() || contentView.isExecuted(); return res; } return false; } public void cancelSaveSearch() { savedSearchTitle = null; } /* * ----- Permanent links ----- */ public void setState(String state) throws UnsupportedEncodingException { if (isNotBlank(state)) { Long finalPageSize = null; if (!StringUtils.isBlank(pageSize)) { try { finalPageSize = Long.valueOf(pageSize); } catch (NumberFormatException e) { log.warn(String.format("Unable to parse '%s' parameter with value '%s'", PAGE_SIZE_PARAMETER, pageSize)); } } Long finalCurrentPage = null; if (!StringUtils.isBlank(currentPage)) { try { finalCurrentPage = Long.valueOf(currentPage); } catch (NumberFormatException e) { log.warn(String.format("Unable to parse '%s' parameter with value '%s'", CURRENT_PAGE_PARAMETER, currentPage)); } } String cvName = getCurrentContentViewName(); List<ContentViewHeader> contentViewHeaders = getContentViewHeaders(); if (cvName != null && contentViewHeaders != null) { boolean canRestore = false; for (ContentViewHeader contentViewHeader : getContentViewHeaders()) { if (cvName.equals(contentViewHeader.getName())) { canRestore = true; } } if (canRestore) { contentViewActions.restoreContentView(getCurrentContentViewName(), finalCurrentPage, finalPageSize, null, state); } else { invalidateContentViewsName(); } } } } public String getCurrentPage() { return currentPage; } public void setCurrentPage(String currentPage) { this.currentPage = currentPage; } public String getPageSize() { return pageSize; } public void setPageSize(String pageSize) { this.pageSize = pageSize; } public void setSearchTerm(String searchTerm) throws UnsupportedEncodingException { // If the search term is not defined, we don't do the logic if (!StringUtils.isEmpty(searchTerm)) { // By default, the "simple_search" content view is used currentContentViewName = SIMPLE_SEARCH_CONTENT_VIEW_NAME; // Create a ContentViewState ContentView cv = contentViewService.getContentView(SIMPLE_SEARCH_CONTENT_VIEW_NAME); DocumentModel searchDocumentModel = cv.getSearchDocumentModel(); // set the search term searchDocumentModel.setPropertyValue("default_search:ecm_fulltext", searchTerm); ContentViewState state = new ContentViewStateImpl(); state.setSearchDocumentModel(searchDocumentModel); state.setContentViewName(getCurrentContentViewName()); ContentView ccv = contentViewActions.restoreContentView(state); ccv.setExecuted(true); } } /** * Compute a permanent link for the current search. */ public String getSearchPermanentLinkUrl() throws UnsupportedEncodingException { // do not try to compute an URL if we don't have any CoreSession if (documentManager == null) { return null; } return generateSearchUrl(true); } /** * @return the URL of the search tab with the search term defined. */ public String getSearchTabUrl(String searchTerm) throws UnsupportedEncodingException { // do not try to compute an URL if we don't have any CoreSession if (documentManager == null) { return null; } // Set the value of the searched term this.searchTerm = searchTerm; return generateSearchUrl(false); } /** * Create the url to access the Search tab. * * @param withState If set to true, the state is added in the parameters. */ protected String generateSearchUrl(boolean withState) throws UnsupportedEncodingException { String currentContentViewName = getCurrentContentViewName(); DocumentModel currentDocument = navigationContext.getCurrentDocument(); DocumentView docView = computeDocumentView(currentDocument); docView.setViewId("search"); docView.addParameter(CONTENT_VIEW_NAME_PARAMETER, currentContentViewName); // Add the state if needed if (withState) { docView.addParameter(CONTENT_VIEW_STATE_PARAMETER, getJSONContentViewState()); } DocumentViewCodecManager documentViewCodecManager = Framework.getService(DocumentViewCodecManager.class); String url = documentViewCodecManager.getUrlFromDocumentView(SEARCH_CODEC, docView, true, BaseURL.getBaseURL()); return RestHelper.addCurrentConversationParameters(url); } protected DocumentView computeDocumentView(DocumentModel doc) { return new DocumentViewImpl(new DocumentLocationImpl(documentManager.getRepositoryName(), doc != null ? new PathRef(doc.getPathAsString()) : null)); } /* * ----- Simple Search ----- */ public String getSimpleSearchKeywords() { return simpleSearchKeywords; } public void setSimpleSearchKeywords(String simpleSearchKeywords) { this.simpleSearchKeywords = simpleSearchKeywords; } public void validateSimpleSearchKeywords(FacesContext context, UIComponent component, Object value) { if (!(value instanceof String) || StringUtils.isEmpty(((String) value).trim())) { FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context, "feedback.search.noKeywords"), null); // also add global message context.addMessage(null, message); throw new ValidatorException(message); } String[] keywords = ((String) value).trim().split(" "); for (String keyword : keywords) { if (keyword.startsWith("*")) { // Can't begin search with * character FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context, "feedback.search.star"), null); // also add global message context.addMessage(null, message); throw new ValidatorException(message); } } } public String doSimpleSearch() { setSearchMainTab(null); currentContentViewName = SIMPLE_SEARCH_CONTENT_VIEW_NAME; ContentView contentView = contentViewActions.getContentView(SIMPLE_SEARCH_CONTENT_VIEW_NAME); DocumentModel searchDoc = contentView.getSearchDocumentModel(); searchDoc.setPropertyValue("defaults:ecm_fulltext", simpleSearchKeywords); refreshAndRewind(); return "search"; } /* * ----- NXQL Search ----- */ public String getNxqlQuery() { return nxqlQuery; } public void setNxqlQuery(String nxqlQuery) { this.nxqlQuery = nxqlQuery; } public boolean isNxqlSearchSelected() { return NXQL_SEARCH_CONTENT_VIEW_NAME.equals(currentContentViewName); } @Begin(id = "#{conversationIdGenerator.currentOrNewMainConversationId}", join = true) public String loadPermanentLink(DocumentView docView) { restHelper.initContextFromRestRequest(docView); return "search"; } @Observer(value = LOCAL_CONFIGURATION_CHANGED) public void invalidateContentViewsName() { clearSearch(); contentViewHeaders = null; currentContentViewName = null; } @Observer(value = USER_ALL_DOCUMENT_TYPES_SELECTION_CHANGED) public void invalidateContentViewsNameIfChanged() { List<ContentViewHeader> temp = new ArrayList<>( Framework.getService(SearchUIService.class).getContentViewHeaders( actionContextProvider.createActionContext(), navigationContext.getCurrentDocument())); if (temp != null) { if (!temp.equals(contentViewHeaders)) { invalidateContentViewsName(); } if (!temp.isEmpty()) { String s = temp.get(0).getName(); if (s != null && !s.equals(currentContentViewName)) { invalidateContentViewsName(); } } } } /** * Reset attributes. */ @Observer(value = { EventNames.FLUSH_EVENT }, create = false) @BypassInterceptors public void resetOnFlush() { contentViewHeaders = null; currentSelectedSavedSearchId = null; currentContentViewName = null; nxqlQuery = DEFAULT_NXQL_QUERY; simpleSearchKeywords = ""; } public String getSearchTermParameter() { return SEARCH_TERM_PARAMETER; } /** * Triggers content view refresh/reset on saved search. * * @since 8.1 */ @Observer(value = { SEARCH_SAVED_EVENT }) public void onSearchSaved() { contentViewActions.refreshOnSeamEvent(SEARCH_SAVED_EVENT); contentViewActions.resetPageProviderOnSeamEvent(SEARCH_SAVED_EVENT); } }