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;
041import java.util.Objects;
042
043import com.google.common.collect.HashMultimap;
044import com.google.gwt.dom.client.Element;
045import com.google.gwt.event.dom.client.FocusEvent;
046import com.google.gwt.event.dom.client.FocusHandler;
047import com.google.gwt.event.logical.shared.ValueChangeEvent;
048import com.google.gwt.event.logical.shared.ValueChangeHandler;
049import com.google.gwt.event.shared.HandlerRegistration;
050import com.google.gwt.user.client.ui.Composite;
051import com.google.gwt.user.client.ui.FlowPanel;
052import com.google.gwt.user.client.ui.Label;
053
054/**
055 * An attribute select widget acts as a select widget and consists of several attribute filter select boxes and one main select box, such
056 * that choosing values from the attribute filters restricts the available options in the main select box to those which
057 * have a matching value for every filter attribute.
058 *
059 * <p>All data related to the options and filter attributes must be passed into the constructor, this widget does not use any RPC calls.
060 */
061public class CmsAttributeSelectWidget extends Composite implements I_CmsEditWidget {
062
063    /**
064     * Class representing a pair of an attribute name and value, for use as a key in the option index.
065     */
066    class IndexKey {
067
068        /** The attribute name. */
069        private String m_name;
070
071        /** The attribute value. */
072        private String m_value;
073
074        /**
075         * Creates a new instance.
076         *
077         * @param name the attribute name
078         * @param value  the attribute value
079         */
080        public IndexKey(String name, String value) {
081
082            m_name = name;
083            m_value = value;
084        }
085
086        /**
087         * @see java.lang.Object#equals(java.lang.Object)
088         */
089        @Override
090        public boolean equals(Object o) {
091
092            if (!(o instanceof IndexKey)) {
093                return false;
094            }
095            IndexKey other = (IndexKey)o;
096            return other.m_name.equals(m_name) && other.m_value.equals(m_value);
097        }
098
099        /**
100         * @see java.lang.Object#hashCode()
101         */
102        @Override
103        public int hashCode() {
104
105            return (31 * m_name.hashCode()) + m_value.hashCode();
106        }
107    }
108
109    /** The panel containing everything else. */
110    protected FlowPanel m_root = new FlowPanel();
111
112    /** Tracks if the widget is active. */
113    private boolean m_active = true;
114
115    /** Map of attribute definitions by name. */
116    private Map<String, AttributeDefinition> m_attributeDefinitions = new HashMap<>();
117
118    /** Map of attribute select boxes by attribute name. */
119    private Map<String, CmsSelectBox> m_attributeSelects = new HashMap<>();
120
121    /** Value set from the outside. */
122    private String m_externalValue;
123
124    /** The main select box for actually choosing the widget value. */
125    private CmsSelectBox m_mainSelect;
126
127    /** An index for quickly locating all options with a given attribute value. */
128    private HashMultimap<IndexKey, String> m_optionIndex = HashMultimap.create();
129
130    /** Map of all options. */
131    private LinkedHashMap<String, OptionWithAttributes> m_options = new LinkedHashMap<>();
132
133    /**
134     * Creates a new instance.
135     *
136     * @param data the widget data
137     */
138    public CmsAttributeSelectWidget(I_CmsAttributeSelectData data) {
139
140        initWidget(m_root);
141        for (AttributeDefinition attrDef : data.getAttributeDefinitions()) {
142            m_attributeDefinitions.put(attrDef.getName(), attrDef);
143            CmsSelectBox selectBox = new CmsSelectBox();
144            LinkedHashMap<String, String> selectBoxOptions = new LinkedHashMap<>();
145            for (Option option : attrDef.getOptions()) {
146                selectBoxOptions.put(option.getValue(), option.getLabel());
147                if (option.getHelpText() != null) {
148                    selectBox.setTitle(option.getValue(), option.getHelpText());
149                }
150            }
151            selectBox.setItems(selectBoxOptions);
152            // set the value before adding the change handler, since we already call handleFilterChange() below to initialize the main select box
153            selectBox.setFormValueAsString(getDefaultOption(attrDef));
154            addFilterLine(attrDef.getLabel(), selectBox);
155            m_attributeSelects.put(attrDef.getName(), selectBox);
156            selectBox.addValueChangeHandler(event -> handleFilterChange());
157        }
158
159        m_mainSelect = new CmsSelectBox();
160        m_root.add(m_mainSelect);
161        LinkedHashMap<String, String> mainOptions = new LinkedHashMap<>();
162        for (OptionWithAttributes option : data.getOptions()) {
163            if (option.getHelpText() != null) {
164                // we only need to set all the help texts once, they will be preserved during option changes
165                m_mainSelect.setTitle(option.getValue(), option.getHelpText());
166            }
167            m_options.put(option.getValue(), option);
168            mainOptions.put(option.getValue(), option.getLabel());
169            for (String attribute : option.getAttributes().keySet()) {
170                for (String attrValue : option.getAttributes().get(attribute)) {
171                    m_optionIndex.put(new IndexKey(attribute, attrValue), option.getValue());
172                }
173            }
174        }
175        m_mainSelect.addValueChangeHandler(event -> fireChangeEvent());
176        handleFilterChange();
177    }
178
179    /**
180     * Adds the focus handler.
181     *
182     * @param handler the handler
183     * @return the handler registration
184     * @see com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.google.gwt.event.dom.client.FocusHandler)
185     */
186    public HandlerRegistration addFocusHandler(FocusHandler handler) {
187
188        return addDomHandler(handler, FocusEvent.getType());
189    }
190
191    /**
192     * Adds the value change handler.
193     *
194     * @param handler the handler
195     * @return the handler registration
196     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
197     */
198    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<String> handler) {
199
200        return addHandler(handler, ValueChangeEvent.getType());
201    }
202
203    /**
204     * Represents a value change event.<p>
205     *
206     */
207    public void fireChangeEvent() {
208
209        String val = m_mainSelect.getFormValueAsString();
210        if (val != null) {
211            ValueChangeEvent.fire(this, val);
212        }
213
214    }
215
216    /**
217     * @see com.google.gwt.user.client.ui.HasValue#getValue()
218     */
219    public String getValue() {
220
221        String value = m_mainSelect.getFormValueAsString();
222        return value;
223    }
224
225    /**
226     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#isActive()
227     */
228    public boolean isActive() {
229
230        return m_active;
231    }
232
233    /**
234     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#onAttachWidget()
235     */
236    public void onAttachWidget() {
237
238        super.onAttach();
239
240    }
241
242    /**
243     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#owns(com.google.gwt.dom.client.Element)
244     */
245    public boolean owns(Element element) {
246
247        return false;
248    }
249
250    /**
251     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setActive(boolean)
252     */
253    public void setActive(boolean active) {
254
255        if (active == m_active) {
256            // Trying to set one value while initializing the widget can result in a different value being set.
257            // But at that time the event handler for the widget may not yet be set up correctly, so fireChangeEvent
258            // does nothing. So we have to fire the event here in setActive (which is called during widget
259            // initialization, after the change handler is set up).
260            if (active && !Objects.equals(getValue(), m_externalValue)) {
261                fireChangeEvent();
262            }
263            return;
264        }
265        m_active = active;
266        m_mainSelect.setEnabled(active);
267        for (CmsSelectBox attrSelect : m_attributeSelects.values()) {
268            attrSelect.setEnabled(active);
269        }
270        if (active) {
271            fireChangeEvent();
272        }
273    }
274
275    /**
276     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setName(java.lang.String)
277     */
278    public void setName(String name) {
279
280        // do nothing
281
282    }
283
284    /**
285     * @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object)
286     */
287    public void setValue(String value) {
288
289        setValue(value, false);
290
291    }
292
293    /**
294     * @see org.opencms.acacia.client.widgets.I_CmsEditWidget#setValue(java.lang.String, boolean)
295     */
296    public void setValue(String value, boolean fireEvent) {
297
298        m_externalValue = value;
299        OptionWithAttributes option = m_options.get(value);
300        if (option != null) {
301            for (String optionAttribute : option.getAttributes().keySet()) {
302                m_attributeSelects.get(optionAttribute).setFormValue(
303                    option.getAttributes().get(optionAttribute).get(0),
304                    false);
305            }
306            handleFilterChange();
307            m_mainSelect.setFormValue(value, false);
308        } else {
309            for (AttributeDefinition attrDef : m_attributeDefinitions.values()) {
310                CmsSelectBox select = m_attributeSelects.get(attrDef.getName());
311                // the editor initializes new widgets with the empty value, so we want to use the default option instead
312                // the neutral option for the attribute in that case.
313                String attrSelectValue = CmsStringUtil.isEmpty(value)
314                ? getDefaultOption(attrDef)
315                : getNeutralOption(attrDef);
316                select.setFormValue(attrSelectValue, false);
317            }
318            handleFilterChange();
319            m_mainSelect.setFormValue(value, false);
320        }
321        if (fireEvent) {
322            fireChangeEvent();
323        }
324    }
325
326    /**
327     * Gets the currently selected attribute filters.
328     *
329     * @return a map with attribute names as its keys and attribute values as its values
330     */
331    protected Map<String, String> getFilterAttributes() {
332
333        Map<String, String> result = new HashMap<>();
334        for (Map.Entry<String, CmsSelectBox> entry : m_attributeSelects.entrySet()) {
335            String filterValue = entry.getValue().getFormValueAsString();
336            result.put(entry.getKey(), filterValue);
337        }
338        return result;
339    }
340
341    /**
342     * Changes the set of available options in the main select box to those which match the currently
343     * selected attribute filters.
344     */
345    protected void handleFilterChange() {
346
347        Map<String, String> attributes = getFilterAttributes();
348        Map<String, String> newMainOptions = new LinkedHashMap<>();
349
350        // first generate a map of all options, then reduce the keys for each chosen filter using the option index
351
352        for (Map.Entry<String, OptionWithAttributes> option : m_options.entrySet()) {
353            newMainOptions.put(option.getKey(), option.getValue().getLabel());
354        }
355        for (Map.Entry<String, String> filterEntry : attributes.entrySet()) {
356            newMainOptions.keySet().retainAll(
357                m_optionIndex.get(new IndexKey(filterEntry.getKey(), filterEntry.getValue())));
358        }
359        m_mainSelect.setItems(newMainOptions);
360        fireChangeEvent();
361    }
362
363    /**
364     * Adds a new line with an attribute filter select box and a label.
365     *
366     * @param label the label
367     * @param selectBox the select box
368     */
369    private void addFilterLine(String label, CmsSelectBox selectBox) {
370
371        FlowPanel filterLine = new FlowPanel();
372        filterLine.add(new Label(label));
373        filterLine.add(selectBox);
374        filterLine.addStyleName(I_CmsWidgetsLayoutBundle.INSTANCE.widgetCss().attributeFilterLine());
375        m_root.add(filterLine);
376
377    }
378
379    /**
380     * Gets the default option for an attribute.
381     * @param attrDef the attribute
382     * @return the default option
383     */
384    private String getDefaultOption(AttributeDefinition attrDef) {
385
386        if (attrDef.getDefaultOption() != null) {
387            return attrDef.getDefaultOption();
388        }
389
390        return attrDef.getOptions().get(0).getValue();
391
392    }
393
394    /**
395     * Gets the filter attribute value to use if no other filter attribute value can be used.
396     *
397     * @param attrDef the attribute definition
398     *
399     * @return the neutral option
400     */
401    private String getNeutralOption(AttributeDefinition attrDef) {
402
403        if (attrDef.getNeutralOption() != null) {
404            return attrDef.getNeutralOption();
405        }
406        return attrDef.getOptions().get(0).getValue();
407    }
408
409}