001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.acacia.client.widgets;
029
030import org.opencms.acacia.client.css.I_CmsWidgetsLayoutBundle;
031import org.opencms.gwt.client.ui.input.CmsSelectBox;
032import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData;
033import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData.AttributeDefinition;
034import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData.Option;
035import org.opencms.gwt.shared.attributeselect.I_CmsAttributeSelectData.OptionWithAttributes;
036import org.opencms.util.CmsStringUtil;
037
038import java.util.HashMap;
039import java.util.LinkedHashMap;
040import java.util.Map;
041
042import com.google.common.collect.HashMultimap;
043import com.google.gwt.dom.client.Element;
044import com.google.gwt.event.dom.client.FocusEvent;
045import com.google.gwt.event.dom.client.FocusHandler;
046import com.google.gwt.event.logical.shared.ValueChangeEvent;
047import com.google.gwt.event.logical.shared.ValueChangeHandler;
048import com.google.gwt.event.shared.HandlerRegistration;
049import com.google.gwt.user.client.ui.Composite;
050import com.google.gwt.user.client.ui.FlowPanel;
051import com.google.gwt.user.client.ui.Label;
052
053/**
054 * An attribute select widget acts as a select widget and consists of several attribute filter select boxes and one main select box, such
055 * that choosing values from the attribute filters restricts the available options in the main select box to those which
056 * have a matching value for every filter attribute.
057 *
058 * <p>All data related to the options and filter attributes must be passed into the constructor, this widget does not use any RPC calls.
059 */
060public class CmsAttributeSelectWidget extends Composite implements I_CmsEditWidget {
061
062    /**
063     * Class representing a pair of an attribute name and value, for use as a key in the option index.
064     */
065    class IndexKey {
066
067        /** The attribute name. */
068        private String m_name;
069
070        /** The attribute value. */
071        private String m_value;
072
073        /**
074         * Creates a new instance.
075         *
076         * @param name the attribute name
077         * @param value  the attribute value
078         */
079        public IndexKey(String name, String value) {
080
081            m_name = name;
082            m_value = value;
083        }
084
085        /**
086         * @see java.lang.Object#equals(java.lang.Object)
087         */
088        @Override
089        public boolean equals(Object o) {
090
091            if (!(o instanceof IndexKey)) {
092                return false;
093            }
094            IndexKey other = (IndexKey)o;
095            return other.m_name.equals(m_name) && other.m_value.equals(m_value);
096        }
097
098        /**
099         * @see java.lang.Object#hashCode()
100         */
101        @Override
102        public int hashCode() {
103
104            return (31 * m_name.hashCode()) + m_value.hashCode();
105        }
106    }
107
108    /** The panel containing everything else. */
109    protected FlowPanel m_root = new FlowPanel();
110
111    /** Tracks if the widget is active. */
112    private boolean m_active = true;
113
114    /** Map of attribute definitions by name. */
115    private Map<String, AttributeDefinition> m_attributeDefinitions = new HashMap<>();
116
117    /** Map of attribute select boxes by attribute name. */
118    private Map<String, CmsSelectBox> m_attributeSelects = new HashMap<>();
119
120    /** The main select box for actually choosing the widget value. */
121    private CmsSelectBox m_mainSelect;
122
123    /** An index for quickly locating all options with a given attribute value. */
124    private HashMultimap<IndexKey, String> m_optionIndex = HashMultimap.create();
125
126    /** Map of all options. */
127    private LinkedHashMap<String, OptionWithAttributes> m_options = new LinkedHashMap<>();
128
129    /**
130     * Creates a new instance.
131     *
132     * @param data the widget data
133     */
134    public CmsAttributeSelectWidget(I_CmsAttributeSelectData data) {
135
136        initWidget(m_root);
137        for (AttributeDefinition attrDef : data.getAttributeDefinitions()) {
138            m_attributeDefinitions.put(attrDef.getName(), attrDef);
139            CmsSelectBox selectBox = new CmsSelectBox();
140            LinkedHashMap<String, String> selectBoxOptions = new LinkedHashMap<>();
141            for (Option option : attrDef.getOptions()) {
142                selectBoxOptions.put(option.getValue(), option.getLabel());
143                if (option.getHelpText() != null) {
144                    selectBox.setTitle(option.getValue(), option.getHelpText());
145                }
146            }
147            selectBox.setItems(selectBoxOptions);
148            // set the value before adding the change handler, since we already call handleFilterChange() below to initialize the main select box
149            selectBox.setFormValueAsString(getDefaultOption(attrDef));
150            addFilterLine(attrDef.getLabel(), selectBox);
151            m_attributeSelects.put(attrDef.getName(), selectBox);
152            selectBox.addValueChangeHandler(event -> handleFilterChange());
153        }
154
155        m_mainSelect = new CmsSelectBox();
156        m_root.add(m_mainSelect);
157        LinkedHashMap<String, String> mainOptions = new LinkedHashMap<>();
158        for (OptionWithAttributes option : data.getOptions()) {
159            if (option.getHelpText() != null) {
160                // we only need to set all the help texts once, they will be preserved during option changes
161                m_mainSelect.setTitle(option.getValue(), option.getHelpText());
162            }
163            m_options.put(option.getValue(), option);
164            mainOptions.put(option.getValue(), option.getLabel());
165            for (String attribute : option.getAttributes().keySet()) {
166                for (String attrValue : option.getAttributes().get(attribute)) {
167                    m_optionIndex.put(new IndexKey(attribute, attrValue), option.getValue());
168                }
169            }
170        }
171        m_mainSelect.addValueChangeHandler(event -> fireChangeEvent());
172        handleFilterChange();
173    }
174
175    /**
176     * Adds the focus handler.
177     *
178     * @param handler the handler
179     * @return the handler registration
180     * @see com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.google.gwt.event.dom.client.FocusHandler)
181     */
182    public HandlerRegistration addFocusHandler(FocusHandler handler) {
183
184        return addDomHandler(handler, FocusEvent.getType());
185    }
186
187    /**
188     * Adds the value change handler.
189     *
190     * @param handler the handler
191     * @return the handler registration
192     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
193     */
194    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
195
196        return addHandler(handler, ValueChangeEvent.getType());
197    }
198
199    /**
200     * Represents a value change event.<p>
201     *
202     */
203    public void fireChangeEvent() {
204
205        String val = m_mainSelect.getFormValueAsString();
206        if (val != null) {
207            ValueChangeEvent.fire(this, val);
208        }
209
210    }
211
212    /**
213     * @see com.google.gwt.user.client.ui.HasValue#getValue()
214     */
215    public String getValue() {
216
217        return m_mainSelect.getFormValueAsString();
218    }
219
220    /**
221     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#isActive()
222     */
223    public boolean isActive() {
224
225        return m_active;
226    }
227
228    /**
229     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#onAttachWidget()
230     */
231    public void onAttachWidget() {
232
233        super.onAttach();
234
235    }
236
237    /**
238     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#owns(com.google.gwt.dom.client.Element)
239     */
240    public boolean owns(Element element) {
241
242        return false;
243    }
244
245    /**
246     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setActive(boolean)
247     */
248    public void setActive(boolean active) {
249
250        if (active == m_active) {
251            return;
252        }
253        m_active = active;
254        m_mainSelect.setEnabled(active);
255        for (CmsSelectBox attrSelect : m_attributeSelects.values()) {
256            attrSelect.setEnabled(active);
257        }
258        if (active) {
259            fireChangeEvent();
260        }
261    }
262
263    /**
264     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setName(java.lang.String)
265     */
266    public void setName(String name) {
267
268        // do nothing
269
270    }
271
272    /**
273     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object)
274     */
275    public void setValue(String value) {
276
277        setValue(value, false);
278
279    }
280
281    /**
282     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setValue(java.lang.String, boolean)
283     */
284    public void setValue(String value, boolean fireEvent) {
285
286        OptionWithAttributes option = m_options.get(value);
287        if (option != null) {
288            for (String optionAttribute : option.getAttributes().keySet()) {
289                m_attributeSelects.get(optionAttribute).setFormValue(
290                    option.getAttributes().get(optionAttribute).get(0),
291                    false);
292            }
293            handleFilterChange();
294            m_mainSelect.setFormValue(value, false);
295        } else {
296            for (AttributeDefinition attrDef : m_attributeDefinitions.values()) {
297                CmsSelectBox select = m_attributeSelects.get(attrDef.getName());
298                // the editor initializes new widgets with the empty value, so we want to use the default option instead
299                // the neutral option for the attribute in that case.
300                String attrSelectValue = CmsStringUtil.isEmpty(value)
301                ? getDefaultOption(attrDef)
302                : getNeutralOption(attrDef);
303                select.setFormValue(attrSelectValue, false);
304            }
305            handleFilterChange();
306            m_mainSelect.setFormValue(value, false);
307        }
308        if (fireEvent) {
309            fireChangeEvent();
310        }
311    }
312
313    /**
314     * Gets the currently selected attribute filters.
315     *
316     * @return a map with attribute names as its keys and attribute values as its values
317     */
318    protected Map<String, String> getFilterAttributes() {
319
320        Map<String, String> result = new HashMap<>();
321        for (Map.Entry<String, CmsSelectBox> entry : m_attributeSelects.entrySet()) {
322            String filterValue = entry.getValue().getFormValueAsString();
323            result.put(entry.getKey(), filterValue);
324        }
325        return result;
326    }
327
328    /**
329     * Changes the set of available options in the main select box to those which match the currently
330     * selected attribute filters.
331     */
332    protected void handleFilterChange() {
333
334        Map<String, String> attributes = getFilterAttributes();
335        Map<String, String> newMainOptions = new LinkedHashMap<>();
336
337        // first generate a map of all options, then reduce the keys for each chosen filter using the option index
338
339        for (Map.Entry<String, OptionWithAttributes> option : m_options.entrySet()) {
340            newMainOptions.put(option.getKey(), option.getValue().getLabel());
341        }
342        for (Map.Entry<String, String> filterEntry : attributes.entrySet()) {
343            newMainOptions.keySet().retainAll(
344                m_optionIndex.get(new IndexKey(filterEntry.getKey(), filterEntry.getValue())));
345        }
346        m_mainSelect.setItems(newMainOptions);
347        fireChangeEvent();
348    }
349
350    /**
351     * Adds a new line with an attribute filter select box and a label.
352     *
353     * @param label the label
354     * @param selectBox the select box
355     */
356    private void addFilterLine(String label, CmsSelectBox selectBox) {
357
358        FlowPanel filterLine = new FlowPanel();
359        filterLine.add(new Label(label));
360        filterLine.add(selectBox);
361        filterLine.addStyleName(I_CmsWidgetsLayoutBundle.INSTANCE.widgetCss().attributeFilterLine());
362        m_root.add(filterLine);
363
364    }
365
366    /**
367     * Gets the default option for an attribute.
368     * @param attrDef the attribute
369     * @return the default option
370     */
371    private String getDefaultOption(AttributeDefinition attrDef) {
372
373        if (attrDef.getDefaultOption() != null) {
374            return attrDef.getDefaultOption();
375        }
376
377        return attrDef.getOptions().get(0).getValue();
378
379    }
380
381    /**
382     * Gets the filter attribute value to use if no other filter attribute value can be used.
383     *
384     * @param attrDef the attribute definition
385     *
386     * @return the neutral option
387     */
388    private String getNeutralOption(AttributeDefinition attrDef) {
389
390        if (attrDef.getNeutralOption() != null) {
391            return attrDef.getNeutralOption();
392        }
393        return attrDef.getOptions().get(0).getValue();
394    }
395
396}