/**
* Copyright 2012 Universitat Pompeu Fabra.
*
* 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 org.onexus.website.api.pages.search;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.IAjaxIndicatorAware;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteBehavior;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSettings;
import org.apache.wicket.extensions.ajax.markup.html.autocomplete.IAutoCompleteRenderer;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.form.RadioChoice;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.image.Image;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.EmptyPanel;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.request.Response;
import org.apache.wicket.util.string.Strings;
import org.onexus.collection.api.Collection;
import org.onexus.collection.api.ICollectionManager;
import org.onexus.collection.api.IEntity;
import org.onexus.collection.api.query.Contains;
import org.onexus.collection.api.query.OrderBy;
import org.onexus.collection.api.query.Query;
import org.onexus.collection.api.utils.EntityIterator;
import org.onexus.collection.api.utils.QueryUtils;
import org.onexus.resource.api.IResourceManager;
import org.onexus.resource.api.ORI;
import org.onexus.website.api.WebsiteApplication;
import org.onexus.website.api.pages.Page;
import org.onexus.website.api.pages.search.boxes.BoxesPanel;
import org.onexus.website.api.utils.panels.ondomready.OnDomReadyPanel;
import org.onexus.website.api.widgets.selection.FilterConfig;
import org.onexus.website.api.widgets.selection.FiltersWidgetConfig;
import org.onexus.website.api.widgets.selection.FiltersWidgetStatus;
import javax.inject.Inject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class SearchPage extends Page<SearchPageConfig, SearchPageStatus> implements IAjaxIndicatorAware {
@Inject
private ICollectionManager collectionManager;
@Inject
private IResourceManager resourceManager;
private transient FiltersWidgetStatus filtersStatus;
private transient FilterConfig userFilter;
private TextField<String> search;
private WebMarkupContainer indicator;
public SearchPage(String componentId, IModel<SearchPageStatus> statusModel) {
this(componentId, statusModel, true, true);
}
public SearchPage(String componentId, IModel<SearchPageStatus> statusModel, boolean showTypes, boolean showLogo) {
super(componentId, statusModel);
IModel<SearchPageStatus> pageStatusModel = new PropertyModel<SearchPageStatus>(this, "status");
indicator = new WebMarkupContainer("indicator");
indicator.setOutputMarkupId(true);
indicator.add(new Image("loading", OnDomReadyPanel.LOADING_IMAGE));
add(indicator);
WebMarkupContainer container = new WebMarkupContainer("container");
container.add(new AttributeModifier("class", showLogo ? "show-logo" : "hide-logo"));
Form form = new Form<SearchPageStatus>("form");
form.add(new AjaxButton("submit") {
@Override
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
setFiltersStatus(null);
userFilter = null;
SearchPage.this.onSubmit(SearchPage.this.getStatus(), SearchPage.this.getConfig().getWebsiteConfig().getORI().getParent(), userFilter);
target.add(SearchPage.this.get("boxes"));
}
@Override
protected void onError(AjaxRequestTarget target, Form<?> form) {
}
});
form.setMultiPart(true);
// By default use the first search type
List<SearchType> types = getConfig().getTypes();
if (getStatus().getType() == null && !types.isEmpty()) {
getStatus().setType(types.get(0));
}
search = new TextField<String>("search", new PropertyModel<String>(pageStatusModel, "search"));
search.setOutputMarkupId(true);
if (!Strings.isEmpty(getStatus().getType().getPlaceholder())) {
search.add(new AttributeModifier("placeholder", getStatus().getType().getPlaceholder()));
}
search.add(new AutoCompleteBehavior<IEntity>(new EntityRenderer(), new AutoCompleteSettings()) {
@Override
protected Iterator<IEntity> getChoices(String input) {
return getAutocompleteChoices(input);
}
@Override
protected String findIndicatorId() {
return null;
}
});
form.add(search);
// Filters list modal
final WebMarkupContainer widgetModal = new WebMarkupContainer("widgetModal");
widgetModal.setOutputMarkupId(true);
widgetModal.add(new Label("header", ""));
widgetModal.add(new EmptyPanel("widget"));
widgetModal.add(new AjaxLink<String>("close") {
@Override
public void onClick(AjaxRequestTarget target) {
target.appendJavaScript("$('#" + widgetModal.getMarkupId() + "').modal('hide')");
}
});
form.add(widgetModal);
final AjaxLink<String> list = new AjaxLink<String>("list") {
@Override
public void onClick(AjaxRequestTarget target) {
FiltersWidgetConfig filters = getStatus().getType().getFilters();
if (filters != null) {
FiltersWidgetStatus status = filters.createEmptyStatus();
status.setConfig(filters);
setFiltersStatus(status);
widgetModal.addOrReplace(new Label("header", filters.getTitle()));
widgetModal.addOrReplace(new SearchFiltersWidget("widget", new PropertyModel<FiltersWidgetStatus>(SearchPage.this, "filtersStatus")) {
@Override
protected void applyFilter(FilterConfig filterConfig, AjaxRequestTarget target) {
filterConfig.setDeletable(true);
search.setModelValue(new String[]{filterConfig.getName()});
target.add(search);
userFilter = filterConfig;
SearchPage.this.addOrReplace(internalBoxesPanel(filterConfig));
target.add(SearchPage.this.get("boxes"));
target.appendJavaScript("$('#" + widgetModal.getMarkupId() + "').modal('hide')");
}
});
target.add(widgetModal);
target.appendJavaScript("$('#" + widgetModal.getMarkupId() + "').modal('show')");
}
}
};
list.setOutputMarkupPlaceholderTag(true);
form.add(list);
FiltersWidgetConfig filters = getStatus().getType().getFilters();
if (filters == null) {
list.setVisible(false);
} else {
list.add(new AttributeModifier("rel", "tooltip"));
list.add(new AttributeModifier("title", filters.getTitle()));
list.setVisible(true);
}
setFiltersStatus(null);
// Choose type
RadioChoice<SearchType> typeSelect = new RadioChoice<SearchType>("type", new PropertyModel<SearchType>(pageStatusModel, "type"), types, new SearchTypeRenderer());
typeSelect.add(new AjaxFormChoiceComponentUpdatingBehavior() {
@Override
protected void onUpdate(AjaxRequestTarget target) {
getStatus().setSearch("");
if (!Strings.isEmpty(getStatus().getType().getPlaceholder())) {
search.add(new AttributeModifier("placeholder", getStatus().getType().getPlaceholder()));
} else {
search.add(new AttributeModifier("placeholder", ""));
}
SearchPage.this.addOrReplace(internalBoxesPanel());
target.add(search);
target.add(SearchPage.this.get("container").get("form").get("examplesContainer"));
target.add(SearchPage.this.get("boxes"));
FiltersWidgetConfig filters = getStatus().getType().getFilters();
if (filters == null) {
list.setVisible(false);
} else {
list.setVisible(true);
list.add(new AttributeModifier("title", filters.getTitle()));
}
setFiltersStatus(null);
target.add(list);
}
});
form.add(typeSelect);
typeSelect.setVisible(showTypes);
container.add(form);
add(container);
// Examples
WebMarkupContainer examples = new WebMarkupContainer("examplesContainer");
examples.setOutputMarkupId(true);
examples.add(new ListView<SearchExample>("examples", new ExamplesModel(new PropertyModel<SearchType>(pageStatusModel, "type"))) {
@Override
protected void populateItem(ListItem<SearchExample> item) {
AjaxLink<SearchExample> link = new AjaxLink<SearchExample>("link", item.getModel()) {
@Override
public void onClick(AjaxRequestTarget target) {
onSearch(target, getModelObject().getQuery());
}
};
link.add(new Label("label", new PropertyModel<String>(item.getModel(), "label")));
item.add(link);
WebMarkupContainer sep = new WebMarkupContainer("sep");
sep.setVisible(item.getIndex() + 1 != getModelObject().size());
item.add(sep);
}
});
form.add(examples);
add(internalBoxesPanel());
}
@Override
public String getAjaxIndicatorMarkupId() {
return indicator.getMarkupId();
}
public FiltersWidgetStatus getFiltersStatus() {
return filtersStatus;
}
public void setFiltersStatus(FiltersWidgetStatus filtersStatus) {
this.filtersStatus = filtersStatus;
}
private BoxesPanel internalBoxesPanel(FilterConfig filter) {
return newBoxesPanel(getStatus(), getConfig().getWebsiteConfig().getORI().getParent(), filter);
}
private BoxesPanel internalBoxesPanel() {
return newBoxesPanel(getStatus(), getConfig().getWebsiteConfig().getORI().getParent(), null);
}
private BoxesPanel newBoxesPanel(SearchPageStatus status, ORI baseUri, FilterConfig filter) {
return new BoxesPanel("boxes", status, baseUri, filter) {
@Override
protected void onDisambiguation(AjaxRequestTarget target, String query) {
onSearch(target, query);
}
};
}
protected void onSearch(AjaxRequestTarget target, String query) {
getStatus().setSearch(query);
addOrReplace(internalBoxesPanel());
target.add(search);
target.add(SearchPage.this.get("boxes"));
}
protected void onSubmit(SearchPageStatus status, ORI baseUri, FilterConfig filter) {
addOrReplace(newBoxesPanel(status, baseUri, filter));
}
private Iterator<IEntity> getAutocompleteChoices(String in) {
int lastComma = in.lastIndexOf(',');
String input = lastComma > -1 ? in.substring(lastComma + 1).trim() : in.trim();
Query query = new Query();
SearchType type = getStatus().getType();
ORI collectionUri = getAbsoluteUri(type.getCollection());
String collectionAlias = QueryUtils.newCollectionAlias(query, collectionUri);
query.setFrom(collectionAlias);
List<String> fieldList = type.getFieldsList();
query.addSelect(collectionAlias, fieldList);
for (String field : fieldList) {
QueryUtils.or(query, new Contains(collectionAlias, field, input));
}
query.addOrderBy(new OrderBy(collectionAlias, fieldList.get(0)));
query.setCount(10);
return new EntityIterator(collectionManager.load(query), collectionUri);
}
private ORI getAbsoluteUri(ORI partialUri) {
ORI baseUri = SearchPage.this.getConfig().getWebsiteConfig().getORI().getParent();
return partialUri.toAbsolute(baseUri);
}
private IResourceManager getResourceManager() {
if (resourceManager == null) {
WebsiteApplication.inject(this);
}
return resourceManager;
}
private class SearchTypeRenderer implements IChoiceRenderer<SearchType> {
@Override
public Object getDisplayValue(SearchType type) {
ORI collectionUri = type.getCollection();
Collection collection = getResourceManager().load(Collection.class, getAbsoluteUri(collectionUri));
if (collection == null) {
return collectionUri;
}
String title = collection.getTitle();
return title == null ? collection.getName() : title;
}
@Override
public String getIdValue(SearchType object, int index) {
return Integer.toString(index);
}
}
/**
* Generic IEntity renderer that show all the fields.
*/
private class EntityRenderer implements IAutoCompleteRenderer<IEntity> {
public final void render(final IEntity object, final Response response, final String criteria) {
String textValue = getTextValue(object, criteria);
if (textValue == null) {
throw new IllegalStateException(
"A call to textValue(Object) returned an illegal value: null for object: " +
object.toString());
}
textValue = textValue.replaceAll("\\\"", """);
response.write("<li textvalue=\"" + textValue + "\"");
response.write(">");
renderChoice(object, response, criteria);
response.write("</li>");
}
private String getTextValue(IEntity object, String in) {
int lastComma = in.lastIndexOf(',');
String criteria = lastComma > -1 ? in.substring(lastComma + 1).trim() : in.trim();
String previous = lastComma > -1 ? in.substring(0, lastComma) + ", " : "";
SearchType type = getStatus().getType();
List<String> fields = type.getKeysList();
for (String field : fields) {
String value = String.valueOf(object.get(field));
if (StringUtils.containsIgnoreCase(value, criteria)) {
return previous + value;
}
}
return previous + object.get(fields.get(0));
}
public final void renderHeader(final Response response) {
response.write("<ul>");
}
public final void renderFooter(final Response response, int count) {
response.write("</ul>");
}
protected void renderChoice(IEntity object, Response response, String criteria) {
SearchType type = getStatus().getType();
List<String> keys = type.getKeysList();
List<String> fields = type.getFieldsList();
boolean keyFieldAdded = false;
for (String field : fields) {
String value = String.valueOf(object.get(field));
if (StringUtils.containsIgnoreCase(value, criteria)) {
if (keys.contains(field)) {
keyFieldAdded = true;
}
renderValue(response, value, criteria, field);
}
}
if (!keyFieldAdded) {
String field = keys.get(0);
String value = String.valueOf(object.get(field));
renderValue(response, value, criteria, field);
}
}
private void renderValue(Response response, String value, String criteria, String field) {
String hlValue = value.replaceAll("(?i)(" + criteria + ")", "<strong>$1</strong>");
response.write("<span class='f'>" + field.toLowerCase() + ":</span>" + hlValue);
response.write("<br />");
}
}
private final class ExamplesModel extends AbstractReadOnlyModel<List<SearchExample>> {
private IModel<SearchType> model;
private ExamplesModel(IModel<SearchType> model) {
this.model = model;
}
@Override
public List<SearchExample> getObject() {
SearchType searchType = model.getObject();
if (searchType == null || searchType.getExamples() == null) {
return Collections.EMPTY_LIST;
}
List<SearchExample> values = new ArrayList<SearchExample>();
for (String value : searchType.getExamples().split(",")) {
String label = value.trim();
String query = label;
if (label.contains("|")) {
String[] split = label.split("\\|");
label = split[0].trim();
query = split[1].replace(';', ',');
}
values.add(new SearchExample(label, query));
}
return values;
}
}
private static class SearchExample implements Serializable {
private String label;
private String query;
public SearchExample() {
}
public SearchExample(String label, String query) {
this.label = label;
this.query = query;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
}
}