/* * Copyright (c) 2010-2015 Evolveum * * 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. */ package com.evolveum.midpoint.web.component.search; import com.evolveum.midpoint.gui.api.component.BasePanel; import com.evolveum.midpoint.gui.api.model.LoadableModel; import com.evolveum.midpoint.gui.api.page.PageBase; import com.evolveum.midpoint.prism.ItemDefinition; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.query.ObjectQuery; import com.evolveum.midpoint.schema.constants.ObjectTypes; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.web.component.AjaxButton; import com.evolveum.midpoint.web.component.AjaxSubmitButton; import com.evolveum.midpoint.web.component.input.TextPanel; import com.evolveum.midpoint.web.component.menu.cog.InlineMenuItem; import com.evolveum.midpoint.web.component.menu.cog.InlineMenuItemAction; import com.evolveum.midpoint.web.component.menu.cog.MenuLinkPanel; import com.evolveum.midpoint.web.component.prism.InputPanel; import com.evolveum.midpoint.web.component.util.VisibleEnableBehaviour; import com.evolveum.midpoint.web.page.admin.configuration.PageRepositoryQuery; import com.evolveum.midpoint.web.security.SecurityUtils; import com.evolveum.midpoint.xml.ns._public.common.common_3.SearchBoxModeType; import org.apache.commons.lang.StringUtils; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.attributes.ThrottlingSettings; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.ajax.markup.html.AjaxLink; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.CheckBox; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.util.time.Duration; import javax.xml.namespace.QName; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * @author Viliam Repan (lazyman) */ public class SearchPanel extends BasePanel<Search> { private static final Trace LOG = TraceManager.getTrace(SearchPanel.class); private static final String ID_FORM = "form"; private static final String ID_ITEMS = "items"; private static final String ID_ITEM = "item"; private static final String ID_SEARCH_CONTAINER = "searchContainer"; private static final String ID_SEARCH_SIMPLE = "searchSimple"; private static final String ID_SEARCH_BUTTON_BEFORE_DROPDOWN = "searchButtonBeforeDropdown"; private static final String ID_SEARCH_DROPDOWN = "searchDropdown"; private static final String ID_MORE = "more"; private static final String ID_POPOVER = "popover"; private static final String ID_ADD_TEXT = "addText"; private static final String ID_ADD = "add"; private static final String ID_CLOSE = "close"; private static final String ID_PROPERTIES = "properties"; private static final String ID_CHECK = "check"; private static final String ID_PROP_NAME = "propName"; private static final String ID_PROP_LINK = "propLink"; private static final String ID_PROP_LIST = "propList"; private static final String ID_ADVANCED = "advanced"; private static final String ID_FULL_TEXT = "fullText"; private static final String ID_BASIC_SEARCH = "basic"; private static final String ID_FULL_TEXT_CONTAINER = "fullTextContainer"; private static final String ID_LINKS_CONTAINER = "linksContainer"; private static final String ID_FULL_TEXT_FIELD = "fullTextField"; private static final String ID_ADVANCED_GROUP = "advancedGroup"; private static final String ID_MORE_GROUP = "moreGroup"; private static final String ID_ADVANCED_AREA = "advancedArea"; private static final String ID_ADVANCED_CHECK = "advancedCheck"; private static final String ID_ADVANCED_ERROR= "advancedError"; private static final String ID_MENU_ITEM = "menuItem"; private static final String ID_MENU_ITEM_BODY = "menuItemBody"; private LoadableModel<MoreDialogDto> moreDialogModel; boolean advancedSearch = true; boolean queryPlagroundAccessible; public SearchPanel(String id, IModel<Search> model) { this(id, model, true); } public SearchPanel(String id, IModel<Search> model, boolean advancedSearch) { super(id, model); this.advancedSearch = advancedSearch; queryPlagroundAccessible = SecurityUtils.isPageAuthorized(PageRepositoryQuery.class); initLayout(); } private void initLayout() { moreDialogModel = new LoadableModel<MoreDialogDto>(false) { @Override protected MoreDialogDto load() { MoreDialogDto dto = new MoreDialogDto(); dto.setProperties(createPropertiesList()); return dto; } }; Form form = new Form(ID_FORM); add(form); ListView items = new ListView<SearchItem>(ID_ITEMS, new PropertyModel<List<SearchItem>>(getModel(), Search.F_ITEMS)) { @Override protected void populateItem(ListItem<SearchItem> item) { SearchItemPanel searchItem = new SearchItemPanel(ID_ITEM, item.getModel()); item.add(searchItem); } }; items.add(createVisibleBehaviour(SearchBoxModeType.BASIC)); form.add(items); WebMarkupContainer moreGroup = new WebMarkupContainer(ID_MORE_GROUP); moreGroup.add(createVisibleBehaviour(SearchBoxModeType.BASIC)); form.add(moreGroup); AjaxLink more = new AjaxLink(ID_MORE) { @Override public void onClick(AjaxRequestTarget target) { Component button = SearchPanel.this.get(createComponentPath(ID_FORM, ID_MORE_GROUP, ID_MORE)); Component popover = SearchPanel.this.get(createComponentPath(ID_POPOVER)); togglePopover(target, button, popover, 14); } }; more.add(new VisibleEnableBehaviour() { @Override public boolean isVisible() { Search search = getModelObject(); return !search.getAvailableDefinitions().isEmpty(); } }); more.setOutputMarkupId(true); moreGroup.add(more); WebMarkupContainer searchContainer = new WebMarkupContainer(ID_SEARCH_CONTAINER); searchContainer.setOutputMarkupId(true); form.add(searchContainer); AjaxSubmitButton searchSimple = new AjaxSubmitButton(ID_SEARCH_SIMPLE) { @Override protected void onError(AjaxRequestTarget target, Form<?> form) { target.add(form); } @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { searchPerformed(target); } }; searchSimple.add(new VisibleEnableBehaviour() { @Override public boolean isEnabled() { if (SearchBoxModeType.BASIC.equals(getModelObject().getSearchType()) || SearchBoxModeType.FULLTEXT.equals(getModelObject().getSearchType())) { return true; } return false; } @Override public boolean isVisible() { return SearchBoxModeType.BASIC.equals(getModelObject().getSearchType()) || SearchBoxModeType.FULLTEXT.equals(getModelObject().getSearchType()); } }); searchSimple.setOutputMarkupId(true); searchContainer.add(searchSimple); WebMarkupContainer searchDropdown = new WebMarkupContainer(ID_SEARCH_DROPDOWN); searchDropdown.add(new VisibleEnableBehaviour() { @Override public boolean isVisible() { return SearchBoxModeType.ADVANCED.equals(getModelObject().getSearchType()) && queryPlagroundAccessible; } }); searchContainer.add(searchDropdown); AjaxSubmitButton searchButtonBeforeDropdown = new AjaxSubmitButton(ID_SEARCH_BUTTON_BEFORE_DROPDOWN) { @Override protected void onError(AjaxRequestTarget target, Form<?> form) { target.add(form); } @Override protected void onSubmit(AjaxRequestTarget target, Form<?> form) { searchPerformed(target); } }; searchButtonBeforeDropdown.add(new VisibleEnableBehaviour() { @Override public boolean isEnabled() { if (SearchBoxModeType.BASIC.equals(getModelObject().getSearchType()) || SearchBoxModeType.FULLTEXT.equals(getModelObject().getSearchType())) { return true; } Search search = getModelObject(); PrismContext ctx = getPageBase().getPrismContext(); return search.isAdvancedQueryValid(ctx); } }); searchDropdown.add(searchButtonBeforeDropdown); List<InlineMenuItem> searchItems = new ArrayList<>(); InlineMenuItem searchItem = new InlineMenuItem( createStringResource("SearchPanel.search"), new InlineMenuItemAction() { @Override public void onClick(AjaxRequestTarget target) { PrismContext ctx = getPageBase().getPrismContext(); if (getModelObject().isAdvancedQueryValid(ctx)) { searchPerformed(target); } } }); searchItems.add(searchItem); searchItem = new InlineMenuItem(createStringResource("SearchPanel.debug"), new InlineMenuItemAction() { @Override public void onClick(AjaxRequestTarget target) { debugPerformed(); } }); searchItems.add(searchItem); ListView<InlineMenuItem> li = new ListView<InlineMenuItem>(ID_MENU_ITEM, Model.ofList(searchItems)) { @Override protected void populateItem(ListItem<InlineMenuItem> item) { WebMarkupContainer menuItemBody = new MenuLinkPanel(ID_MENU_ITEM_BODY, item.getModel()); menuItemBody.setRenderBodyOnly(true); item.add(menuItemBody); } }; searchDropdown.add(li); WebMarkupContainer linksContainer = new WebMarkupContainer(ID_LINKS_CONTAINER); linksContainer.setOutputMarkupId(true); form.add(linksContainer); AjaxButton advanced = new AjaxButton(ID_ADVANCED, createStringResource("SearchPanel.advanced")) { @Override public void onClick(AjaxRequestTarget target) { searchTypeUpdated(target, SearchBoxModeType.ADVANCED); } }; advanced.add(new VisibleEnableBehaviour(){ @Override public boolean isVisible() { return !SearchBoxModeType.ADVANCED.equals(getModelObject().getSearchType()); } }); linksContainer.add(advanced); AjaxButton fullTextButton = new AjaxButton(ID_FULL_TEXT, createStringResource("SearchPanel.fullText")) { @Override public void onClick(AjaxRequestTarget target) { searchTypeUpdated(target, SearchBoxModeType.FULLTEXT); } }; fullTextButton.add(new VisibleEnableBehaviour(){ @Override public boolean isVisible() { return isFullTextSearchEnabled() && !SearchBoxModeType.FULLTEXT.equals(getModelObject().getSearchType()); } }); linksContainer.add(fullTextButton); AjaxButton basicSearchButton = new AjaxButton(ID_BASIC_SEARCH, createStringResource("SearchPanel.basic")) { @Override public void onClick(AjaxRequestTarget target) { searchTypeUpdated(target, SearchBoxModeType.BASIC); } }; basicSearchButton.add(new VisibleEnableBehaviour(){ @Override public boolean isVisible() { return !SearchBoxModeType.BASIC.equals(getModelObject().getSearchType()); } }); linksContainer.add(basicSearchButton); advanced.add(new AttributeAppender("style", new LoadableModel<String>() { @Override public String load() { return basicSearchButton.isVisible() ? "margin-top: -20px;" : "display: table-cell; vertical-align: top;"; } })); initPopover(); WebMarkupContainer fullTextContainer = new WebMarkupContainer(ID_FULL_TEXT_CONTAINER); fullTextContainer.add(new VisibleEnableBehaviour(){ private static final long serialVersionUID = 1L; @Override public boolean isVisible(){ return isFullTextSearchEnabled() && getModelObject().getSearchType().equals(SearchBoxModeType.FULLTEXT); } }); fullTextContainer.setOutputMarkupId(true); form.add(fullTextContainer); TextField fullTextInput = new TextField(ID_FULL_TEXT_FIELD, new PropertyModel<String>(getModel(), Search.F_FULL_TEXT)); fullTextInput.add(new AjaxFormComponentUpdatingBehavior("blur") { @Override protected void onUpdate(AjaxRequestTarget target) { } }); fullTextInput.add(new Behavior() { @Override public void bind(Component component) { super.bind( component ); component.add( AttributeModifier.replace( "onkeydown", Model.of("if(event.keyCode == 13) {$('[about=\"searchSimple\"]').click();}") ) ); } }); fullTextInput.setOutputMarkupId(true); fullTextInput.add(new AttributeAppender("placeholder", createStringResource("SearchPanel.fullTextSearch"))); fullTextInput.add(createVisibleBehaviour(SearchBoxModeType.FULLTEXT)); fullTextContainer.add(fullTextInput); WebMarkupContainer advancedGroup = new WebMarkupContainer(ID_ADVANCED_GROUP); advancedGroup.add(createVisibleBehaviour(SearchBoxModeType.ADVANCED)); advancedGroup.add(AttributeAppender.append("class", createAdvancedGroupStyle())); advancedGroup.setOutputMarkupId(true); form.add(advancedGroup); Label advancedCheck = new Label(ID_ADVANCED_CHECK); advancedCheck.add(AttributeAppender.append("class", createAdvancedGroupLabelStyle())); advancedGroup.add(advancedCheck); final TextArea advancedArea = new TextArea(ID_ADVANCED_AREA, new PropertyModel(getModel(), Search.F_ADVANCED_QUERY)); advancedArea.add(new AjaxFormComponentUpdatingBehavior("keyup") { @Override protected void onUpdate(AjaxRequestTarget target) { updateAdvancedArea(advancedArea, target); } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); attributes.setThrottlingSettings( new ThrottlingSettings(ID_ADVANCED_AREA, Duration.milliseconds(500), true)); } }); advancedGroup.add(advancedArea); Label advancedError = new Label(ID_ADVANCED_ERROR, new PropertyModel<String>(getModel(), Search.F_ADVANCED_ERROR)); advancedError.add(new VisibleEnableBehaviour() { @Override public boolean isVisible() { Search search = getModelObject(); if (!search.isShowAdvanced()) { return false; } return StringUtils.isNotEmpty(search.getAdvancedError()); } }); advancedGroup.add(advancedError); } private void debugPerformed() { Search search = getModelObject(); PageRepositoryQuery pageQuery; if (search != null) { ObjectTypes type = search.getType() != null ? ObjectTypes.getObjectType(search.getType()) : null; QName typeName = type != null ? type.getTypeQName() : null; String inner = search.getAdvancedQuery(); if (StringUtils.isNotBlank(inner)) { inner = "\n" + inner + "\n"; } else if (inner == null) { inner = ""; } pageQuery = new PageRepositoryQuery(typeName, "<query>" + inner + "</query>"); } else { pageQuery = new PageRepositoryQuery(); } SearchPanel.this.setResponsePage(pageQuery); } private IModel<String> createAdvancedGroupLabelStyle() { return new AbstractReadOnlyModel<String>() { @Override public String getObject() { Search search = getModelObject(); return StringUtils.isEmpty(search.getAdvancedError()) ? "fa-check-circle-o" : "fa-exclamation-triangle"; } }; } private IModel<String> createAdvancedGroupStyle() { return new AbstractReadOnlyModel<String>() { @Override public String getObject() { Search search = getModelObject(); return StringUtils.isEmpty(search.getAdvancedError()) ? "has-success" : "has-error"; } }; } private IModel<String> createAdvancedModel() { return new AbstractReadOnlyModel<String>() { @Override public String getObject() { Search search = getModelObject(); String key = search.isShowAdvanced() ? "SearchPanel.basic" : "SearchPanel.advanced"; return createStringResource(key).getString(); } }; } private VisibleEnableBehaviour createAdvancedVisibleBehaviour(final boolean showAdvanced) { return new VisibleEnableBehaviour() { @Override public boolean isVisible() { Search search = getModelObject(); return showAdvanced ? search.isShowAdvanced() : !search.isShowAdvanced(); } }; } private VisibleEnableBehaviour createVisibleBehaviour(SearchBoxModeType searchType) { return new VisibleEnableBehaviour() { @Override public boolean isVisible() { return getModelObject().getSearchType().equals(searchType); } }; } private void initPopover() { WebMarkupContainer popover = new WebMarkupContainer(ID_POPOVER); popover.setOutputMarkupId(true); add(popover); final WebMarkupContainer propList = new WebMarkupContainer(ID_PROP_LIST); propList.setOutputMarkupId(true); popover.add(propList); ListView properties = new ListView<Property>(ID_PROPERTIES, new PropertyModel<List<Property>>(moreDialogModel, MoreDialogDto.F_PROPERTIES)) { @Override protected void populateItem(final ListItem<Property> item) { CheckBox check = new CheckBox(ID_CHECK, new PropertyModel<Boolean>(item.getModel(), Property.F_SELECTED)); check.add(new AjaxFormComponentUpdatingBehavior("change") { @Override protected void onUpdate(AjaxRequestTarget target) { //nothing, just update model. } }); item.add(check); AjaxLink propLink = new AjaxLink(ID_PROP_LINK) { @Override public void onClick(AjaxRequestTarget target) { addOneItemPerformed(item.getModelObject(), target); } }; item.add(propLink); Label name = new Label(ID_PROP_NAME, new PropertyModel<>(item.getModel(), Property.F_NAME)); name.setRenderBodyOnly(true); propLink.add(name); item.add(new VisibleEnableBehaviour() { @Override public boolean isVisible() { Property property = item.getModelObject(); Search search = SearchPanel.this.getModelObject(); if (!search.getAvailableDefinitions().contains(property.getDefinition())) { return false; } MoreDialogDto dto = moreDialogModel.getObject(); String nameFilter = dto.getNameFilter(); String propertyName = property.getName().toLowerCase(); if (StringUtils.isNotEmpty(nameFilter) && !propertyName.contains(nameFilter.toLowerCase())) { return false; } return true; } }); } }; propList.add(properties); TextField addText = new TextField(ID_ADD_TEXT, new PropertyModel(moreDialogModel, MoreDialogDto.F_NAME_FILTER)); addText.add(new Behavior() { @Override public void bind(Component component) { super.bind( component ); component.add( AttributeModifier.replace( "onkeydown", Model.of("if(event.keyCode == 13) {event.preventDefault();}") ) ); } }); popover.add(addText); addText.add(new AjaxFormComponentUpdatingBehavior("keyup") { @Override protected void onUpdate(AjaxRequestTarget target) { target.add(propList); } }); popover.add(addText); AjaxButton add = new AjaxButton(ID_ADD, createStringResource("SearchPanel.add")) { @Override public void onClick(AjaxRequestTarget target) { addItemPerformed(target); } }; popover.add(add); AjaxButton close = new AjaxButton(ID_CLOSE, createStringResource("SearchPanel.close")) { @Override public void onClick(AjaxRequestTarget target) { closeMorePopoverPerformed(target); } }; popover.add(close); } private List<Property> createPropertiesList() { List<Property> list = new ArrayList<>(); Search search = getModelObject(); List<ItemDefinition> defs = search.getAllDefinitions(); for (ItemDefinition def : defs) { list.add(new Property(def)); } Collections.sort(list); return list; } private void addOneItemPerformed(Property property, AjaxRequestTarget target) { Search search = getModelObject(); SearchItem item = search.addItem(property.getDefinition()); item.setEditWhenVisible(true); moreDialogModel.reset(); refreshSearchForm(target); } private void addItemPerformed(AjaxRequestTarget target) { Search search = getModelObject(); MoreDialogDto dto = moreDialogModel.getObject(); for (Property property : dto.getProperties()) { if (!property.isSelected()) { continue; } search.addItem(property.getDefinition()); } moreDialogModel.reset(); refreshSearchForm(target); } private void closeMorePopoverPerformed(AjaxRequestTarget target) { String popoverId = get(ID_POPOVER).getMarkupId(); target.appendJavaScript("$('#" + popoverId + "').toggle();"); } void searchPerformed(AjaxRequestTarget target) { PageBase page = (PageBase) getPage(); PrismContext ctx = page.getPrismContext(); Search search = getModelObject(); ObjectQuery query = search.createObjectQuery(ctx); LOG.debug("Created query: {}", query); searchPerformed(query, target); } void refreshSearchForm(AjaxRequestTarget target) { target.add(get(ID_FORM), get(ID_POPOVER)); } public void searchPerformed(ObjectQuery query, AjaxRequestTarget target) { } public void togglePopover(AjaxRequestTarget target, Component button, Component popover, int paddingRight) { StringBuilder script = new StringBuilder(); script.append("toggleSearchPopover('"); script.append(button.getMarkupId()).append("','"); script.append(popover.getMarkupId()).append("',"); script.append(paddingRight).append(");"); target.appendJavaScript(script.toString()); } private void searchTypeUpdated(AjaxRequestTarget target, SearchBoxModeType searchType) { getModelObject().setSearchType(searchType); refreshSearchForm(target); } private void updateAdvancedArea(Component area, AjaxRequestTarget target) { Search search = getModelObject(); PrismContext ctx = getPageBase().getPrismContext(); search.isAdvancedQueryValid(ctx); target.prependJavaScript("storeTextAreaSize('" + area.getMarkupId() + "');"); target.appendJavaScript("restoreTextAreaSize('" + area.getMarkupId() + "');"); target.add( get(createComponentPath(ID_FORM, ID_ADVANCED_GROUP)), get(createComponentPath(ID_FORM, ID_SEARCH_CONTAINER))); } private boolean isFullTextSearchEnabled(){ return getModelObject().isFullTextSearchEnabled(); } }