1 | /* |
---|
2 | * Copyright (C) 2014 CLARIN |
---|
3 | * |
---|
4 | * This program is free software: you can redistribute it and/or modify |
---|
5 | * it under the terms of the GNU General Public License as published by |
---|
6 | * the Free Software Foundation, either version 3 of the License, or |
---|
7 | * (at your option) any later version. |
---|
8 | * |
---|
9 | * This program is distributed in the hope that it will be useful, |
---|
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
12 | * GNU General Public License for more details. |
---|
13 | * |
---|
14 | * You should have received a copy of the GNU General Public License |
---|
15 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
---|
16 | */ |
---|
17 | package eu.clarin.cmdi.vlo.wicket.panels.search; |
---|
18 | |
---|
19 | import com.google.common.collect.ImmutableList; |
---|
20 | import eu.clarin.cmdi.vlo.pojo.FacetSelection; |
---|
21 | import eu.clarin.cmdi.vlo.pojo.FieldValuesFilter; |
---|
22 | import eu.clarin.cmdi.vlo.pojo.FieldValuesOrder; |
---|
23 | import eu.clarin.cmdi.vlo.wicket.components.AjaxIndicatingForm; |
---|
24 | import eu.clarin.cmdi.vlo.wicket.components.FieldValueLabel; |
---|
25 | import eu.clarin.cmdi.vlo.wicket.components.FieldValueOrderSelector; |
---|
26 | import eu.clarin.cmdi.vlo.wicket.model.BridgeModel; |
---|
27 | import eu.clarin.cmdi.vlo.wicket.model.BridgeOuterModel; |
---|
28 | import eu.clarin.cmdi.vlo.wicket.provider.FacetFieldValuesProvider; |
---|
29 | import eu.clarin.cmdi.vlo.wicket.provider.FieldValueConverterProvider; |
---|
30 | import java.util.Collections; |
---|
31 | import org.apache.solr.client.solrj.response.FacetField; |
---|
32 | import org.apache.wicket.ajax.AjaxRequestTarget; |
---|
33 | import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; |
---|
34 | import org.apache.wicket.ajax.form.OnChangeAjaxBehavior; |
---|
35 | import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; |
---|
36 | import org.apache.wicket.ajax.markup.html.navigation.paging.AjaxPagingNavigator; |
---|
37 | import org.apache.wicket.extensions.markup.html.repeater.util.SortParam; |
---|
38 | import org.apache.wicket.markup.html.WebMarkupContainer; |
---|
39 | import org.apache.wicket.markup.html.basic.Label; |
---|
40 | import org.apache.wicket.markup.html.form.CheckBox; |
---|
41 | import org.apache.wicket.markup.html.form.DropDownChoice; |
---|
42 | import org.apache.wicket.markup.html.form.Form; |
---|
43 | import org.apache.wicket.markup.html.form.TextField; |
---|
44 | import org.apache.wicket.markup.html.link.Link; |
---|
45 | import org.apache.wicket.markup.html.panel.GenericPanel; |
---|
46 | import org.apache.wicket.markup.repeater.Item; |
---|
47 | import org.apache.wicket.markup.repeater.data.DataView; |
---|
48 | import org.apache.wicket.model.CompoundPropertyModel; |
---|
49 | import org.apache.wicket.model.IModel; |
---|
50 | import org.apache.wicket.model.Model; |
---|
51 | import org.apache.wicket.model.PropertyModel; |
---|
52 | import org.apache.wicket.spring.injection.annot.SpringBean; |
---|
53 | |
---|
54 | /** |
---|
55 | * A panel that shows all available values for a selected facet. Supports two |
---|
56 | * ordering modes (by name or result count) and dynamic filtering. |
---|
57 | * |
---|
58 | * TODO: group by first letter when sorted by name |
---|
59 | * |
---|
60 | * @author twagoo |
---|
61 | */ |
---|
62 | public abstract class AllFacetValuesPanel extends GenericPanel<FacetField> { |
---|
63 | |
---|
64 | @SpringBean |
---|
65 | private FieldValueConverterProvider fieldValueConverterProvider; |
---|
66 | |
---|
67 | private final FacetFieldValuesProvider valuesProvider; |
---|
68 | private final WebMarkupContainer valuesContainer; |
---|
69 | private final IModel<FieldValuesFilter> filterModel; |
---|
70 | |
---|
71 | /** |
---|
72 | * |
---|
73 | * @param id component id |
---|
74 | * @param model model for the facet field to show values for |
---|
75 | */ |
---|
76 | public AllFacetValuesPanel(String id, IModel<FacetField> model) { |
---|
77 | this(id, model, null); |
---|
78 | } |
---|
79 | |
---|
80 | /** |
---|
81 | * |
---|
82 | * @param id component id |
---|
83 | * @param model model for the facet field to show values for |
---|
84 | * @param filterModel model that holds a string to filter in (can be null) |
---|
85 | */ |
---|
86 | public AllFacetValuesPanel(String id, IModel<FacetField> model, IModel<FieldValuesFilter> filterModel) { |
---|
87 | super(id, model); |
---|
88 | |
---|
89 | if (filterModel != null) { |
---|
90 | this.filterModel = filterModel; |
---|
91 | } else { |
---|
92 | this.filterModel = Model.of(new FieldValuesFilter()); |
---|
93 | } |
---|
94 | |
---|
95 | // create a provider that shows all values and is sorted by name by default |
---|
96 | valuesProvider = new FacetFieldValuesProvider(model, Integer.MAX_VALUE, FieldValueOrderSelector.NAME_SORT, fieldValueConverterProvider) { |
---|
97 | |
---|
98 | @Override |
---|
99 | protected IModel<FieldValuesFilter> getFilterModel() { |
---|
100 | // filters the values |
---|
101 | return AllFacetValuesPanel.this.filterModel; |
---|
102 | } |
---|
103 | |
---|
104 | }; |
---|
105 | |
---|
106 | // create a container for the values to allow for AJAX updates |
---|
107 | valuesContainer = new WebMarkupContainer("facetValuesContainer"); |
---|
108 | valuesContainer.setOutputMarkupId(true); |
---|
109 | add(valuesContainer); |
---|
110 | |
---|
111 | // create the view of the actual values |
---|
112 | final DataView<FacetField.Count> valuesView = createValuesView("facetValue"); |
---|
113 | valuesContainer.add(new AjaxPagingNavigator("navigator", valuesView) { |
---|
114 | |
---|
115 | @Override |
---|
116 | protected void onConfigure() { |
---|
117 | super.onConfigure(); |
---|
118 | setVisible(valuesView.getPageCount() > 1); |
---|
119 | } |
---|
120 | |
---|
121 | }); |
---|
122 | valuesContainer.add(valuesView); |
---|
123 | |
---|
124 | // create the form for selection sort option and entering filter string |
---|
125 | final Form optionsForm = createOptionsForm("options"); |
---|
126 | optionsForm.setOutputMarkupId(true); |
---|
127 | add(optionsForm); |
---|
128 | } |
---|
129 | |
---|
130 | private DataView<FacetField.Count> createValuesView(String id) { |
---|
131 | final IModel<String> fieldNameModel = new PropertyModel<>(getModel(), "name"); |
---|
132 | return new DataView<FacetField.Count>(id, valuesProvider, ITEMS_PER_PAGE) { |
---|
133 | |
---|
134 | @Override |
---|
135 | protected void populateItem(final Item<FacetField.Count> item) { |
---|
136 | item.setDefaultModel(new CompoundPropertyModel<>(item.getModel())); |
---|
137 | |
---|
138 | // link to select an individual facet value |
---|
139 | final Link selectLink = new AjaxFallbackLink("facetSelect") { |
---|
140 | |
---|
141 | @Override |
---|
142 | public void onClick(AjaxRequestTarget target) { |
---|
143 | // call callback |
---|
144 | onValuesSelected( |
---|
145 | item.getModelObject().getFacetField().getName(), |
---|
146 | // for now only single values can be selected |
---|
147 | new FacetSelection(Collections.singleton(item.getModelObject().getName())), |
---|
148 | target); |
---|
149 | } |
---|
150 | }; |
---|
151 | item.add(selectLink); |
---|
152 | |
---|
153 | // 'name' field from Count (name of value) |
---|
154 | selectLink.add(new FieldValueLabel("name", fieldNameModel)); |
---|
155 | |
---|
156 | // 'count' field from Count (document count for value) |
---|
157 | item.add(new Label("count")); |
---|
158 | } |
---|
159 | }; |
---|
160 | } |
---|
161 | private static final int ITEMS_PER_PAGE = 250; |
---|
162 | |
---|
163 | private Form createOptionsForm(String id) { |
---|
164 | final Form options = new AjaxIndicatingForm(id); |
---|
165 | |
---|
166 | final DropDownChoice<SortParam<FieldValuesOrder>> sortSelect |
---|
167 | = new FieldValueOrderSelector("sort", new PropertyModel<SortParam<FieldValuesOrder>>(valuesProvider, "sort")); |
---|
168 | sortSelect.add(new UpdateOptionsFormBehavior(options)); |
---|
169 | options.add(sortSelect); |
---|
170 | |
---|
171 | final TextField filterField = new TextField<>("filter", new PropertyModel(filterModel, "name")); |
---|
172 | filterField.add(new AjaxFormComponentUpdatingBehavior("keyup") { |
---|
173 | |
---|
174 | @Override |
---|
175 | protected void onUpdate(AjaxRequestTarget target) { |
---|
176 | target.add(valuesContainer); |
---|
177 | } |
---|
178 | }); |
---|
179 | options.add(filterField); |
---|
180 | |
---|
181 | addOccurenceOptions(options); |
---|
182 | |
---|
183 | return options; |
---|
184 | } |
---|
185 | |
---|
186 | /** |
---|
187 | * Creates form controls for minimal occurrences options. |
---|
188 | * |
---|
189 | * This requires a 'bridge' construct to combine a checkbox to |
---|
190 | * enable/disable filtering by occurence altogether (which actually sets the |
---|
191 | * min occurence to 0) and a dropdown for the minimal number of occurences, |
---|
192 | * which only gets applied if the checkbox is ticket. |
---|
193 | * |
---|
194 | * The checkbox opens/closes a bridge between the minimal occurences in the |
---|
195 | * filter model and the model that holds the selected option (modulated via |
---|
196 | * the dropdown). |
---|
197 | * |
---|
198 | * @param options options form |
---|
199 | */ |
---|
200 | private void addOccurenceOptions(final Form options) { |
---|
201 | |
---|
202 | // Model that holds the actual number of occurences filtered on |
---|
203 | final IModel<Integer> minOccurenceModel = new PropertyModel<>(filterModel, "minimalOccurence"); |
---|
204 | // Model that represents the filter state ('bridge' between filter and selection) |
---|
205 | final IModel<Boolean> bridgeStateModel = Model.of(false); |
---|
206 | // Model that represents the *selected* number of minimal occurences (passes it on if not decoupled) |
---|
207 | final IModel<Integer> minOccurenceSelectModel = new BridgeOuterModel<>(minOccurenceModel, bridgeStateModel, 2); |
---|
208 | // Model that links the actual filter, selection and bridge (object opens and closes it) |
---|
209 | final IModel<Boolean> minOccurenceCheckBoxModel = new BridgeModel<>(minOccurenceModel, minOccurenceSelectModel, bridgeStateModel, 0); |
---|
210 | |
---|
211 | // checkbox to open and close the 'bridge' |
---|
212 | final CheckBox minOccurenceToggle = new CheckBox("minOccurrencesToggle", minOccurenceCheckBoxModel); |
---|
213 | minOccurenceToggle.add(new UpdateOptionsFormBehavior(options)); |
---|
214 | options.add(minOccurenceToggle); |
---|
215 | |
---|
216 | // Dropdown to select a value (which is applied to the filter if the 'bridge' is open) |
---|
217 | final DropDownChoice<Integer> minOccurence = new DropDownChoice<>("minOccurences", minOccurenceSelectModel, ImmutableList.of(2, 5, 10, 100, 1000)); |
---|
218 | minOccurence.add(new UpdateOptionsFormBehavior(options) { |
---|
219 | |
---|
220 | @Override |
---|
221 | protected void onUpdate(AjaxRequestTarget target) { |
---|
222 | super.onUpdate(target); |
---|
223 | // on change, apply to inner model ('open bridge') |
---|
224 | if (!minOccurenceCheckBoxModel.getObject()) { |
---|
225 | minOccurenceCheckBoxModel.setObject(true); |
---|
226 | } |
---|
227 | } |
---|
228 | |
---|
229 | }); |
---|
230 | options.add(minOccurence); |
---|
231 | } |
---|
232 | |
---|
233 | private class UpdateOptionsFormBehavior extends OnChangeAjaxBehavior { |
---|
234 | |
---|
235 | private final Form options; |
---|
236 | |
---|
237 | public UpdateOptionsFormBehavior(Form options) { |
---|
238 | this.options = options; |
---|
239 | } |
---|
240 | |
---|
241 | @Override |
---|
242 | protected void onUpdate(AjaxRequestTarget target) { |
---|
243 | target.add(options); |
---|
244 | target.add(valuesContainer); |
---|
245 | } |
---|
246 | |
---|
247 | } |
---|
248 | |
---|
249 | @Override |
---|
250 | public void detachModels() { |
---|
251 | super.detachModels(); |
---|
252 | filterModel.detach(); |
---|
253 | } |
---|
254 | |
---|
255 | /** |
---|
256 | * Callback triggered when values have been selected on this facet |
---|
257 | * |
---|
258 | * @param facet name of the facet this panel represents |
---|
259 | * @param values selected values |
---|
260 | * @param target Ajax target allowing for a partial update. May be null |
---|
261 | * (fallback)! |
---|
262 | */ |
---|
263 | protected abstract void onValuesSelected(String facet, FacetSelection values, AjaxRequestTarget target); |
---|
264 | |
---|
265 | } |
---|