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.gwt.client.ui.input.form;
029
030import org.opencms.gwt.client.ui.input.I_CmsFormField;
031import org.opencms.gwt.client.ui.input.I_CmsFormWidget;
032import org.opencms.gwt.client.ui.input.I_CmsHasBlur;
033import org.opencms.gwt.client.ui.input.I_CmsStringModel;
034import org.opencms.gwt.client.util.CmsExtendedValueChangeEvent;
035import org.opencms.gwt.client.validation.CmsValidationController;
036import org.opencms.gwt.client.validation.I_CmsValidationHandler;
037import org.opencms.gwt.shared.CmsValidationResult;
038
039import java.util.ArrayList;
040import java.util.Collection;
041import java.util.Collections;
042import java.util.Date;
043import java.util.HashMap;
044import java.util.HashSet;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.Map;
048import java.util.Set;
049
050import com.google.common.collect.ArrayListMultimap;
051import com.google.common.collect.Multimap;
052import com.google.gwt.core.client.Scheduler;
053import com.google.gwt.core.client.Scheduler.ScheduledCommand;
054import com.google.gwt.event.dom.client.HasKeyPressHandlers;
055import com.google.gwt.event.dom.client.KeyCodes;
056import com.google.gwt.event.dom.client.KeyPressEvent;
057import com.google.gwt.event.dom.client.KeyPressHandler;
058import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
059import com.google.gwt.event.logical.shared.ValueChangeEvent;
060import com.google.gwt.event.logical.shared.ValueChangeHandler;
061
062/**
063 *
064 * This class acts as a container for form fields.<p>
065 *
066 * It is also responsible for collecting and validating the values of the form fields.
067 *
068 * @since 8.0.0
069 *
070 */
071public class CmsForm {
072
073    /** The set of fields which have been edited. */
074    protected Set<String> m_editedFields = new HashSet<String>();
075
076    /** A map from field ids to the corresponding widgets. */
077    protected Map<String, I_CmsFormField> m_fields = new LinkedHashMap<String, I_CmsFormField>();
078
079    /** The form handler. */
080    protected I_CmsFormHandler m_formHandler;
081
082    /** A flag which indicates whether the user has pressed enter in a widget. */
083    protected boolean m_pressedEnter;
084
085    /** A multimap from field groups to fields. */
086    private Multimap<String, I_CmsFormField> m_fieldsByGroup = ArrayListMultimap.create();
087
088    /** The fields indexed by model id. */
089    private Multimap<String, I_CmsFormField> m_fieldsByModelId = ArrayListMultimap.create();
090
091    /** The list of form reset handlers. */
092    private List<I_CmsFormResetHandler> m_resetHandlers = new ArrayList<I_CmsFormResetHandler>();
093
094    /** The server-side form validator class to use. */
095    private String m_validatorClass;
096
097    /** The form widget container. */
098    private A_CmsFormFieldPanel m_widget;
099
100    /**
101     * Creates a new form with an existing form widget container.<p>
102     *
103     * @param panel the form widget container
104     */
105    public CmsForm(A_CmsFormFieldPanel panel) {
106
107        m_widget = panel;
108    }
109
110    /**
111     * Creates a new form and optionally sets the form widget container to a simple form field panel.<p>
112     *
113     * @param initPanel if true, initializes the form widget container
114     */
115    public CmsForm(boolean initPanel) {
116
117        if (initPanel) {
118            setWidget(new CmsSimpleFormFieldPanel());
119        }
120    }
121
122    /**
123     * Adds a form field.<p>
124     *
125     * @param field the field to add
126     * @param initialValue the initial field value
127     */
128    public void addField(I_CmsFormField field, String initialValue) {
129
130        addField("", field, initialValue);
131    }
132
133    /**
134     * Adds a form field to the form.<p>
135     *
136     * @param fieldGroup the form field group key
137     * @param formField the form field which should be added
138     */
139    public void addField(String fieldGroup, final I_CmsFormField formField) {
140
141        initializeFormFieldWidget(formField);
142        m_fields.put(formField.getId(), formField);
143        String modelId = formField.getModelId();
144        m_fieldsByModelId.put(modelId, formField);
145        formField.getLayoutData().put("group", fieldGroup);
146        m_fieldsByGroup.put(fieldGroup, formField);
147        //m_widget.addField(formField);
148
149    }
150
151    /**
152     * Adds a form field to the form and sets its initial value.<p>
153     *
154     * @param fieldGroup the form field group key
155     * @param formField the form field which should be added
156     * @param initialValue the initial value of the form field, or null if the field shouldn't have an initial value
157     */
158    public void addField(String fieldGroup, I_CmsFormField formField, String initialValue) {
159
160        if (initialValue != null) {
161            formField.getWidget().setFormValueAsString(initialValue);
162        }
163        addField(fieldGroup, formField);
164    }
165
166    /**
167     * Adds a new form reset handler to the form.<p>
168     *
169     * @param handler the new form reset handler
170     */
171    public void addResetHandler(I_CmsFormResetHandler handler) {
172
173        m_resetHandlers.add(handler);
174    }
175
176    /**
177     * Collects all values from the form fields.<p>
178     *
179     * This method omits form fields whose values are null.
180     *
181     * @return a map of the form field values
182     */
183    public Map<String, String> collectValues() {
184
185        Map<String, String> result = new HashMap<String, String>();
186        for (Map.Entry<String, I_CmsFormField> entry : m_fields.entrySet()) {
187            String key = entry.getKey();
188            String value = null;
189            I_CmsFormField field = entry.getValue();
190            value = field.getModelValue();
191            result.put(key, value);
192        }
193        return result;
194    }
195
196    /**
197     * Returns the set of names of fields which have been edited by the user in the current form.<p>
198     *
199     * @return the set of names of fields edited by the user
200     */
201    public Set<String> getEditedFields() {
202
203        return m_editedFields;
204
205    }
206
207    /**
208     * Returns the form field with a given id.<p>
209     *
210     * @param id the id of the form field
211     *
212     * @return the form field with the given id, or null if no field was found
213     */
214    public I_CmsFormField getField(String id) {
215
216        return m_fields.get(id);
217    }
218
219    /**
220     * Returns a map of this form's field, indexed by their field name.<p>
221     *
222     * @return a map of form fields
223     */
224    public Map<String, I_CmsFormField> getFields() {
225
226        return Collections.unmodifiableMap(m_fields);
227    }
228
229    /**
230     * Returns the field group ids of the form.<p>
231     *
232     * @return the field groups
233     */
234    public Collection<String> getGroups() {
235
236        return m_fieldsByGroup.keySet();
237    }
238
239    /**
240     * Returns the form widget container.<p>
241     *
242     * @return the form widget container
243     */
244    public A_CmsFormFieldPanel getWidget() {
245
246        return m_widget;
247    }
248
249    /**
250     * Passes this form's data to a form submit handler.<p>
251     *
252     * @param handler the form submit handler
253     */
254    public void handleSubmit(I_CmsFormSubmitHandler handler) {
255
256        Map<String, String> values = collectValues();
257        Set<String> editedFields = new HashSet<String>(getEditedFields());
258        editedFields.retainAll(values.keySet());
259        handler.onSubmitForm(this, values, editedFields);
260    }
261
262    /**
263     * Checks that no fields are invalid.<p>
264     *
265     * @return true if no fields are invalid.
266     */
267    public boolean noFieldsInvalid() {
268
269        return noFieldsInvalid(m_fields.values());
270    }
271
272    /**
273     * Returns true if none of the fields in a collection are marked as invalid.<p>
274     *
275     * @param fields the form fields
276     *
277     * @return true if none of the fields are invalid
278     */
279    public boolean noFieldsInvalid(Collection<I_CmsFormField> fields) {
280
281        for (I_CmsFormField field : fields) {
282            if (field.getValidationStatus().equals(I_CmsFormField.ValidationStatus.invalid)) {
283                return false;
284            }
285        }
286        return true;
287    }
288
289    /**
290     * Removes all fields for the given group.<p>
291     *
292     * @param group the group for which the fields should be removed
293     */
294    public void removeGroup(String group) {
295
296        if (m_fieldsByGroup.get(group) != null) {
297            List<I_CmsFormField> fieldsToRemove = new ArrayList<I_CmsFormField>(m_fieldsByGroup.get(group));
298            for (I_CmsFormField field : fieldsToRemove) {
299                removeField(field);
300            }
301        }
302        m_fieldsByGroup.removeAll(group);
303    }
304
305    /**
306     * Renders all fields.<p>
307     */
308    public void render() {
309
310        m_widget.renderFields(m_fields.values());
311    }
312
313    /**
314     * Renders the fields of the given group.<p>
315     *
316     * @param group the field group
317     */
318    public void renderGroup(String group) {
319
320        m_widget.rerenderFields(group, m_fieldsByGroup.get(group));
321
322    }
323
324    /**
325     * Sets the form handler for this form.<p>
326     *
327     * @param handler the form handler
328     */
329    public void setFormHandler(I_CmsFormHandler handler) {
330
331        m_formHandler = handler;
332    }
333
334    /**
335     * Sets the server-side form validator class to use.<p>
336     *
337     * @param validatorClass the form validator class name
338     */
339    public void setValidatorClass(String validatorClass) {
340
341        m_validatorClass = validatorClass;
342    }
343
344    /**
345     * Sets the form widget container.
346     *
347     * @param widget the form widget container
348     */
349    public void setWidget(A_CmsFormFieldPanel widget) {
350
351        assert m_widget == null;
352        m_widget = widget;
353    }
354
355    /**
356     * Performs an initial validation of all form fields.<p>
357     */
358    public void validateAllFields() {
359
360        CmsValidationController validationController = new CmsValidationController(
361            m_fields.values(),
362            createValidationHandler());
363        validationController.setFormValidator(m_validatorClass);
364        validationController.setFormValidatorConfig(createValidatorConfig());
365        startValidation(validationController);
366    }
367
368    /**
369     * Validates the form fields and submits their values if the validation was successful.<p>
370     */
371    public void validateAndSubmit() {
372
373        CmsValidationController validationController = new CmsValidationController(
374            m_fields.values(),
375            new I_CmsValidationHandler() {
376
377                /**
378                 * @see org.opencms.gwt.client.validation.I_CmsValidationHandler#onValidationFinished(boolean)
379                 */
380                public void onValidationFinished(boolean ok) {
381
382                    m_formHandler.onSubmitValidationResult(CmsForm.this, ok);
383                }
384
385                /**
386                 * @see org.opencms.gwt.client.validation.I_CmsValidationHandler#onValidationResult(java.lang.String, org.opencms.gwt.shared.CmsValidationResult)
387                 */
388                public void onValidationResult(String field, CmsValidationResult result) {
389
390                    updateFieldValidationStatus(field, result);
391
392                }
393
394            });
395        validationController.setFormValidator(m_validatorClass);
396        validationController.setFormValidatorConfig(createValidatorConfig());
397        startValidation(validationController);
398    }
399
400    /**
401     * Validates a single field.<p>
402     *
403     * @param field the field to validate
404     */
405    public void validateField(final I_CmsFormField field) {
406
407        CmsValidationController validationController = new CmsValidationController(field, createValidationHandler());
408        validationController.setFormValidator(m_validatorClass);
409        validationController.setFormValidatorConfig(createValidatorConfig());
410        startValidation(validationController);
411    }
412
413    /**
414     * Returns the configuration string for the server side form validator.<p>
415     *
416     * @return the form validator configuration string
417     */
418    protected String createValidatorConfig() {
419
420        return "";
421    }
422
423    /**
424     * The default keypress event handling function for form fields.<p>
425     *
426     * @param field the form field for which the event has been fired
427     *
428     * @param keyCode the key code
429     */
430    protected void defaultHandleKeyPress(I_CmsFormField field, int keyCode) {
431
432        I_CmsFormWidget widget = field.getWidget();
433        if (keyCode == KeyCodes.KEY_ENTER) {
434            m_pressedEnter = true;
435            if (widget instanceof I_CmsHasBlur) {
436                // force a blur because not all browsers send a change event if the user just presses enter in a field
437                ((I_CmsHasBlur)widget).blur();
438            }
439            // make sure that the flag is set to false again after the other events have been processed
440            Scheduler.get().scheduleDeferred(new ScheduledCommand() {
441
442                /**
443                 * @see com.google.gwt.core.client.Scheduler.ScheduledCommand#execute()
444                 */
445                public void execute() {
446
447                    m_pressedEnter = false;
448                }
449            });
450        }
451    }
452
453    /**
454     * Default handler for value change events of form fields.<p>
455     *
456     * @param field the form field for which the event has been fired
457     * @param inhibitValidation prevents validation of the edited field
458     *
459     * @param newValue the new value
460     */
461    protected void defaultHandleValueChange(I_CmsFormField field, String newValue, boolean inhibitValidation) {
462
463        m_editedFields.add(field.getId());
464        I_CmsStringModel model = field.getModel();
465
466        if (model != null) {
467            model.setValue(newValue, true);
468        }
469        field.setValidationStatus(I_CmsFormField.ValidationStatus.unknown);
470
471        // if the user presses enter, the keypressed event is fired before the change event,
472        // so we use a flag to keep track of whether enter was pressed.
473        if (!m_pressedEnter) {
474            if (!inhibitValidation) {
475                validateField(field);
476            }
477        } else {
478            validateAndSubmit();
479        }
480    }
481
482    /**
483     * Gets the fields with a given model id.<p>
484     *
485     * @param modelId the model id
486     *
487     * @return the fields with the given model id
488     */
489    protected Collection<I_CmsFormField> getFieldsByModelId(String modelId) {
490
491        return m_fieldsByModelId.get(modelId);
492    }
493
494    /**
495     * Updates the field validation status.<p>
496     *
497     * @param field the form field
498     * @param result the validation result
499     */
500    protected void updateFieldValidationStatus(I_CmsFormField field, CmsValidationResult result) {
501
502        if (result.hasNewValue()) {
503            if (field.getModel() != null) {
504                field.getModel().setValue(result.getNewValue(), true);
505            }
506            field.getWidget().setFormValueAsString(result.getNewValue());
507        }
508        String errorMessage = result.getErrorMessage();
509        field.getWidget().setErrorMessage(result.getErrorMessage());
510        field.setValidationStatus(
511            errorMessage == null ? I_CmsFormField.ValidationStatus.valid : I_CmsFormField.ValidationStatus.invalid);
512    }
513
514    /**
515     * Applies a validation result to a form field.<p>
516     *
517     * @param fieldId the field id to which the validation result should be applied
518     * @param result the result of the validation operation
519     */
520    protected void updateFieldValidationStatus(String fieldId, CmsValidationResult result) {
521
522        I_CmsFormField field = m_fields.get(fieldId);
523        updateFieldValidationStatus(field, result);
524    }
525
526    /**
527     * Updates the model validation status.<p>
528     *
529     * @param modelId the model id
530     * @param result the validation result
531     */
532    protected void updateModelValidationStatus(String modelId, CmsValidationResult result) {
533
534        Collection<I_CmsFormField> fields = getFieldsByModelId(modelId);
535        for (I_CmsFormField field : fields) {
536            updateFieldValidationStatus(field, result);
537        }
538
539    }
540
541    /**
542    * Creates a validation handler which updates the OK button state when validation results come in.<p>
543    *
544    * @return a validation handler
545    */
546    private I_CmsValidationHandler createValidationHandler() {
547
548        return new I_CmsValidationHandler() {
549
550            /**
551             * @see org.opencms.gwt.client.validation.I_CmsValidationHandler#onValidationFinished(boolean)
552             */
553            public void onValidationFinished(boolean ok) {
554
555                m_formHandler.onValidationResult(CmsForm.this, noFieldsInvalid(m_fields.values()));
556            }
557
558            /**
559             * @see org.opencms.gwt.client.validation.I_CmsValidationHandler#onValidationResult(java.lang.String, org.opencms.gwt.shared.CmsValidationResult)
560             */
561            public void onValidationResult(String fieldId, CmsValidationResult result) {
562
563                I_CmsFormField field = m_fields.get(fieldId);
564                String modelId = field.getModelId();
565                updateModelValidationStatus(modelId, result);
566            }
567        };
568    }
569
570    /**
571     * Initializes the widget for a new form field.<p>
572     *
573     * @param formField the form field whose widget should be initialized
574     */
575    @SuppressWarnings({"rawtypes", "unchecked"})
576    private void initializeFormFieldWidget(final I_CmsFormField formField) {
577
578        final I_CmsFormWidget widget = formField.getWidget();
579        if (widget instanceof HasValueChangeHandlers) {
580            ((HasValueChangeHandlers)widget).addValueChangeHandler(new ValueChangeHandler() {
581
582                /**
583                 * @see com.google.gwt.event.logical.shared.ValueChangeHandler#onValueChange(ValueChangeEvent event)
584                 */
585                public void onValueChange(ValueChangeEvent event) {
586
587                    boolean inhibitValidation = false;
588                    if (event instanceof CmsExtendedValueChangeEvent) {
589                        CmsExtendedValueChangeEvent extEvent = (CmsExtendedValueChangeEvent)event;
590                        inhibitValidation = extEvent.isInhibitValidation();
591                    }
592                    Object eventValue = event.getValue();
593                    if ((eventValue instanceof String) || (event.getValue() == null)) {
594                        // only makes sense for strings
595                        defaultHandleValueChange(formField, (String)(event.getValue()), inhibitValidation);
596                    } else if (eventValue instanceof Date) {
597                        defaultHandleValueChange(formField, "" + ((Date)eventValue).getTime(), inhibitValidation);
598                    } else if (eventValue instanceof Boolean) {
599                        defaultHandleValueChange(formField, "" + eventValue, inhibitValidation);
600                    }
601
602                }
603            });
604        }
605
606        if (widget instanceof HasKeyPressHandlers) {
607            ((HasKeyPressHandlers)widget).addKeyPressHandler(new KeyPressHandler() {
608
609                /**
610                 * @see com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google.gwt.event.dom.client.KeyPressEvent)
611                 */
612                public void onKeyPress(KeyPressEvent event) {
613
614                    int keyCode = event.getNativeEvent().getKeyCode();
615                    defaultHandleKeyPress(formField, keyCode);
616                }
617            });
618        }
619    }
620
621    /**
622     * Removes a field from the form's data structure (but not from the form's widget!).<p>
623     *
624     * @param field the field to remove
625     */
626    private void removeField(I_CmsFormField field) {
627
628        String id = field.getId();
629        m_fields.remove(id);
630        // *not* removing the field id from m_editedFields, because a field of the same id may
631        // be added later
632        m_fieldsByModelId.remove(field.getModelId(), field);
633        field.unbind();
634    }
635
636    /**
637     * Starts the validation of the form.<p>
638     *
639     * @param validationController the validation controller to use for the validation
640     */
641    private void startValidation(CmsValidationController validationController) {
642
643        validationController.startValidation();
644    }
645}